brut 0.0.1 → 0.0.2
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/.gitignore +6 -0
- data/Gemfile.lock +66 -1
- data/README.md +36 -0
- data/Rakefile +22 -0
- data/brut.gemspec +7 -0
- data/doc-src/architecture.md +102 -0
- data/doc-src/assets.md +98 -0
- data/doc-src/forms.md +214 -0
- data/doc-src/handlers.md +83 -0
- data/doc-src/javascript.md +265 -0
- data/doc-src/keyword-injection.md +183 -0
- data/doc-src/pages.md +210 -0
- data/doc-src/route-hooks.md +59 -0
- data/docs-todo.md +32 -0
- data/lib/brut/back_end/seed_data.rb +5 -1
- data/lib/brut/back_end/validator.rb +1 -1
- data/lib/brut/back_end/validators/form_validator.rb +31 -6
- data/lib/brut/cli/app.rb +100 -4
- data/lib/brut/cli/app_runner.rb +38 -5
- data/lib/brut/cli/apps/build_assets.rb +4 -6
- data/lib/brut/cli/apps/db.rb +2 -7
- data/lib/brut/cli/apps/scaffold.rb +413 -7
- data/lib/brut/cli/apps/test.rb +14 -1
- data/lib/brut/cli/command.rb +141 -6
- data/lib/brut/cli/error.rb +30 -3
- data/lib/brut/cli/execution_results.rb +44 -6
- data/lib/brut/cli/executor.rb +21 -1
- data/lib/brut/cli/options.rb +29 -2
- data/lib/brut/cli/output.rb +47 -0
- data/lib/brut/cli.rb +26 -0
- data/lib/brut/factory_bot.rb +2 -0
- data/lib/brut/framework/app.rb +38 -5
- data/lib/brut/framework/config.rb +97 -54
- data/lib/brut/framework/container.rb +97 -33
- data/lib/brut/framework/errors/abstract_method.rb +3 -7
- data/lib/brut/framework/errors/missing_parameter.rb +12 -0
- data/lib/brut/framework/errors/no_class_for_path.rb +25 -0
- data/lib/brut/framework/errors/not_found.rb +12 -2
- data/lib/brut/framework/errors/not_implemented.rb +14 -0
- data/lib/brut/framework/errors.rb +18 -0
- data/lib/brut/framework/fussy_type_enforcement.rb +17 -12
- data/lib/brut/framework/mcp.rb +106 -49
- data/lib/brut/framework/project_environment.rb +9 -0
- data/lib/brut/framework.rb +1 -0
- data/lib/brut/front_end/asset_metadata.rb +7 -1
- data/lib/brut/front_end/component.rb +129 -38
- data/lib/brut/front_end/components/constraint_violations.rb +57 -0
- data/lib/brut/front_end/components/form_tag.rb +23 -32
- data/lib/brut/front_end/components/i18n_translations.rb +34 -1
- data/lib/brut/front_end/components/input.rb +3 -0
- data/lib/brut/front_end/components/inputs/csrf_token.rb +2 -0
- data/lib/brut/front_end/components/inputs/radio_button.rb +41 -0
- data/lib/brut/front_end/components/inputs/select.rb +26 -2
- data/lib/brut/front_end/components/inputs/text_field.rb +27 -5
- data/lib/brut/front_end/components/inputs/textarea.rb +24 -4
- data/lib/brut/front_end/components/locale_detection.rb +8 -1
- data/lib/brut/front_end/components/page_identifier.rb +2 -0
- data/lib/brut/front_end/components/time.rb +95 -0
- data/lib/brut/front_end/components/traceparent.rb +22 -0
- data/lib/brut/front_end/download.rb +11 -0
- data/lib/brut/front_end/flash.rb +32 -0
- data/lib/brut/front_end/form.rb +109 -106
- data/lib/brut/front_end/forms/constraint_violation.rb +16 -11
- data/lib/brut/front_end/forms/input.rb +30 -42
- data/lib/brut/front_end/forms/input_declarations.rb +90 -0
- data/lib/brut/front_end/forms/input_definition.rb +45 -30
- data/lib/brut/front_end/forms/radio_button_group_input.rb +46 -0
- data/lib/brut/front_end/forms/radio_button_group_input_definition.rb +29 -0
- data/lib/brut/front_end/forms/select_input.rb +47 -0
- data/lib/brut/front_end/forms/select_input_definition.rb +27 -0
- data/lib/brut/front_end/forms/validity_state.rb +23 -9
- data/lib/brut/front_end/handler.rb +27 -8
- data/lib/brut/front_end/handlers/csp_reporting_handler.rb +4 -2
- data/lib/brut/front_end/handlers/instrumentation_handler.rb +99 -0
- data/lib/brut/front_end/handlers/locale_detection_handler.rb +9 -4
- data/lib/brut/front_end/handlers/missing_handler.rb +9 -0
- data/lib/brut/front_end/handling_results.rb +14 -4
- data/lib/brut/front_end/http_method.rb +13 -1
- data/lib/brut/front_end/http_status.rb +10 -0
- data/lib/brut/front_end/layouts/_internal.html.erb +68 -0
- data/lib/brut/front_end/middleware.rb +5 -0
- data/lib/brut/front_end/middlewares/annotate_brut_owned_paths.rb +15 -0
- data/lib/brut/front_end/middlewares/favicon.rb +16 -0
- data/lib/brut/front_end/middlewares/open_telemetry_span.rb +12 -0
- data/lib/brut/front_end/middlewares/reload_app.rb +27 -9
- data/lib/brut/front_end/page.rb +50 -11
- data/lib/brut/front_end/pages/_missing_page.html.erb +17 -0
- data/lib/brut/front_end/pages/missing_page.rb +36 -0
- data/lib/brut/front_end/request_context.rb +117 -8
- data/lib/brut/front_end/route_hook.rb +43 -1
- data/lib/brut/front_end/route_hooks/age_flash.rb +3 -2
- data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +8 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +18 -10
- data/lib/brut/front_end/route_hooks/locale_detection.rb +11 -5
- data/lib/brut/front_end/route_hooks/setup_request_context.rb +7 -4
- data/lib/brut/front_end/routing.rb +138 -31
- data/lib/brut/front_end/session.rb +86 -7
- data/lib/brut/front_end/template.rb +17 -2
- data/lib/brut/front_end/templates/block_filter.rb +4 -3
- data/lib/brut/front_end/templates/erb_parser.rb +1 -1
- data/lib/brut/front_end/templates/escapable_filter.rb +2 -0
- data/lib/brut/front_end/templates/html_safe_string.rb +30 -2
- data/lib/brut/front_end/templates/locator.rb +60 -0
- data/lib/brut/i18n/base_methods.rb +4 -0
- data/lib/brut/i18n.rb +1 -0
- data/lib/brut/instrumentation/logger_span_exporter.rb +75 -0
- data/lib/brut/instrumentation/open_telemetry.rb +107 -0
- data/lib/brut/instrumentation.rb +4 -6
- data/lib/brut/junk_drawer.rb +54 -4
- data/lib/brut/sinatra_helpers.rb +42 -38
- data/lib/brut/spec_support/clock_support.rb +6 -0
- data/lib/brut/spec_support/component_support.rb +53 -26
- data/lib/brut/spec_support/e2e_test_server.rb +82 -0
- data/lib/brut/spec_support/enhanced_node.rb +45 -0
- data/lib/brut/spec_support/general_support.rb +14 -3
- data/lib/brut/spec_support/handler_support.rb +2 -0
- data/lib/brut/spec_support/matcher.rb +6 -3
- data/lib/brut/spec_support/matchers/be_page_for.rb +3 -2
- data/lib/brut/spec_support/matchers/have_constraint_violation.rb +22 -9
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +9 -2
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +24 -0
- data/lib/brut/spec_support/matchers/have_link_to.rb +14 -0
- data/lib/brut/spec_support/matchers/have_redirected_to.rb +30 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +4 -11
- data/lib/brut/spec_support/matchers/have_returned_http_status.rb +16 -8
- data/lib/brut/spec_support/rspec_setup.rb +182 -0
- data/lib/brut/spec_support.rb +8 -3
- data/lib/brut/version.rb +2 -1
- data/lib/brut.rb +28 -5
- data/lib/sequel/extensions/brut_instrumentation.rb +5 -27
- data/lib/sequel/extensions/brut_migrations.rb +18 -8
- data/lib/sequel/plugins/created_at.rb +2 -0
- data/lib/sequel/plugins/external_id.rb +39 -1
- data/lib/sequel/plugins/find_bang.rb +4 -1
- metadata +140 -13
- data/lib/brut/back_end/action.rb +0 -3
- data/lib/brut/back_end/result.rb +0 -46
- data/lib/brut/front_end/components/timestamp.rb +0 -33
- data/lib/brut/instrumentation/basic.rb +0 -66
- data/lib/brut/instrumentation/event.rb +0 -19
- data/lib/brut/instrumentation/http_event.rb +0 -5
- data/lib/brut/instrumentation/subscriber.rb +0 -41
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
require_relative "flash_support"
|
|
2
|
+
require_relative "clock_support"
|
|
2
3
|
require_relative "session_support"
|
|
3
4
|
module Brut::SpecSupport::HandlerSupport
|
|
4
5
|
include Brut::SpecSupport::FlashSupport
|
|
6
|
+
include Brut::SpecSupport::ClockSupport
|
|
5
7
|
include Brut::SpecSupport::SessionSupport
|
|
6
8
|
end
|
|
7
9
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
module Brut::SpecSupport::Matchers
|
|
2
2
|
end
|
|
3
|
+
require_relative "matchers/be_a_bug"
|
|
4
|
+
require_relative "matchers/be_page_for"
|
|
5
|
+
require_relative "matchers/be_routing_for"
|
|
3
6
|
require_relative "matchers/have_constraint_violation"
|
|
4
7
|
require_relative "matchers/have_html_attribute"
|
|
5
|
-
require_relative "matchers/
|
|
6
|
-
require_relative "matchers/
|
|
8
|
+
require_relative "matchers/have_i18n_string"
|
|
9
|
+
require_relative "matchers/have_redirected_to"
|
|
7
10
|
require_relative "matchers/have_rendered"
|
|
8
11
|
require_relative "matchers/have_returned_http_status"
|
|
9
|
-
require_relative "matchers/
|
|
12
|
+
require_relative "matchers/have_link_to"
|
|
@@ -6,9 +6,10 @@ RSpec::Matchers.define :be_page_for do |klass|
|
|
|
6
6
|
failure_message do |page|
|
|
7
7
|
meta = page.locator("meta[name='class']")
|
|
8
8
|
if meta.count == 0
|
|
9
|
-
"Could not find <meta name='class'> on the page:\n\n#{page.content}"
|
|
9
|
+
"Could not find <meta name='class'> on the page, which is what's needed to know what page we are on:\n\n#{page.content}"
|
|
10
10
|
else
|
|
11
|
-
|
|
11
|
+
page_name = meta.get_attribute("content")
|
|
12
|
+
"Expected to be on page #{klass.name}, but we seem to be on page #{page_name}, based on this meta tag:\n\n#{meta.evaluate('e => e.outerHTML')}"
|
|
12
13
|
end
|
|
13
14
|
end
|
|
14
15
|
end
|
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
RSpec::Matchers.define :have_constraint_violation do |field,key
|
|
1
|
+
RSpec::Matchers.define :have_constraint_violation do |field,key:,index:nil|
|
|
2
2
|
match do |form|
|
|
3
|
-
Brut::SpecSupport::Matchers::HaveConstraintViolation.new(form,field,key).matches?
|
|
3
|
+
Brut::SpecSupport::Matchers::HaveConstraintViolation.new(form,field,key,index).matches?
|
|
4
4
|
end
|
|
5
5
|
|
|
6
6
|
failure_message do |form|
|
|
7
|
-
analysis = Brut::SpecSupport::Matchers::HaveConstraintViolation.new(form,field,key)
|
|
7
|
+
analysis = Brut::SpecSupport::Matchers::HaveConstraintViolation.new(form,field,key,index)
|
|
8
8
|
if analysis.found_field?
|
|
9
|
-
"#{field} did not have #{key} as a violation. These keys were found: #{analysis.keys_on_field_found.map(&:to_s).join(", ")}"
|
|
9
|
+
"Field '#{field}' did not have key '#{key}' as a violation. These keys were found: #{analysis.keys_on_field_found.map(&:to_s).join(", ")}"
|
|
10
10
|
else
|
|
11
|
-
|
|
11
|
+
field_searched_for = if index.nil?
|
|
12
|
+
field
|
|
13
|
+
else
|
|
14
|
+
"#{field}, index #{index}"
|
|
15
|
+
end
|
|
16
|
+
fields_with_errors = analysis.fields_found.map { |(field,index)|
|
|
17
|
+
if index.nil?
|
|
18
|
+
field
|
|
19
|
+
else
|
|
20
|
+
"#{field}, index #{index}"
|
|
21
|
+
end
|
|
22
|
+
}.join(", ")
|
|
23
|
+
"Field '#{field_searched_for}' had no errors. These fields DID: #{fields_with_errors}"
|
|
12
24
|
end
|
|
13
25
|
end
|
|
14
26
|
|
|
@@ -21,21 +33,22 @@ class Brut::SpecSupport::Matchers::HaveConstraintViolation
|
|
|
21
33
|
attr_reader :fields_found
|
|
22
34
|
attr_reader :keys_on_field_found
|
|
23
35
|
|
|
24
|
-
def initialize(form, field, key)
|
|
36
|
+
def initialize(form, field, key, index)
|
|
25
37
|
if !form.kind_of?(Brut::FrontEnd::Form)
|
|
26
38
|
raise "#{self.class} only works with forms, not #{form.class}"
|
|
27
39
|
end
|
|
28
40
|
@form = form
|
|
29
41
|
@field = field.to_s
|
|
30
42
|
@key = key.to_s
|
|
43
|
+
@index = index || 0
|
|
31
44
|
|
|
32
45
|
@matches = false
|
|
33
46
|
@found_field = false
|
|
34
47
|
@fields_found = Set.new
|
|
35
48
|
@keys_on_field_found = Set.new
|
|
36
49
|
|
|
37
|
-
@form.constraint_violations.each do |input_name, constraint_violations|
|
|
38
|
-
if input_name.to_s == @field
|
|
50
|
+
@form.constraint_violations.each do |input_name, (constraint_violations, index)|
|
|
51
|
+
if input_name.to_s == @field && index == @index
|
|
39
52
|
@found_field = true
|
|
40
53
|
constraint_violations.each do |constraint_violation|
|
|
41
54
|
if constraint_violation.key.to_s == @key
|
|
@@ -45,7 +58,7 @@ class Brut::SpecSupport::Matchers::HaveConstraintViolation
|
|
|
45
58
|
end
|
|
46
59
|
end
|
|
47
60
|
else
|
|
48
|
-
@fields_found << input_name.to_s
|
|
61
|
+
@fields_found << [ input_name.to_s, index ]
|
|
49
62
|
end
|
|
50
63
|
end
|
|
51
64
|
end
|
|
@@ -38,8 +38,15 @@ class Brut::SpecSupport::Matchers::HaveHTMLAttribute
|
|
|
38
38
|
else
|
|
39
39
|
result = result.first
|
|
40
40
|
end
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
else
|
|
42
|
+
object_to_check = if result.kind_of?(SimpleDelegator)
|
|
43
|
+
result.__getobj__
|
|
44
|
+
else
|
|
45
|
+
result
|
|
46
|
+
end
|
|
47
|
+
if !object_to_check.kind_of?(Nokogiri::XML::Element)
|
|
48
|
+
@error = "Received a #{result.class} instead of a NodeSet or Element, as could be returned by `.css(...)`"
|
|
49
|
+
end
|
|
43
50
|
end
|
|
44
51
|
if !@error
|
|
45
52
|
if attribute.kind_of?(Hash)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
RSpec::Matchers.define :have_i18n_string do |key,**args|
|
|
2
|
+
include Brut::I18n::ForHTML
|
|
3
|
+
match do |nokogiri_node|
|
|
4
|
+
|
|
5
|
+
text = nokogiri_node.text.strip
|
|
6
|
+
i18n_string = t(key,**args).to_s
|
|
7
|
+
|
|
8
|
+
text == i18n_string
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
failure_message do |nokogiri_node|
|
|
12
|
+
text = nokogiri_node.text.strip
|
|
13
|
+
begin
|
|
14
|
+
"Expected '#{text}' to be '#{t(key,**args)}'\n#{nokogiri_node.to_html}\n"
|
|
15
|
+
rescue => ex
|
|
16
|
+
"I18n key '#{key}' could not be found: #{ex.message}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
failure_message_when_negated do |nokogiri_node|
|
|
21
|
+
"Did not expect node's text to be '#{t(key,**args)}'\n#{nokogiri_node.to_html}\n"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
RSpec::Matchers.define :have_link_to do |page_klass,**args|
|
|
2
|
+
match do |node|
|
|
3
|
+
node.css("a[href='#{page_klass.routing(**args)}']").any?
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
failure_message do |node|
|
|
7
|
+
links = node.css("a").map(&:to_html)
|
|
8
|
+
"Did not find link to #{page_klass.routing(**args)}. Found these links: #{links.join(',')}"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
failure_message_when_negated do |result|
|
|
12
|
+
"Did not expect to find link to #{page_klass.routing(**args)}."
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
RSpec::Matchers.define :have_redirected_to do |page_or_uri,**page_params|
|
|
2
|
+
match do |result|
|
|
3
|
+
if page_or_uri.kind_of?(URI)
|
|
4
|
+
if !page_params.empty?
|
|
5
|
+
raise "have_redirected_to, when given a URI, must NOT be given parameters. Got '#{page_params}'"
|
|
6
|
+
end
|
|
7
|
+
result == page_or_uri
|
|
8
|
+
elsif page_or_uri.ancestors.include?(Brut::FrontEnd::Page)
|
|
9
|
+
result == page_or_uri.routing(**page_params)
|
|
10
|
+
else
|
|
11
|
+
raise "have_redirected_to must be given a URI or a Brut::FrontEnd::Page class, got #{page_or_uri.class}"
|
|
12
|
+
end
|
|
13
|
+
rescue Brut::Framework::Errors::MissingParameter => ex
|
|
14
|
+
raise "#{page_or_uri}'s routing requires parameters you must specicfy to `have_redirected_to`: #{ex.message}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
failure_message do |result|
|
|
18
|
+
if page_or_uri.kind_of?(URI)
|
|
19
|
+
"Expected #{page_or_uri} but got #{result}"
|
|
20
|
+
elsif page_or_uri.ancestors.include?(Brut::FrontEnd::Page)
|
|
21
|
+
"Expected #{page_or_uri}'s routing (#{page_or_uri.routing(**page_params)}), but got #{result}"
|
|
22
|
+
else
|
|
23
|
+
"Unknown error occured or bug with have_redirected_to"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
failure_message_when_negated do |result|
|
|
27
|
+
"Got a redirect when it wasn't expected"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
RSpec::Matchers.define :have_rendered do
|
|
1
|
+
RSpec::Matchers.define :have_rendered do |component_or_page|
|
|
2
2
|
match do |result|
|
|
3
|
-
result.
|
|
3
|
+
result.class.ancestors.include?(component_or_page)
|
|
4
4
|
end
|
|
5
5
|
|
|
6
6
|
failure_message do |result|
|
|
7
|
-
|
|
8
|
-
in URI => uri
|
|
9
|
-
"Got a redirect to #{uri} instead of rendering"
|
|
10
|
-
in Brut::FrontEnd::HttpStatus => http_status
|
|
11
|
-
"Got an HTTP status of #{http_status} instead of rendering"
|
|
12
|
-
else
|
|
13
|
-
"Got an unexpected result: #{result.class} instead of a String"
|
|
14
|
-
end
|
|
7
|
+
"Expected a #{component_or_page} to be rendered, but got #{result}"
|
|
15
8
|
end
|
|
16
9
|
failure_message_when_negated do |result|
|
|
17
|
-
"
|
|
10
|
+
"Got #{component_or_page} when not expected"
|
|
18
11
|
end
|
|
19
12
|
|
|
20
13
|
end
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
RSpec::Matchers.define :have_returned_http_status do |http_status|
|
|
1
|
+
RSpec::Matchers.define :have_returned_http_status do |http_status=nil|
|
|
2
2
|
match do |result|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
http_status == 302
|
|
6
|
-
in Brut::FrontEnd::HttpStatus => result_http_status
|
|
7
|
-
http_status == result_http_status.to_i
|
|
3
|
+
if http_status.nil?
|
|
4
|
+
result.kind_of?(Brut::FrontEnd::HttpStatus)
|
|
8
5
|
else
|
|
9
|
-
|
|
6
|
+
case result
|
|
7
|
+
in URI => uri
|
|
8
|
+
http_status == 302
|
|
9
|
+
in Brut::FrontEnd::HttpStatus => result_http_status
|
|
10
|
+
http_status == result_http_status.to_i
|
|
11
|
+
else
|
|
12
|
+
http_status == 200
|
|
13
|
+
end
|
|
10
14
|
end
|
|
11
15
|
end
|
|
12
16
|
|
|
@@ -21,7 +25,11 @@ RSpec::Matchers.define :have_returned_http_status do |http_status|
|
|
|
21
25
|
end
|
|
22
26
|
end
|
|
23
27
|
failure_message_when_negated do |result|
|
|
24
|
-
|
|
28
|
+
if http_status.nil?
|
|
29
|
+
"#{result} was rendered, but was not expecting an HTTP status"
|
|
30
|
+
else
|
|
31
|
+
"Got #{http_status} when not expected (#{result.class} was returned)"
|
|
32
|
+
end
|
|
25
33
|
end
|
|
26
34
|
|
|
27
35
|
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Configures RSpec for Brut. This provides several bits of infrastructure only present when the app is running, as well as
|
|
2
|
+
# quality of life improvements to make testing Brut components a bit easier. Even though Brut classes are all normal classes, it's
|
|
3
|
+
# convenient to have extraneous classes set up for you.
|
|
4
|
+
#
|
|
5
|
+
# * Metadata is added based on the class names under test:
|
|
6
|
+
# - `*Component` -> `:component`
|
|
7
|
+
# - `*Page` -> `:component`
|
|
8
|
+
# - `*Page` -> `:page`
|
|
9
|
+
# - `*Handler` -> `:handler`
|
|
10
|
+
# - If you add the `:page` metadata automatically, `:component` is added as well.
|
|
11
|
+
# - Tests in `specs/e2e` or any subfolder -> `:e2e`
|
|
12
|
+
# - Note that you are free to explicitly add these tags to your test metadata. That will cause the inclusion of modules as described below
|
|
13
|
+
# * Modules are included to provide additional support for writing tests:
|
|
14
|
+
# - {Brut::SpecSupport::GeneralSupport} included in all tests
|
|
15
|
+
# - {Brut::SpecSupport::ComponentSupport} included when `:component` metadata is set (generally, this is for page and component tests)
|
|
16
|
+
# - {Brut::SpecSupport::HandlerSupport} included when `:handler` is set.
|
|
17
|
+
# - {Playwright::Test::Matchers{ included when `:e2e` is set, which allows use of the Ruby binding for Playwright.
|
|
18
|
+
# * Non end-to-end tests will be run inside a database transaction to allow all database changes to be instantly undone. This does
|
|
19
|
+
# mean that any tests that tests database transaction behavior will not work as expected.
|
|
20
|
+
# * In component tests (which generally includes page tests), a {Brut::FrontEnd::RequestContext} is created for you and placed into
|
|
21
|
+
# the thread local storage. This allows any component that is injected with data from the `RequestContext` to access it as it would
|
|
22
|
+
# normally. You can also seed this with data that a component may need.
|
|
23
|
+
# * Handles all infrastructure for end-to-end tests:
|
|
24
|
+
# - starting a test server using {Brut::SpecSupport::E2ETestServer}
|
|
25
|
+
# - launching Chromium via Playwright
|
|
26
|
+
# * If using Sidekiq:
|
|
27
|
+
# - Jobs are cleared before each test
|
|
28
|
+
# - For end-to-end tests, Redis is flushed and actual Sidekiq is used instead of testing mode
|
|
29
|
+
#
|
|
30
|
+
# You can set certain metadata to change behavior:
|
|
31
|
+
#
|
|
32
|
+
# * `e2e_timeout` - number of milliseconds to wait until an end-to-end test gives up on a selector being found. The default is 5,000 (5 seconds). Use this only if there's no other way to keep your test from needing more than 5 seconds.
|
|
33
|
+
#
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# RSpec.configure do |config|
|
|
37
|
+
# rspec_setup = Brut::SpecSupport::RSpecSetup.new(rspec_config: config)
|
|
38
|
+
# rspec_setup.setup!
|
|
39
|
+
#
|
|
40
|
+
# # rest of the RSpec configuration
|
|
41
|
+
# end
|
|
42
|
+
class Brut::SpecSupport::RSpecSetup
|
|
43
|
+
# Create the setup with the given RSpec configuration.
|
|
44
|
+
#
|
|
45
|
+
# @param [RSpec::Core::Configuration] rspec_config yielded from `RSpec.configure`
|
|
46
|
+
def initialize(rspec_config:)
|
|
47
|
+
@config = rspec_config
|
|
48
|
+
SemanticLogger.default_level = ENV.fetch("LOGGER_LEVEL_FOR_TESTS","warn")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Sets up RSpec with variouis configurations needed by Brut to run your tests.
|
|
52
|
+
#
|
|
53
|
+
# @param [Proc] inside_db_transaction if given, this is run inside the DB transaction before your example is run. This is useful if you need to set up some reference data for all tests.
|
|
54
|
+
def setup!(inside_db_transaction: ->() {})
|
|
55
|
+
|
|
56
|
+
Brut::FactoryBot.new.setup!
|
|
57
|
+
optional_sidekiq_support = OptionalSidekiqSupport.new
|
|
58
|
+
|
|
59
|
+
@config.define_derived_metadata do |metadata|
|
|
60
|
+
if metadata[:described_class].to_s =~ /[a-z0-9]Component$/ ||
|
|
61
|
+
metadata[:described_class].to_s =~ /[a-z0-9]Page$/ ||
|
|
62
|
+
metadata[:page] == true
|
|
63
|
+
metadata[:component] = true
|
|
64
|
+
end
|
|
65
|
+
if metadata[:described_class].to_s =~ /[a-z0-9]Page$/ ||
|
|
66
|
+
metadata[:page] == true
|
|
67
|
+
metadata[:page] = true
|
|
68
|
+
end
|
|
69
|
+
if metadata[:described_class].to_s =~ /[a-z0-9]Handler$/
|
|
70
|
+
metadata[:handler] = true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
relative_path = Pathname(metadata[:absolute_file_path]).relative_path_from(Brut.container.app_specs_dir)
|
|
74
|
+
|
|
75
|
+
top_level_directory = relative_path.each_filename.to_a[0].to_s
|
|
76
|
+
if top_level_directory == "e2e"
|
|
77
|
+
metadata[:e2e] = true
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
@config.include Brut::SpecSupport::GeneralSupport
|
|
81
|
+
@config.include Brut::SpecSupport::ComponentSupport, component: true
|
|
82
|
+
@config.include Brut::SpecSupport::HandlerSupport, handler: true
|
|
83
|
+
@config.include Playwright::Test::Matchers, e2e: true
|
|
84
|
+
|
|
85
|
+
@config.around do |example|
|
|
86
|
+
|
|
87
|
+
needs_request_context = example.metadata[:component] ||
|
|
88
|
+
example.metadata[:handler] ||
|
|
89
|
+
example.metadata[:page]
|
|
90
|
+
|
|
91
|
+
if needs_request_context
|
|
92
|
+
session = {
|
|
93
|
+
"session_id" => "test-session-id",
|
|
94
|
+
"csrf" => "test-csrf-token"
|
|
95
|
+
}
|
|
96
|
+
env = {
|
|
97
|
+
"rack.session" => session
|
|
98
|
+
}
|
|
99
|
+
app_session = Brut.container.session_class.new(rack_session: session)
|
|
100
|
+
request_context = Brut::FrontEnd::RequestContext.new(
|
|
101
|
+
env: env,
|
|
102
|
+
session: app_session,
|
|
103
|
+
flash: empty_flash,
|
|
104
|
+
body: nil,
|
|
105
|
+
xhr: false,
|
|
106
|
+
)
|
|
107
|
+
Thread.current.thread_variable_set(:request_context, request_context)
|
|
108
|
+
example.example_group.let(:request_context) { request_context }
|
|
109
|
+
end
|
|
110
|
+
if example.metadata[:component]
|
|
111
|
+
example.example_group.let(:component_name) { described_class.component_name }
|
|
112
|
+
end
|
|
113
|
+
if example.metadata[:page]
|
|
114
|
+
example.example_group.let(:page_name) { described_class.page_name }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if example.metadata[:e2e]
|
|
118
|
+
e2e_timeout = (ENV["E2E_TIMEOUT_MS"] || example.metadata[:e2e_timeout] || 5_000).to_i
|
|
119
|
+
optional_sidekiq_support.disable_sidekiq_testing do
|
|
120
|
+
Brut::SpecSupport::E2ETestServer.instance.start
|
|
121
|
+
Playwright.create(playwright_cli_executable_path: "./node_modules/.bin/playwright") do |playwright|
|
|
122
|
+
launch_args = {
|
|
123
|
+
headless: true,
|
|
124
|
+
}
|
|
125
|
+
if ENV["E2E_SLOW_MO"]
|
|
126
|
+
launch_args[:slowMo] = ENV["E2E_SLOW_MO"].to_i
|
|
127
|
+
end
|
|
128
|
+
playwright.chromium.launch(**launch_args) do |browser|
|
|
129
|
+
context_options = {
|
|
130
|
+
baseURL: "http://0.0.0.0:6503/",
|
|
131
|
+
}
|
|
132
|
+
if ENV["E2E_RECORD_VIDEOS"]
|
|
133
|
+
context_options[:record_video_dir] = Brut.container.tmp_dir / "e2e-videos"
|
|
134
|
+
end
|
|
135
|
+
browser_context = browser.new_context(**context_options)
|
|
136
|
+
browser_context.default_timeout = e2e_timeout
|
|
137
|
+
example.example_group.let(:page) { browser_context.new_page }
|
|
138
|
+
example.run
|
|
139
|
+
browser_context.close
|
|
140
|
+
browser.close
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
else
|
|
145
|
+
optional_sidekiq_support.clear_background_jobs
|
|
146
|
+
Sequel::Model.db.transaction do
|
|
147
|
+
inside_db_transaction.()
|
|
148
|
+
example.run
|
|
149
|
+
raise Sequel::Rollback
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
@config.after(:suite) do
|
|
154
|
+
Brut::SpecSupport::E2ETestServer.instance.stop
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
class OptionalSidekiqSupport
|
|
159
|
+
def initialize
|
|
160
|
+
@sidekiq_in_use = defined?(Sidekiq)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def disable_sidekiq_testing(&block)
|
|
164
|
+
if @sidekiq_in_use
|
|
165
|
+
Sidekiq::Testing.disable! do
|
|
166
|
+
Sidekiq.redis do |redis|
|
|
167
|
+
redis.flushall
|
|
168
|
+
end
|
|
169
|
+
block.()
|
|
170
|
+
end
|
|
171
|
+
else
|
|
172
|
+
block.()
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
def clear_background_jobs
|
|
176
|
+
if @sidekiq_in_use
|
|
177
|
+
Sidekiq::Worker.clear_all
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
end
|
data/lib/brut/spec_support.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Convention here is different. We don't want to autoload
|
|
2
|
-
# a lot of stuff, since RSpec pollutes the Object namespace.
|
|
3
|
-
# Instead, we'll require that these files are required explicitly
|
|
4
1
|
module Brut
|
|
2
|
+
# Spec Support holds various matchers and helpers useful when writing tests with RSpec.
|
|
3
|
+
# Note that this module and it's contents aren't loaded by default when you `require "brut"`.
|
|
4
|
+
# Your app's `spec_helper.rb` should require these properly.
|
|
5
5
|
module SpecSupport
|
|
6
6
|
end
|
|
7
7
|
end
|
|
@@ -9,4 +9,9 @@ require_relative "spec_support/matcher"
|
|
|
9
9
|
require_relative "spec_support/component_support"
|
|
10
10
|
require_relative "spec_support/handler_support"
|
|
11
11
|
require_relative "spec_support/general_support"
|
|
12
|
+
require_relative "spec_support/e2e_test_server"
|
|
13
|
+
require_relative "spec_support/rspec_setup"
|
|
12
14
|
require_relative "factory_bot"
|
|
15
|
+
# Convention here is different. We don't want to autoload
|
|
16
|
+
# a lot of stuff, since RSpec pollutes the Object namespace.
|
|
17
|
+
# Instead, we'll require that these files are required explicitly
|
data/lib/brut/version.rb
CHANGED
data/lib/brut.rb
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
require_relative "brut/framework"
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# Brut is a way to make web apps with Ruby. It focuses on web standards, object-orientation, and other fundamentals. Brut seeks to
|
|
4
|
+
# minimize abstractions where possible.
|
|
4
5
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
6
|
+
# Brut encourages the use of the browser's technology and encourages you to build a web app based on good practices that are set up by
|
|
7
|
+
# default. Brut may not look easy, but it aims to be simple. It attempts to minimize dependencies and complexity, while leveraging
|
|
8
|
+
# common tested Ruby libraries related to web development.
|
|
9
|
+
#
|
|
10
|
+
# Have fun!
|
|
7
11
|
module Brut
|
|
12
|
+
# In Brut, the _front end_ is considered anything that interacts directly with a web browser or HTTP. This includes rendering HTML,
|
|
13
|
+
# managing JavaScript and CSS, and processing form submissions. It contrasts to {Brut::BackEnd}, which handles the business logic
|
|
14
|
+
# and database.
|
|
15
|
+
#
|
|
16
|
+
# You {Brut::App} defines pages, forms, and actions. A page is backed by a subclass of {Brut::FrontEnd::Page}, which provides
|
|
17
|
+
# dynamic data for rendering. A page can reference {Brut::FrontEnd::Component} subclasses to allow functional decomposition of front
|
|
18
|
+
# end logic and markup, as well as re-use. Both pages and components have ERB files that describe the HTML to be rendered.
|
|
19
|
+
#
|
|
20
|
+
# A {Brut::FrontEnd::Form} subclass defines a form that a browser will submit to your app. That
|
|
21
|
+
# submission is processed by a {Brut::FrontEnd::Handler} subclass. Handlers can also respond to other HTTP requests.
|
|
22
|
+
#
|
|
23
|
+
# In addition to responding to requests, you can subclass {Brut::FrontEnd::RouteHook} or {Brut::FrontEnd::Middleware} to perform
|
|
24
|
+
# further manipulation of the request.
|
|
25
|
+
#
|
|
26
|
+
# The entire front-end is based on Rack, so you should be able to achieve anything you need to.
|
|
8
27
|
module FrontEnd
|
|
9
28
|
autoload(:AssetMetadata, "brut/front_end/asset_metadata")
|
|
10
29
|
autoload(:Component, "brut/front_end/component")
|
|
@@ -20,19 +39,23 @@ module Brut
|
|
|
20
39
|
autoload(:Middleware, "brut/front_end/middleware")
|
|
21
40
|
autoload(:Middlewares, "brut/front_end/middleware")
|
|
22
41
|
autoload(:Page, "brut/front_end/page")
|
|
42
|
+
autoload(:Pages, "brut/front_end/page")
|
|
23
43
|
autoload(:RequestContext, "brut/front_end/request_context")
|
|
24
44
|
autoload(:RouteHook, "brut/front_end/route_hook")
|
|
25
45
|
autoload(:RouteHooks, "brut/front_end/route_hook")
|
|
26
46
|
autoload(:Routing, "brut/front_end/routing")
|
|
27
47
|
autoload(:Session, "brut/front_end/session")
|
|
28
48
|
end
|
|
49
|
+
# The _back end_ of a Brut app is where your app's business logic and database are managed. While the bulk of your Brut app's code
|
|
50
|
+
# will be in the back end, Brut is far less prescriptive about how to manage that than it is the front end.
|
|
29
51
|
module BackEnd
|
|
30
|
-
autoload(:Result, "brut/back_end/result")
|
|
31
52
|
autoload(:Validators, "brut/back_end/validator")
|
|
53
|
+
# Do not put SeedData here - it must be loaded only when needed
|
|
32
54
|
end
|
|
33
|
-
#
|
|
55
|
+
# I18n is where internationalization and localization support lives.
|
|
34
56
|
autoload(:I18n, "brut/i18n")
|
|
35
57
|
autoload(:Instrumentation,"brut/instrumentation")
|
|
36
58
|
autoload(:SinatraHelpers, "brut/sinatra_helpers")
|
|
59
|
+
# DO NOT autoload(:CLI) - that is intended to be require-able on its own
|
|
37
60
|
end
|
|
38
61
|
require "sequel/plugins"
|
|
@@ -1,37 +1,15 @@
|
|
|
1
1
|
module Sequel
|
|
2
2
|
module Extensions
|
|
3
|
+
# Instruments all SQL executions.
|
|
3
4
|
module BrutInstrumentation
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
end
|
|
8
|
-
end
|
|
9
|
-
def execute(sql, opts = Sequel::OPTS, &block)
|
|
10
|
-
Brut.container.instrumentation.instrument(Event.new(operation: "execute", sql: sql)) do
|
|
11
|
-
super
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
def execute_dui(sql, opts = Sequel::OPTS, &block)
|
|
15
|
-
Brut.container.instrumentation.instrument(Event.new(operation: "execute_dui", sql: sql)) do
|
|
16
|
-
super
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
def execute_insert(sql, opts = Sequel::OPTS, &block)
|
|
20
|
-
Brut.container.instrumentation.instrument(Event.new(operation: "execute_insert", sql: sql)) do
|
|
21
|
-
super
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
def insert_select(*values)
|
|
25
|
-
Brut.container.instrumentation.instrument(Event.new(operation: "insert_select", sql: values)) do
|
|
26
|
-
super
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
def returning_fetch_rows(sql,&block)
|
|
30
|
-
Brut.container.instrumentation.instrument(Event.new(operation: "returning_fetch_rows", sql: sql)) do
|
|
5
|
+
# @!visibility private
|
|
6
|
+
def log_connection_yield(sql,conn,args=nil)
|
|
7
|
+
Brut.container.instrumentation.span("SQL", sql: sql) do |span|
|
|
31
8
|
super
|
|
32
9
|
end
|
|
33
10
|
end
|
|
34
11
|
end
|
|
35
12
|
end
|
|
36
13
|
Sequel::Dataset.register_extension(:brut_instrumentation, Sequel::Extensions::BrutInstrumentation)
|
|
14
|
+
Sequel::Database.register_extension(:brut_instrumentation, Sequel::Extensions::BrutInstrumentation)
|
|
37
15
|
end
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
module Sequel
|
|
2
2
|
module Extensions
|
|
3
|
-
#
|
|
3
|
+
# Modifies and enhances Sequel's migrations DSL to default to best practices.
|
|
4
4
|
#
|
|
5
|
-
# * If no primary key is specified, a primary key column named
|
|
6
|
-
# * If no created_at is specified, a column name created_at of type timestamptz is created
|
|
7
|
-
# * create_table requires a comment
|
|
8
|
-
# * create_table accepts an external_id: true
|
|
9
|
-
# *
|
|
10
|
-
# *
|
|
11
|
-
# *
|
|
5
|
+
# * If no primary key is specified, a primary key column named `id` of type `int` will be created.
|
|
6
|
+
# * If no `created_at` is specified, a column name `created_at` of type `timestamptz` is created.
|
|
7
|
+
# * `create_table` requires a `comment:` attribute that explains the purpose of the table.
|
|
8
|
+
# * `create_table` accepts an `external_id: true` attribute that will create a unique `citext` field named `external_id`. This is intended to be used with {Sequel::Plugins::ExternalId}.
|
|
9
|
+
# * Columns are non-null by default. To make a nullable column, use `null: true`.
|
|
10
|
+
# * Foreign keys are non-null by default and an index is created by default.
|
|
11
|
+
# * The `key` method allows specifying additional keys on the table. This effecitvely creates a unique constraint on the fields given to `key`.
|
|
12
12
|
module BrutMigrations
|
|
13
|
+
# Overrides Sequel's `create_table`
|
|
14
|
+
#
|
|
15
|
+
# @param args [Object] the arguments to pass to Sequel's `create_table`. If the last entry in `*args` is a `Hash`, new options are recognized:
|
|
16
|
+
# @option args [String] :comment String containing the table's description, included in the table definition. Required.
|
|
17
|
+
# @option args [true|false] :external_id If true, adds a `:citext` column named `external_id` that has a unique index on it.
|
|
13
18
|
def create_table(*args)
|
|
14
19
|
super
|
|
15
20
|
|
|
@@ -27,10 +32,15 @@ module Sequel
|
|
|
27
32
|
end
|
|
28
33
|
end
|
|
29
34
|
|
|
35
|
+
# Specifies a non-primary key based on the fields given. Effectively creates a unique index on these fields.
|
|
36
|
+
# Inside a `create_table` block, this can be called via `key`
|
|
37
|
+
#
|
|
38
|
+
# @param fields [Array] fields that should form the key.
|
|
30
39
|
def add_key(fields)
|
|
31
40
|
add_index fields, unique: true
|
|
32
41
|
end
|
|
33
42
|
|
|
43
|
+
# Overrides Sequel's `add_column` to default `null: false`.
|
|
34
44
|
def add_column(table,*args)
|
|
35
45
|
options = args.last
|
|
36
46
|
if options.is_a?(Hash)
|