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:) %>
+
+
+
+
+
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 %>
+
+
+
+ <% scenario.ongoing_allocations.each_with_index do |allocation, index| %>
+ -
+
+
+
+ <%= allocation.display_label %>
+
+
+ <%= allocation.percentage %>%
+ <%= number_to_currency(allocation.dollar_amount, precision: 0) %>
+
+
+
+ ∞ Est. <%= number_to_currency(allocation.perpetuity_annual_amount, precision: 0) %> /yr in perpetuity
+
+
+ <% 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) %>
+
+
+ <% scenario.one_time_allocations.each do |allocation| %>
+ -
+
+
+ <%= allocation.display_label %>
+
+
+ <%= allocation.share_percentage %>%
+ <%= number_to_currency(allocation.amount, precision: 0) %>
+
+
+ <% end %>
+
+
+ <% 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 %>
-
-
-
- <% @scenario.ongoing_allocations.each_with_index do |allocation, index| %>
- -
-
-
-
- <%= allocation.display_label %>
-
-
- <%= allocation.percentage %>%
- <%= number_to_currency(allocation.dollar_amount, precision: 0) %>
-
-
-
- ∞ Est. <%= number_to_currency(allocation.perpetuity_annual_amount, precision: 0) %> /yr in perpetuity
-
-
- <% 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) %>
-
-
- <% @scenario.one_time_allocations.each do |allocation| %>
- -
-
-
- <%= allocation.display_label %>
-
-
- <%= allocation.share_percentage %>%
- <%= number_to_currency(allocation.amount, precision: 0) %>
-
-
- <% end %>
-
-
- <% 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