diff --git a/app/controllers/public/scenarios_controller.rb b/app/controllers/public/scenarios_controller.rb new file mode 100644 index 0000000..540074e --- /dev/null +++ b/app/controllers/public/scenarios_controller.rb @@ -0,0 +1,8 @@ +class Public::ScenariosController < ApplicationController + allow_unauthenticated_access only: :show + skip_before_action :require_organization_membership, only: :show + + def show + @scenario = Current.organization.scenarios.find_by!(share_token: params[:token]) + end +end diff --git a/app/controllers/scenarios/shares_controller.rb b/app/controllers/scenarios/shares_controller.rb new file mode 100644 index 0000000..a4b98eb --- /dev/null +++ b/app/controllers/scenarios/shares_controller.rb @@ -0,0 +1,26 @@ +class Scenarios::SharesController < ApplicationController + include ScenarioScoping + + before_action :set_scenario + + def create + @scenario.enable_sharing! + redirect_to scenario_path(@scenario) + end + + def update + @scenario.regenerate_share_token! + redirect_to scenario_path(@scenario) + end + + def destroy + @scenario.disable_sharing! + redirect_to scenario_path(@scenario) + end + + private + + def set_scenario + @scenario = accessible_scenarios.find(params[:scenario_id]) + end +end diff --git a/app/javascript/controllers/clipboard_controller.js b/app/javascript/controllers/clipboard_controller.js new file mode 100644 index 0000000..f858019 --- /dev/null +++ b/app/javascript/controllers/clipboard_controller.js @@ -0,0 +1,25 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["source", "label"] + static values = { confirmDuration: { type: Number, default: 1500 } } + + select() { + this.sourceTarget.select() + } + + async copy() { + try { + await navigator.clipboard.writeText(this.sourceTarget.value) + } catch { + this.sourceTarget.select() + document.execCommand("copy") + } + + if (this.hasLabelTarget) { + const original = this.labelTarget.textContent + this.labelTarget.textContent = "Copied!" + setTimeout(() => { this.labelTarget.textContent = original }, this.confirmDurationValue) + } + } +} diff --git a/app/models/scenario.rb b/app/models/scenario.rb index 9c51ce0..1c69ede 100644 --- a/app/models/scenario.rb +++ b/app/models/scenario.rb @@ -12,6 +12,22 @@ class Scenario < ApplicationRecord before_create :ensure_greatest_community_need + def shared? + share_token.present? + end + + def enable_sharing! + regenerate_share_token! unless shared? + end + + def regenerate_share_token! + update!(share_token: SecureRandom.base58(24)) + end + + def disable_sharing! + update!(share_token: nil) + end + def ongoing_percentage_total ongoing_allocations.sum { |allocation| allocation.percentage.to_i } end diff --git a/app/views/public/scenarios/show.html.erb b/app/views/public/scenarios/show.html.erb new file mode 100644 index 0000000..aa1237a --- /dev/null +++ b/app/views/public/scenarios/show.html.erb @@ -0,0 +1,19 @@ +
+
+ Read-only shared view of a giving scenario. +
+ +
+

<%= @scenario.name %>

+

Shared by <%= @scenario.user.display_name %>

+ <% if @scenario.total_giving_amount.to_i.positive? %> +

+ Total giving <%= number_to_currency(@scenario.total_giving_amount, precision: 0) %> +

+ <% end %> +
+ +
+ <%= render "scenarios/summary", scenario: @scenario %> +
+
diff --git a/app/views/scenarios/_share.html.erb b/app/views/scenarios/_share.html.erb new file mode 100644 index 0000000..e6cd59a --- /dev/null +++ b/app/views/scenarios/_share.html.erb @@ -0,0 +1,30 @@ +<%# locals: (scenario:) %> +
+ + + +
+
+
+

Share a read-only link

+

Anyone with the link can view this scenario without signing in.

+
+ +
+ + <%= render "scenarios/share_panel", scenario: scenario %> +
+
+
diff --git a/app/views/scenarios/_share_panel.html.erb b/app/views/scenarios/_share_panel.html.erb new file mode 100644 index 0000000..b14c3b4 --- /dev/null +++ b/app/views/scenarios/_share_panel.html.erb @@ -0,0 +1,31 @@ +<%# locals: (scenario:) %> +<%= turbo_frame_tag dom_id(scenario, :share) do %> + <% if scenario.shared? %> +
+
+ + +
+
+ <%= button_to "Regenerate link", scenario_share_path(scenario), method: :patch, + class: "text-ink-soft hover:text-ink cursor-pointer bg-transparent p-0", + data: { turbo_confirm: "Regenerate the link? The current link will stop working." } %> + <%= button_to "Stop sharing", scenario_share_path(scenario), method: :delete, + class: "text-ink-soft hover:text-danger cursor-pointer bg-transparent p-0", + data: { turbo_confirm: "Stop sharing? The link will stop working." } %> +
+
+ <% else %> +
+ <%= button_to "Create share link", scenario_share_path(scenario), method: :post, + class: "w-full rounded-md px-4 py-2.5 bg-accent hover:bg-[#444] text-white text-sm font-medium cursor-pointer transition" %> +
+ <% end %> +<% end %> diff --git a/app/views/scenarios/_summary.html.erb b/app/views/scenarios/_summary.html.erb new file mode 100644 index 0000000..897a405 --- /dev/null +++ b/app/views/scenarios/_summary.html.erb @@ -0,0 +1,93 @@ +<%# locals: (scenario:) %> +<% ongoing_total = scenario.ongoing_percentage_total %> +<% ongoing_amount = scenario.ongoing_giving_amount %> +<% one_time_amount = scenario.one_time_giving_amount %> +<% total_amount = scenario.total_giving_amount.to_i %> +
+

Allocation summary

+ <% if total_amount.positive? %> +

+ Ongoing <%= number_to_currency(ongoing_amount, precision: 0) %> + + One-time <%= number_to_currency(one_time_amount, precision: 0) %> + = Total <%= number_to_currency(total_amount, precision: 0) %> +

+ <% else %> +

Set your total above to see a full breakdown.

+ <% end %> + +
+

Ongoing giving

+

<%= number_to_currency(ongoing_amount, precision: 0) %>

+ <% if scenario.ongoing_allocations.any? %> +

+ <%= ongoing_total %>% allocated across <%= pluralize(scenario.ongoing_allocations.count, "cause") %> +

+ + <% remaining = [100 - ongoing_total, 0].max %> +
+ <% scenario.ongoing_allocations.each_with_index do |allocation, index| %> +
+ <% end %> + <% if remaining.positive? %> +
+ <% end %> +
+ + + <% else %> +

No ongoing allocations yet.

+ <% end %> +
+ + <% if scenario.one_time_allocations.any? %> +
+
+

+ One time giving + ADD-ON +

+ <%= number_to_currency(one_time_amount, precision: 0) %> +
+ +
+ <% end %> + + <% if total_amount.positive? %> +
+ Ongoing <%= number_to_currency(ongoing_amount, precision: 0) %> + + One-time <%= number_to_currency(one_time_amount, precision: 0) %> + = Total <%= number_to_currency(total_amount, precision: 0) %> +
+ <% end %> +
diff --git a/app/views/scenarios/show.html.erb b/app/views/scenarios/show.html.erb index e244233..f7e900b 100644 --- a/app/views/scenarios/show.html.erb +++ b/app/views/scenarios/show.html.erb @@ -13,7 +13,10 @@ <% end %> - <%= render "scenarios/names/name", scenario: @scenario %> +
+ <%= render "scenarios/names/name", scenario: @scenario %> + <%= render "scenarios/share", scenario: @scenario %> +
<%= render "scenarios/total_giving_amounts/total_giving_amount", scenario: @scenario %> @@ -37,97 +40,6 @@ - <% ongoing_total = @scenario.ongoing_percentage_total %> - <% ongoing_amount = @scenario.ongoing_giving_amount %> - <% one_time_amount = @scenario.one_time_giving_amount %> - <% total_amount = @scenario.total_giving_amount.to_i %> -
-

Allocation summary

- <% if total_amount.positive? %> -

- Ongoing <%= number_to_currency(ongoing_amount, precision: 0) %> - + One-time <%= number_to_currency(one_time_amount, precision: 0) %> - = Total <%= number_to_currency(total_amount, precision: 0) %> -

- <% else %> -

Set your total above to see a full breakdown.

- <% end %> - -
-

Ongoing giving

-

<%= number_to_currency(ongoing_amount, precision: 0) %>

- <% if @scenario.ongoing_allocations.any? %> -

- <%= ongoing_total %>% allocated across <%= pluralize(@scenario.ongoing_allocations.count, "cause") %> -

- - <% remaining = [100 - ongoing_total, 0].max %> -
- <% @scenario.ongoing_allocations.each_with_index do |allocation, index| %> -
- <% end %> - <% if remaining.positive? %> -
- <% end %> -
- - - <% else %> -

No ongoing allocations yet.

- <% end %> -
- - <% if @scenario.one_time_allocations.any? %> -
-
-

- One time giving - ADD-ON -

- <%= number_to_currency(one_time_amount, precision: 0) %> -
- -
- <% end %> - - <% if total_amount.positive? %> -
- Ongoing <%= number_to_currency(ongoing_amount, precision: 0) %> - + One-time <%= number_to_currency(one_time_amount, precision: 0) %> - = Total <%= number_to_currency(total_amount, precision: 0) %> -
- <% end %> -
+ <%= render "summary", scenario: @scenario %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index 77a409f..d2b9189 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -17,9 +17,14 @@ # Dev-only convenience: sign in as the first user without a password get "auto_sign_in", to: "auto_sign_in#create" if Rails.env.development? + namespace :public do + resources :scenarios, only: :show, param: :token + end + resources :scenarios do resource :name, only: %i[ show edit update ], module: :scenarios resource :total_giving_amount, only: %i[ show edit update ], module: :scenarios + resource :share, only: %i[ create update destroy ], module: :scenarios resources :allocations, only: %i[ create update destroy ] end diff --git a/db/migrate/20260701000000_add_share_token_to_scenarios.rb b/db/migrate/20260701000000_add_share_token_to_scenarios.rb new file mode 100644 index 0000000..e8aee40 --- /dev/null +++ b/db/migrate/20260701000000_add_share_token_to_scenarios.rb @@ -0,0 +1,6 @@ +class AddShareTokenToScenarios < ActiveRecord::Migration[8.1] + def change + add_column :scenarios, :share_token, :string + add_index :scenarios, :share_token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 867a6d6..e6f925a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_06_27_000000) do +ActiveRecord::Schema[8.1].define(version: 2026_07_01_000000) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -102,7 +102,9 @@ t.integer "total_giving_amount" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "share_token" t.index ["organization_id"], name: "index_scenarios_on_organization_id" + t.index ["share_token"], name: "index_scenarios_on_share_token", unique: true t.index ["user_id"], name: "index_scenarios_on_user_id" end diff --git a/test/controllers/admin/allocation_categories_controller_test.rb b/test/controllers/admin/allocation_categories_controller_test.rb index c6f124e..38eebec 100644 --- a/test/controllers/admin/allocation_categories_controller_test.rb +++ b/test/controllers/admin/allocation_categories_controller_test.rb @@ -2,7 +2,6 @@ class Admin::AllocationCategoriesControllerTest < ActionDispatch::IntegrationTest setup do - host! "arlington.localhost" @owner = users(:one) @admin = users(:admin) @member = users(:passwordless) diff --git a/test/controllers/admin/organization_memberships_controller_test.rb b/test/controllers/admin/organization_memberships_controller_test.rb index 275bb00..5a78386 100644 --- a/test/controllers/admin/organization_memberships_controller_test.rb +++ b/test/controllers/admin/organization_memberships_controller_test.rb @@ -2,7 +2,6 @@ class Admin::OrganizationMembershipsControllerTest < ActionDispatch::IntegrationTest setup do - host! "arlington.localhost" @owner = users(:one) @admin = users(:admin) @member = users(:passwordless) diff --git a/test/controllers/admin/organizations_controller_test.rb b/test/controllers/admin/organizations_controller_test.rb index f38dd17..cdff02d 100644 --- a/test/controllers/admin/organizations_controller_test.rb +++ b/test/controllers/admin/organizations_controller_test.rb @@ -2,7 +2,6 @@ class Admin::OrganizationsControllerTest < ActionDispatch::IntegrationTest setup do - host! "arlington.localhost" @organization = organizations(:arlington) @owner = users(:one) @admin = users(:admin) diff --git a/test/controllers/admin/scenarios_controller_test.rb b/test/controllers/admin/scenarios_controller_test.rb index a19b5fb..67b0155 100644 --- a/test/controllers/admin/scenarios_controller_test.rb +++ b/test/controllers/admin/scenarios_controller_test.rb @@ -2,7 +2,6 @@ class Admin::ScenariosControllerTest < ActionDispatch::IntegrationTest setup do - host! "arlington.localhost" @owner = users(:one) @admin = users(:admin) @member = users(:passwordless) diff --git a/test/controllers/allocations_controller_test.rb b/test/controllers/allocations_controller_test.rb index 111b352..4b216d6 100644 --- a/test/controllers/allocations_controller_test.rb +++ b/test/controllers/allocations_controller_test.rb @@ -2,7 +2,6 @@ class AllocationsControllerTest < ActionDispatch::IntegrationTest setup do - host! "arlington.localhost" sign_in_as users(:one) @scenario = scenarios(:one_arlington) end diff --git a/test/controllers/email_confirmations_controller_test.rb b/test/controllers/email_confirmations_controller_test.rb index 64c587f..b29859f 100644 --- a/test/controllers/email_confirmations_controller_test.rb +++ b/test/controllers/email_confirmations_controller_test.rb @@ -2,7 +2,6 @@ class EmailConfirmationsControllerTest < ActionDispatch::IntegrationTest setup do - host! "arlington.localhost" @user = users(:unconfirmed) end diff --git a/test/controllers/home_controller_test.rb b/test/controllers/home_controller_test.rb index ada8624..efb9bd5 100644 --- a/test/controllers/home_controller_test.rb +++ b/test/controllers/home_controller_test.rb @@ -2,7 +2,6 @@ class HomeControllerTest < ActionDispatch::IntegrationTest test "show on a tenant subdomain offers sign up and log in when signed out" do - host! "arlington.localhost" get root_url assert_response :success @@ -12,7 +11,6 @@ class HomeControllerTest < ActionDispatch::IntegrationTest end test "show offers explore options and sign out when signed in" do - host! "arlington.localhost" sign_in_as(users(:one)) get root_url @@ -23,7 +21,6 @@ class HomeControllerTest < ActionDispatch::IntegrationTest end test "a signed-in non-member is redirected to the apex" do - host! "arlington.localhost" sign_in_as(users(:two)) # member of boston, not arlington get root_url diff --git a/test/controllers/magic_links_controller_test.rb b/test/controllers/magic_links_controller_test.rb index 34dcb7e..5cab762 100644 --- a/test/controllers/magic_links_controller_test.rb +++ b/test/controllers/magic_links_controller_test.rb @@ -2,7 +2,6 @@ class MagicLinksControllerTest < ActionDispatch::IntegrationTest setup do - host! "arlington.localhost" @member = users(:one) # member of arlington end diff --git a/test/controllers/passwords_controller_test.rb b/test/controllers/passwords_controller_test.rb index 4db89d6..ef43b1e 100644 --- a/test/controllers/passwords_controller_test.rb +++ b/test/controllers/passwords_controller_test.rb @@ -2,7 +2,6 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest setup do - host! "arlington.localhost" @user = users(:one) end diff --git a/test/controllers/public/scenarios_controller_test.rb b/test/controllers/public/scenarios_controller_test.rb new file mode 100644 index 0000000..7db2116 --- /dev/null +++ b/test/controllers/public/scenarios_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class Public::ScenariosControllerTest < ActionDispatch::IntegrationTest + test "renders a shared scenario without authentication" do + scenario = scenarios(:one_arlington) + scenario.enable_sharing! + + get public_scenario_url(scenario.share_token) + + assert_response :success + assert_match scenario.name, response.body + assert_match scenario.user.display_name, response.body + assert_match "Allocation summary", response.body + end + + test "does not render edit controls" do + scenario = scenarios(:one_arlington) + scenario.enable_sharing! + + get public_scenario_url(scenario.share_token) + + assert_no_match "Add allocation", response.body + assert_select "input[type=range]", false + end + + test "returns not found for a scenario that is not shared" do + get public_scenario_url("nope") + assert_response :not_found + end + + test "returns not found after sharing is disabled" do + scenario = scenarios(:one_arlington) + scenario.enable_sharing! + token = scenario.share_token + scenario.disable_sharing! + + get public_scenario_url(token) + assert_response :not_found + end + + test "cannot reach a shared scenario from another organization" do + boston_scenario = scenarios(:two_boston) + boston_scenario.enable_sharing! + + get public_scenario_url(boston_scenario.share_token) + assert_response :not_found + end +end diff --git a/test/controllers/registrations_controller_test.rb b/test/controllers/registrations_controller_test.rb index 9efe8ea..0688720 100644 --- a/test/controllers/registrations_controller_test.rb +++ b/test/controllers/registrations_controller_test.rb @@ -1,8 +1,6 @@ require "test_helper" class RegistrationsControllerTest < ActionDispatch::IntegrationTest - setup { host! "arlington.localhost" } - test "new" do get new_registration_path assert_response :success diff --git a/test/controllers/scenarios/names_controller_test.rb b/test/controllers/scenarios/names_controller_test.rb index 162fe53..a227e2e 100644 --- a/test/controllers/scenarios/names_controller_test.rb +++ b/test/controllers/scenarios/names_controller_test.rb @@ -2,7 +2,6 @@ class Scenarios::NamesControllerTest < ActionDispatch::IntegrationTest setup do - host! "arlington.localhost" sign_in_as users(:one) @scenario = scenarios(:one_arlington) end diff --git a/test/controllers/scenarios/shares_controller_test.rb b/test/controllers/scenarios/shares_controller_test.rb new file mode 100644 index 0000000..5582747 --- /dev/null +++ b/test/controllers/scenarios/shares_controller_test.rb @@ -0,0 +1,50 @@ +require "test_helper" + +class Scenarios::SharesControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as users(:one) + end + + test "create enables sharing" do + scenario = scenarios(:one_arlington) + post scenario_share_url(scenario) + + assert_redirected_to scenario_path(scenario) + assert scenario.reload.shared? + end + + test "update regenerates the token" do + scenario = scenarios(:one_arlington) + scenario.enable_sharing! + token = scenario.share_token + + patch scenario_share_url(scenario) + + assert_redirected_to scenario_path(scenario) + assert scenario.reload.shared? + assert_not_equal token, scenario.share_token + end + + test "destroy disables sharing" do + scenario = scenarios(:one_arlington) + scenario.enable_sharing! + + delete scenario_share_url(scenario) + + assert_redirected_to scenario_path(scenario) + assert_not scenario.reload.shared? + end + + test "requires authentication" do + sign_out + post scenario_share_url(scenarios(:one_arlington)) + assert_redirected_to new_session_path + end + + test "a member cannot toggle another user's scenario" do + sign_in_as users(:passwordless) + post scenario_share_url(scenarios(:one_arlington)) + assert_response :not_found + assert_not scenarios(:one_arlington).reload.shared? + end +end diff --git a/test/controllers/scenarios/total_giving_amounts_controller_test.rb b/test/controllers/scenarios/total_giving_amounts_controller_test.rb index 0281475..7a57b1d 100644 --- a/test/controllers/scenarios/total_giving_amounts_controller_test.rb +++ b/test/controllers/scenarios/total_giving_amounts_controller_test.rb @@ -2,7 +2,6 @@ class Scenarios::TotalGivingAmountsControllerTest < ActionDispatch::IntegrationTest setup do - host! "arlington.localhost" sign_in_as users(:one) @scenario = scenarios(:one_arlington) end diff --git a/test/controllers/scenarios_controller_test.rb b/test/controllers/scenarios_controller_test.rb index 8b2131b..c5084be 100644 --- a/test/controllers/scenarios_controller_test.rb +++ b/test/controllers/scenarios_controller_test.rb @@ -2,7 +2,6 @@ class ScenariosControllerTest < ActionDispatch::IntegrationTest setup do - host! "arlington.localhost" sign_in_as users(:one) end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb index ef05842..cfece16 100644 --- a/test/controllers/sessions_controller_test.rb +++ b/test/controllers/sessions_controller_test.rb @@ -2,7 +2,6 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest setup do - host! "arlington.localhost" @user = users(:one) # member of arlington end diff --git a/test/controllers/users/emails_controller_test.rb b/test/controllers/users/emails_controller_test.rb index e3b4132..c14c533 100644 --- a/test/controllers/users/emails_controller_test.rb +++ b/test/controllers/users/emails_controller_test.rb @@ -1,8 +1,6 @@ require "test_helper" class Users::EmailsControllerTest < ActionDispatch::IntegrationTest - setup { host! "arlington.localhost" } - test "show requires authentication" do get users_email_path assert_redirected_to new_session_path diff --git a/test/controllers/users/passwords_controller_test.rb b/test/controllers/users/passwords_controller_test.rb index d292b20..e685591 100644 --- a/test/controllers/users/passwords_controller_test.rb +++ b/test/controllers/users/passwords_controller_test.rb @@ -1,8 +1,6 @@ require "test_helper" class Users::PasswordsControllerTest < ActionDispatch::IntegrationTest - setup { host! "arlington.localhost" } - test "show requires authentication" do get users_password_path assert_redirected_to new_session_path diff --git a/test/controllers/users/profiles_controller_test.rb b/test/controllers/users/profiles_controller_test.rb index c9f4c91..2476ffd 100644 --- a/test/controllers/users/profiles_controller_test.rb +++ b/test/controllers/users/profiles_controller_test.rb @@ -1,8 +1,6 @@ require "test_helper" class Users::ProfilesControllerTest < ActionDispatch::IntegrationTest - setup { host! "arlington.localhost" } - test "show requires authentication" do get users_profile_path assert_redirected_to new_session_path diff --git a/test/models/scenario_test.rb b/test/models/scenario_test.rb index 5fc66ef..0a59e17 100644 --- a/test/models/scenario_test.rb +++ b/test/models/scenario_test.rb @@ -37,6 +37,43 @@ class ScenarioTest < ActiveSupport::TestCase end end + test "enable_sharing! generates a token and marks the scenario shared" do + scenario = scenarios(:one_arlington) + assert_not scenario.shared? + + scenario.enable_sharing! + assert scenario.shared? + assert scenario.share_token.present? + end + + test "enable_sharing! is idempotent and keeps the existing token" do + scenario = scenarios(:one_arlington) + scenario.enable_sharing! + token = scenario.share_token + + scenario.enable_sharing! + assert_equal token, scenario.share_token + end + + test "regenerate_share_token! replaces the token" do + scenario = scenarios(:one_arlington) + scenario.enable_sharing! + token = scenario.share_token + + scenario.regenerate_share_token! + assert scenario.shared? + assert_not_equal token, scenario.share_token + end + + test "disable_sharing! clears the token" do + scenario = scenarios(:one_arlington) + scenario.enable_sharing! + + scenario.disable_sharing! + assert_not scenario.shared? + assert_nil scenario.share_token + end + test "new scenarios are created with a Greatest Community Need allocation at 0%" do scenario = users(:one).scenarios.create!( organization: organizations(:arlington), name: "Fresh plan", total_giving_amount: 1000) diff --git a/test/test_helper.rb b/test/test_helper.rb index a2448cc..4c9c130 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -18,6 +18,13 @@ require "rails/test_help" require_relative "test_helpers/session_test_helper" +# Every request in this app belongs to a tenant resolved from the subdomain, so +# default all integration tests to a tenant host. Individual tests still override +# with host! for apex / cross-tenant / unknown-org cases. +ActiveSupport.on_load(:action_dispatch_integration_test) do + setup { host! "arlington.localhost" } +end + module ActiveSupport class TestCase # Run tests in parallel with specified workers. Under coverage, run serially