Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ GEM
block_cipher_kit (0.0.4)
bootsnap (1.23.0)
msgpack (~> 1.2)
brakeman (8.0.6)
brakeman (8.1.0)
racc
browser (6.2.0)
builder (3.3.0)
Expand Down
2 changes: 1 addition & 1 deletion app/models/identity/backup_code.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Identity::BackupCode < ApplicationRecord
end
end
event :mark_discarded do
transitions from: [:active, :previewed], to: :discarded
transitions from: [ :active, :previewed ], to: :discarded
end
end

Expand Down
2 changes: 1 addition & 1 deletion app/views/identities/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
</svg>
<span><%= t(".email_typo", suggested: ERB::Util.html_escape(@identity.suggested_email)).html_safe %></span>
<span><%= t(".email_typo_html", suggested: @identity.suggested_email) %></span>
<div class="banner-actions">
<button type="button" class="secondary small-btn" onclick="
var suggested = this.closest('[data-suggested-email]').dataset.suggestedEmail;
Expand Down
19 changes: 19 additions & 0 deletions config/brakeman.ignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@
470
],
"note": "this is clamped to valid PublicIdentifiable models in @shortcodes.rb"
},
{
"warning_type": "Unmaintained Dependency",
"warning_code": 122,
"fingerprint": "98b26f60d776fd41ee6f088c833725145be9aac2d7c5b33780241c273622db42",
"check_name": "EOLRails",
"message": "Support for Rails 8.0.5.1 ends on 2026-11-07",
"file": "Gemfile.lock",
"line": 466,
"link": "https://brakemanscanner.org/docs/warning_types/unmaintained_dependency/",
"code": null,
"render_path": null,
"location": null,
"user_input": null,
"confidence": "Medium",
"cwe_id": [
1104
],
"note": "rails 8.0 support ends 2026-11-07 \u2014 upgrade to 8.1 before then and drop this ignore. added so --ensure-latest brakeman 8.1.0 doesn't block unrelated PRs."
}
],
"brakeman_version": "8.0.4"
Expand Down
2 changes: 1 addition & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ en:
email_code: We'll send a one-time code to this address.
continue: Continue
wrong_email: "← Wrong email? Go back"
email_typo: "Looks like a typo... did you mean <strong>%{suggested}</strong>?"
email_typo_html: "Looks like a typo... did you mean <strong>%{suggested}</strong>?"
email_typo_fix: "yeah, that one! →"
email_typo_dismiss: "no, it's correct"
update:
Expand Down
15 changes: 1 addition & 14 deletions spec/components/sidebar_spec.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,4 @@
require "spec_helper"

# Stub the component hierarchy so we can load Sidebar without Rails/Phlex
module Components; class Base; end; end
module Phlex; module Rails; module Helpers; module LinkTo; end; module CurrentPage; end; end; end; end

# Stub the macros that Sidebar's class body calls
class Components::Base
def self.register_value_helper(*); end
def self.register_output_helper(*); end
def self.include(*); end
end

require_relative "../../app/components/sidebar"
require "rails_helper"

RSpec.describe Components::Sidebar do
describe "#active_nav_item?" do
Expand Down
11 changes: 6 additions & 5 deletions spec/factories/verifications.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,35 +67,36 @@
factory :persona_verification, class: "Verification::PersonaVerification" do
association :identity
status { :draft }
persona_inquiry_id { "inq_#{SecureRandom.hex(12)}" }
# must match the record's inquiry_id — persona_record_matches_inquiry
persona_inquiry_id { persona_record&.inquiry_id || "inq_#{SecureRandom.hex(12)}" }

trait :with_inquiry do
persona_session_token { "session_#{SecureRandom.hex(16)}" }
end

trait :pending do
status { :pending }
association :persona_record, factory: :identity_persona_record
persona_record { association :identity_persona_record, identity: identity }
association :identity_document
end

trait :approved do
status { :approved }
association :persona_record, factory: :identity_persona_record
persona_record { association :identity_persona_record, identity: identity }
association :identity_document
end

trait :rejected do
status { :rejected }
rejection_reason { "info_mismatch" }
association :persona_record, factory: :identity_persona_record
persona_record { association :identity_persona_record, identity: identity }
end

trait :fatal_rejection do
status { :rejected }
fatal { true }
rejection_reason { "duplicate" }
association :persona_record, factory: :identity_persona_record
persona_record { association :identity_persona_record, identity: identity }
end
end
end
4 changes: 3 additions & 1 deletion spec/integration/persona_lifecycle_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
#
# also: the sad path (declined), resume flow, and idempotency.
RSpec.describe "Persona verification lifecycle", type: :request do
let(:identity) { create(:identity, persona_account_id: nil, legal_first_name: nil, legal_last_name: nil, birthday: Date.parse("2008-06-15")) }
# reload: prod loads the identity fresh each request, and the create-time
# before_commit leaves the in-memory record dirty, which with_lock refuses
let(:identity) { create(:identity, persona_account_id: nil, legal_first_name: nil, legal_last_name: nil, birthday: Date.parse("2008-06-15")).reload }
let(:session) do
identity.sessions.create!(
session_token: SecureRandom.hex(32),
Expand Down
7 changes: 7 additions & 0 deletions spec/models/identity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@

it "returns :persona even when aadhaar flag is also enabled" do
Flipper.enable(:authbridge_aadhaar_2025_07_10, identity)
Flipper.enable(:persona_verification_in_india_2026_06_05, identity)
identity.update!(country: "IN")

expect(identity.required_verification_method).to eq(:persona)
ensure
Flipper.disable(:authbridge_aadhaar_2025_07_10)
Flipper.disable(:persona_verification_in_india_2026_06_05)
end

it "returns :document for India until the India rollout flag is on" do
identity.update!(country: "IN")
expect(identity.required_verification_method).to eq(:document)
end

it "returns :document for persona-unsupported countries" do
Expand Down
5 changes: 4 additions & 1 deletion spec/models/verification/manual_verification_call_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,14 @@

it "pings guardians but sends no shared rejection mail on a fatal rejection" do
verification = create(:manual_verification_call)
# enqueued_jobs isn't cleared between model specs — only look at what
# the rejection itself enqueues
already_enqueued = ActiveJob::Base.queue_adapter.enqueued_jobs.size

expect { verification.mark_as_rejected!("fraud", "internal note") }
.to have_enqueued_job(Slack::NotifyGuardiansJob).with(verification.identity)

expect(ActiveJob::Base.queue_adapter.enqueued_jobs.map { |j| j[:job] })
expect(ActiveJob::Base.queue_adapter.enqueued_jobs.drop(already_enqueued).map { |j| j[:job] })
.not_to include(ActionMailer::MailDeliveryJob)

expect(verification.reload).to be_rejected
Expand Down
6 changes: 4 additions & 2 deletions spec/models/verification/persona_verification_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,12 @@
expect(identity.ysws_eligible).to be false
end

it "sends the approved mailer" do
# the pipeline job and the backend controllers send it themselves —
# sending it here as well would mail the user twice
it "leaves the approved mailer to the caller" do
expect {
verification.approve!
}.to have_enqueued_mail(VerificationMailer, :approved)
}.not_to have_enqueued_mail(VerificationMailer, :approved)
end
end

Expand Down
4 changes: 3 additions & 1 deletion spec/requests/authorized_applications_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ def tok(scopes:, app: program)

get authorized_applications_path
expect(response).to have_http_status(:ok)
expect(response.body.scan(program.name).size).to eq(1)
# the name also appears in each card's revoke confirmation, so count cards
cards = Nokogiri::HTML(response.body).css(".session-device").map { |el| el.text.strip }
expect(cards).to eq([ program.name ])
end
end

Expand Down
15 changes: 15 additions & 0 deletions spec/requests/step_up_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,24 @@
allow_any_instance_of(ApplicationController).to receive(:identity_signed_in?).and_return(true)
end

# the step-up page records the pending action in the session; the
# send/verify/resend endpoints refuse to run without it
def open_step_up(action_type, return_to)
get new_step_up_path(action_type: action_type, return_to: return_to)
end

describe "email step-up method blocked for email_change action" do
describe "POST /step_up/send_email_code" do
it "rejects email code request for email_change action" do
open_step_up("email_change", "/email_changes/new")
post "/step_up/send_email_code", params: { action_type: "email_change", return_to: "/email_changes/new" }

expect(response).to redirect_to(new_step_up_path(action_type: "email_change", return_to: "/email_changes/new"))
expect(flash[:error]).to eq("Email verification is not available for this action")
end

it "allows email code request for other actions" do
open_step_up("remove_totp", "/security")
post "/step_up/send_email_code", params: { action_type: "remove_totp", return_to: "/security" }

expect(response).to redirect_to(new_step_up_path(action_type: "remove_totp", method: :email, return_to: "/security", code_sent: true))
Expand All @@ -42,6 +50,8 @@
it "rejects email method verification for email_change action" do
login_code = identity.v2_login_codes.create!

open_step_up("email_change", "/email_changes/new")

post "/step_up/verify", params: {
action_type: "email_change",
method: "email",
Expand All @@ -56,6 +66,8 @@
it "allows TOTP verification for email_change action" do
code = ROTP::TOTP.new(totp.secret).now

open_step_up("email_change", "/email_changes/new")

post "/step_up/verify", params: {
action_type: "email_change",
method: "totp",
Expand All @@ -71,6 +83,8 @@
it "binds step-up to specific action type" do
code = ROTP::TOTP.new(totp.secret).now

open_step_up("oidc_reauth", "/oauth/authorize")

post "/step_up/verify", params: {
action_type: "oidc_reauth",
method: "totp",
Expand All @@ -86,6 +100,7 @@

describe "POST /step_up/resend_email" do
it "rejects resend for email_change action" do
open_step_up("email_change", "/email_changes/new")
post "/step_up/resend_email", params: { action_type: "email_change", return_to: "/email_changes/new" }

expect(response).to redirect_to(new_step_up_path(action_type: "email_change", return_to: "/email_changes/new"))
Expand Down
Loading