From b03088706dee7dfe18f77e71baa2593e4e5d3609 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 6 Jun 2026 17:02:48 -0400 Subject: [PATCH 1/2] Split dev-only user variations out of db/seeds.rb db/seeds.rb runs on every db:seed, including production, so it must hold only required base data. The invite/lock/confirmation-state user variations are dev test fixtures, not base data, so move them to db/seeds/dev/users.rb behind a new db:seed:users task (wired into db:seed:dev), matching how dummy.rb and payments.rb are already organized. Base users (Umberto, Amy, Aisha, Orphaned) stay in db/seeds.rb. Co-Authored-By: Claude Opus 4.8 --- db/seeds.rb | 175 +------------------------------------ db/seeds/dev/users.rb | 198 ++++++++++++++++++++++++++++++++++++++++++ lib/tasks/dev.rake | 7 +- 3 files changed, 206 insertions(+), 174 deletions(-) create mode 100644 db/seeds/dev/users.rb diff --git a/db/seeds.rb b/db/seeds.rb index 87612402c..a99b3f73f 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -82,179 +82,8 @@ def find_or_create_by_name!(klass, name, **attrs, &block) user.confirmed_at = Time.current end -# Invited but hasn't clicked the link yet -invited = User.find_or_create_by!(email: "invited.pending@example.com") do |user| - user.password = "password" - user.super_user = false - user.confirmed_at = Time.current -end -unless invited.confirmed_at.present? - invited.update_columns( - confirmed_at: nil, - welcome_instructions_token: Devise.friendly_token, - welcome_instructions_created_at: 3.days.ago, - welcome_instructions_sent_at: 3.days.ago - ) -end -unless invited.person.present? - person = Person.create!( - first_name: "Invited", - last_name: "Pending", - email: invited.email, - created_by: admin, - updated_by: admin, - profile_is_searchable: true - ) - invited.update!(person: person) -end - -# Clicked confirmation link but didn't set password -confirmed_no_pw = User.find_or_create_by!(email: "confirmed.nopassword@example.com") do |user| - user.password = "password" - user.super_user = false - user.confirmed_at = Time.current -end -unless confirmed_no_pw.welcome_instructions_token.present? - token = Devise.friendly_token - confirmed_no_pw.update_columns( - confirmed_at: 2.days.ago, - welcome_instructions_token: token, - welcome_instructions_created_at: 5.days.ago, - welcome_instructions_sent_at: 5.days.ago - ) -end -unless confirmed_no_pw.person.present? - person = Person.create!( - first_name: "Confirmed", - last_name: "NoPassword", - email: confirmed_no_pw.email, - created_by: admin, - updated_by: admin, - profile_is_searchable: true - ) - confirmed_no_pw.update!(person: person) -end - -# Locked account (too many failed attempts) -locked = User.find_or_create_by!(email: "locked.user@example.com") do |user| - user.password = "password" - user.super_user = false - user.confirmed_at = 1.month.ago -end -unless locked.locked_at.present? - locked.update_columns( - locked_at: 1.day.ago, - failed_attempts: Devise.maximum_attempts - ) -end -unless locked.person.present? - person = Person.create!( - first_name: "Locked", - last_name: "User", - email: locked.email, - created_by: admin, - updated_by: admin, - profile_is_searchable: true - ) - locked.update!(person: person) -end - -# Never invited (created but no confirmation sent) -never_invited = User.find_or_create_by!(email: "never.invited@example.com") do |user| - user.password = "password" - user.super_user = false - user.confirmed_at = Time.current -end -unless never_invited.welcome_instructions_sent_at.present? - never_invited.update_columns(confirmed_at: nil) -end -unless never_invited.person.present? - person = Person.create!( - first_name: "Never", - last_name: "Invited", - email: never_invited.email, - created_by: admin, - updated_by: admin, - profile_is_searchable: true - ) - never_invited.update!(person: person) -end - -# Invited a while ago, never clicked (stale invite) -stale_invited = User.find_or_create_by!(email: "stale.invite@example.com") do |user| - user.password = "password" - user.super_user = false - user.confirmed_at = Time.current -end -unless stale_invited.confirmed_at.present? - stale_invited.update_columns( - confirmed_at: nil, - welcome_instructions_token: Devise.friendly_token, - welcome_instructions_created_at: 45.days.ago, - welcome_instructions_sent_at: 45.days.ago - ) -end -unless stale_invited.person.present? - person = Person.create!( - first_name: "Stale", - last_name: "Invite", - email: stale_invited.email, - created_by: admin, - updated_by: admin, - profile_is_searchable: true - ) - stale_invited.update!(person: person) -end - -# Invited yesterday -recent_invited = User.find_or_create_by!(email: "recent.invite@example.com") do |user| - user.password = "password" - user.super_user = false - user.confirmed_at = Time.current -end -unless recent_invited.confirmed_at.present? - recent_invited.update_columns( - confirmed_at: nil, - welcome_instructions_token: Devise.friendly_token, - welcome_instructions_created_at: 1.day.ago, - welcome_instructions_sent_at: 1.day.ago - ) -end -unless recent_invited.person.present? - person = Person.create!( - first_name: "Recent", - last_name: "Invite", - email: recent_invited.email, - created_by: admin, - updated_by: admin, - profile_is_searchable: true - ) - recent_invited.update!(person: person) -end - -# Never invited, no person record either -User.find_or_create_by!(email: "orphan.uninvited@example.com") do |user| - user.password = "password" - user.super_user = false - user.confirmed_at = Time.current -end.tap do |u| - u.update_columns(confirmed_at: nil) unless u.welcome_instructions_sent_at.present? -end - -# Invited, no person record -invited_no_person = User.find_or_create_by!(email: "invited.noperson@example.com") do |user| - user.password = "password" - user.super_user = false - user.confirmed_at = Time.current -end -unless invited_no_person.confirmed_at.present? - invited_no_person.update_columns( - confirmed_at: nil, - welcome_instructions_token: Devise.friendly_token, - welcome_instructions_created_at: 10.days.ago, - welcome_instructions_sent_at: 10.days.ago - ) -end +# Dev-only user variations (invite/lock/confirmation edge cases) live in +# db/seeds/dev/users.rb and run via `rake db:seed:users` / `db:seed:dev`. # Only reset seed-user passwords, not every user in the database seed_emails = %w[umberto.user@example.com amy.user@example.com aisha.user@example.com orphaned_reports@awbw.org] diff --git a/db/seeds/dev/users.rb b/db/seeds/dev/users.rb new file mode 100644 index 000000000..b092785e4 --- /dev/null +++ b/db/seeds/dev/users.rb @@ -0,0 +1,198 @@ +# Dev-only user variations - run on their own via `rake db:seed:users`, or as +# part of `rake db:seed:dev`. These exercise login/invite/lock edge cases and +# must never run in production, so they live here rather than in `db/seeds.rb` +# (which seeds only the required base users: Umberto, Amy, Aisha, Orphaned). + +puts "Seeding dev user variations (invite/lock/confirmation states)…" + +# created_by/updated_by point at the base admin, seeded by `db:seed` first. +admin = User.find_by!(email: "umberto.user@example.com") + +# Invited but hasn't clicked the link yet +invited = User.find_or_create_by!(email: "invited.pending@example.com") do |user| + user.password = "password" + user.super_user = false + user.confirmed_at = Time.current +end +unless invited.confirmed_at.present? + invited.update_columns( + confirmed_at: nil, + welcome_instructions_token: Devise.friendly_token, + welcome_instructions_created_at: 3.days.ago, + welcome_instructions_sent_at: 3.days.ago + ) +end +unless invited.person.present? + person = Person.create!( + first_name: "Invited", + last_name: "Pending", + email: invited.email, + created_by: admin, + updated_by: admin, + profile_is_searchable: true + ) + invited.update!(person: person) +end + +# Clicked confirmation link but didn't set password +confirmed_no_pw = User.find_or_create_by!(email: "confirmed.nopassword@example.com") do |user| + user.password = "password" + user.super_user = false + user.confirmed_at = Time.current +end +unless confirmed_no_pw.welcome_instructions_token.present? + token = Devise.friendly_token + confirmed_no_pw.update_columns( + confirmed_at: 2.days.ago, + welcome_instructions_token: token, + welcome_instructions_created_at: 5.days.ago, + welcome_instructions_sent_at: 5.days.ago + ) +end +unless confirmed_no_pw.person.present? + person = Person.create!( + first_name: "Confirmed", + last_name: "NoPassword", + email: confirmed_no_pw.email, + created_by: admin, + updated_by: admin, + profile_is_searchable: true + ) + confirmed_no_pw.update!(person: person) +end + +# Locked account (too many failed attempts) +locked = User.find_or_create_by!(email: "locked.user@example.com") do |user| + user.password = "password" + user.super_user = false + user.confirmed_at = 1.month.ago +end +unless locked.locked_at.present? + locked.update_columns( + locked_at: 1.day.ago, + failed_attempts: Devise.maximum_attempts + ) +end +unless locked.person.present? + person = Person.create!( + first_name: "Locked", + last_name: "User", + email: locked.email, + created_by: admin, + updated_by: admin, + profile_is_searchable: true + ) + locked.update!(person: person) +end + +# Never invited (created but no confirmation sent) +never_invited = User.find_or_create_by!(email: "never.invited@example.com") do |user| + user.password = "password" + user.super_user = false + user.confirmed_at = Time.current +end +unless never_invited.welcome_instructions_sent_at.present? + never_invited.update_columns(confirmed_at: nil) +end +unless never_invited.person.present? + person = Person.create!( + first_name: "Never", + last_name: "Invited", + email: never_invited.email, + created_by: admin, + updated_by: admin, + profile_is_searchable: true + ) + never_invited.update!(person: person) +end + +# Invited a while ago, never clicked (stale invite) +stale_invited = User.find_or_create_by!(email: "stale.invite@example.com") do |user| + user.password = "password" + user.super_user = false + user.confirmed_at = Time.current +end +unless stale_invited.confirmed_at.present? + stale_invited.update_columns( + confirmed_at: nil, + welcome_instructions_token: Devise.friendly_token, + welcome_instructions_created_at: 45.days.ago, + welcome_instructions_sent_at: 45.days.ago + ) +end +unless stale_invited.person.present? + person = Person.create!( + first_name: "Stale", + last_name: "Invite", + email: stale_invited.email, + created_by: admin, + updated_by: admin, + profile_is_searchable: true + ) + stale_invited.update!(person: person) +end + +# Invited yesterday +recent_invited = User.find_or_create_by!(email: "recent.invite@example.com") do |user| + user.password = "password" + user.super_user = false + user.confirmed_at = Time.current +end +unless recent_invited.confirmed_at.present? + recent_invited.update_columns( + confirmed_at: nil, + welcome_instructions_token: Devise.friendly_token, + welcome_instructions_created_at: 1.day.ago, + welcome_instructions_sent_at: 1.day.ago + ) +end +unless recent_invited.person.present? + person = Person.create!( + first_name: "Recent", + last_name: "Invite", + email: recent_invited.email, + created_by: admin, + updated_by: admin, + profile_is_searchable: true + ) + recent_invited.update!(person: person) +end + +# Never invited, no person record either +User.find_or_create_by!(email: "orphan.uninvited@example.com") do |user| + user.password = "password" + user.super_user = false + user.confirmed_at = Time.current +end.tap do |u| + u.update_columns(confirmed_at: nil) unless u.welcome_instructions_sent_at.present? +end + +# Invited, no person record +invited_no_person = User.find_or_create_by!(email: "invited.noperson@example.com") do |user| + user.password = "password" + user.super_user = false + user.confirmed_at = Time.current +end +unless invited_no_person.confirmed_at.present? + invited_no_person.update_columns( + confirmed_at: nil, + welcome_instructions_token: Devise.friendly_token, + welcome_instructions_created_at: 10.days.ago, + welcome_instructions_sent_at: 10.days.ago + ) +end + +# Reset only these dev users' passwords (mirrors the base seed reset), so they +# stay loggable-in after a reseed without touching any other user. +dev_user_emails = %w[ + invited.pending@example.com + confirmed.nopassword@example.com + locked.user@example.com + never.invited@example.com + stale.invite@example.com + recent.invite@example.com + orphan.uninvited@example.com + invited.noperson@example.com +] +dev_user_password = Devise::Encryptor.digest(User, "password") +User.where(email: dev_user_emails).update_all(encrypted_password: dev_user_password) diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index b64fdc035..de412472f 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -1,7 +1,12 @@ namespace :db do namespace :seed do desc "Generate representative sample data for development" - task dev: [ :environment, "db:seed", "db:seed:dummy", "db:seed:payments" ] + task dev: [ :environment, "db:seed", "db:seed:users", "db:seed:dummy", "db:seed:payments" ] + + desc "Seed dev-only user variations (invite/lock/confirmation states)" + task users: :environment do + load Rails.root.join("db/seeds/dev/users.rb") + end desc "Seed generic dummy dev data (workshops, people, stories, etc.)" task dummy: :environment do From 5ed86896326f230fd67b359f04874ce28729f293 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 6 Jun 2026 17:09:18 -0400 Subject: [PATCH 2/2] Fix stale/pending invite seeds never becoming unconfirmed The invited.pending, stale.invite, recent.invite, and invited.noperson blocks guarded their update_columns with `unless .confirmed_at.present?`, but the find_or_create_by! block had just set confirmed_at to Time.current, so the guard never fired and these users ended up confirmed rather than as pending/stale invites. Key the guard on welcome_instructions_token presence instead (matching the confirmed_no_pw block), so the invite state is applied once and stays idempotent on reseed. update_columns is retained to set the unconfirmed state without firing Devise callbacks (no confirmation email). Co-Authored-By: Claude Opus 4.8 --- db/seeds/dev/users.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/db/seeds/dev/users.rb b/db/seeds/dev/users.rb index b092785e4..f6bc0f493 100644 --- a/db/seeds/dev/users.rb +++ b/db/seeds/dev/users.rb @@ -14,7 +14,7 @@ user.super_user = false user.confirmed_at = Time.current end -unless invited.confirmed_at.present? +unless invited.welcome_instructions_token.present? invited.update_columns( confirmed_at: nil, welcome_instructions_token: Devise.friendly_token, @@ -112,7 +112,7 @@ user.super_user = false user.confirmed_at = Time.current end -unless stale_invited.confirmed_at.present? +unless stale_invited.welcome_instructions_token.present? stale_invited.update_columns( confirmed_at: nil, welcome_instructions_token: Devise.friendly_token, @@ -138,7 +138,7 @@ user.super_user = false user.confirmed_at = Time.current end -unless recent_invited.confirmed_at.present? +unless recent_invited.welcome_instructions_token.present? recent_invited.update_columns( confirmed_at: nil, welcome_instructions_token: Devise.friendly_token, @@ -173,7 +173,7 @@ user.super_user = false user.confirmed_at = Time.current end -unless invited_no_person.confirmed_at.present? +unless invited_no_person.welcome_instructions_token.present? invited_no_person.update_columns( confirmed_at: nil, welcome_instructions_token: Devise.friendly_token,