From f52ddb2a9c4281e22c53e6857598a342fada062d Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:03:33 -0400 Subject: [PATCH 01/15] changelog: Internal, MVP One Account, Match duplicates within same service provider --- app/services/idv/duplicate_ssn_finder.rb | 3 +- .../services/idv/duplicate_ssn_finder_spec.rb | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/app/services/idv/duplicate_ssn_finder.rb b/app/services/idv/duplicate_ssn_finder.rb index 324e69494f7..08e4dc243ed 100644 --- a/app/services/idv/duplicate_ssn_finder.rb +++ b/app/services/idv/duplicate_ssn_finder.rb @@ -7,6 +7,7 @@ class DuplicateSsnFinder def initialize(user:, ssn:) @user = user @ssn = ssn + @sp = user.profiles.last&.initiating_service_provider_issuer end def ssn_is_unique? @@ -17,7 +18,7 @@ def ssn_is_unique? def associated_facial_match_profiles_with_ssn Profile.active.facial_match.where(ssn_signature: ssn_signatures) - .where(initiating_service_provider_issuer: sp_eligible_for_one_account) + .where(initiating_service_provider_issuer: @sp) .where.not(user_id: user.id) end diff --git a/spec/services/idv/duplicate_ssn_finder_spec.rb b/spec/services/idv/duplicate_ssn_finder_spec.rb index 5f8000068ae..a55a260310e 100644 --- a/spec/services/idv/duplicate_ssn_finder_spec.rb +++ b/spec/services/idv/duplicate_ssn_finder_spec.rb @@ -4,6 +4,7 @@ describe '#ssn_is_unique?' do let(:ssn) { '123-45-6789' } let(:user) { create(:user) } + let(:sp) { 'urn:gov:gsa:openidconnect:inactive:sp:test' } subject { described_class.new(ssn: ssn, user: user) } @@ -144,4 +145,46 @@ end end end + + describe '#associated_facial_match_profiles_with_ssn' do + let(:ssn) { '123-45-6789' } + let(:user) { create(:user) } + let(:user2) { create(:user) } + + subject { described_class.new(ssn: ssn, user: user) } + + before do + allow(IdentityConfig.store).to receive(:eligible_one_account_providers) + .and_return([ + 'urn:gov:gsa:openidconnect:inactive:sp:test', + 'urn:gov:gsa:openidconnect:inactive:sp:test2', + ]) + end + context 'when ssn belongs to another profile with the same sp' do + it 'returns matching profile id' do + create(:profile, :facial_match_proof, id: 1, pii: { ssn: ssn }, user: user, active: true) + + create(:profile, :facial_match_proof, id: 2, pii: { ssn: ssn }, user: user2, active: true) + expect(subject.associated_facial_match_profiles_with_ssn.last.id).to eq(2) + end + end + + context 'when ssn belongs to another profile with a different sp' do + it 'returns matching profile id' do + sp2 = 'urn:gov:gsa:openidconnect:inactive:sp:test2' + create(:profile, :facial_match_proof, id: 1, pii: { ssn: ssn }, user: user, active: true) + + create( + :profile, + :facial_match_proof, + id: 2, + pii: { ssn: ssn }, + user: user2, + active: true, + initiating_service_provider_issuer: sp2, + ) + expect(subject.associated_facial_match_profiles_with_ssn.last).to eq(nil) + end + end + end end From a8002cb7e8f889770c98a0ef72cbebc4386a143e Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:59:22 -0400 Subject: [PATCH 02/15] include eligible sp list in match query --- app/services/idv/duplicate_ssn_finder.rb | 1 + spec/services/idv/duplicate_ssn_finder_spec.rb | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/services/idv/duplicate_ssn_finder.rb b/app/services/idv/duplicate_ssn_finder.rb index 08e4dc243ed..093c1497dd1 100644 --- a/app/services/idv/duplicate_ssn_finder.rb +++ b/app/services/idv/duplicate_ssn_finder.rb @@ -18,6 +18,7 @@ def ssn_is_unique? def associated_facial_match_profiles_with_ssn Profile.active.facial_match.where(ssn_signature: ssn_signatures) + .where(initiating_service_provider_issuer: sp_eligible_for_one_account) .where(initiating_service_provider_issuer: @sp) .where.not(user_id: user.id) end diff --git a/spec/services/idv/duplicate_ssn_finder_spec.rb b/spec/services/idv/duplicate_ssn_finder_spec.rb index a55a260310e..d0ee70b131c 100644 --- a/spec/services/idv/duplicate_ssn_finder_spec.rb +++ b/spec/services/idv/duplicate_ssn_finder_spec.rb @@ -170,7 +170,7 @@ end context 'when ssn belongs to another profile with a different sp' do - it 'returns matching profile id' do + it 'does not return matching profile' do sp2 = 'urn:gov:gsa:openidconnect:inactive:sp:test2' create(:profile, :facial_match_proof, id: 1, pii: { ssn: ssn }, user: user, active: true) @@ -186,5 +186,18 @@ expect(subject.associated_facial_match_profiles_with_ssn.last).to eq(nil) end end + + context 'when ssn belongs to another provider but sp has not opted in' do + before do + allow(IdentityConfig.store).to receive(:eligible_one_account_providers) + .and_return([]) + end + + it 'does not return matching profile' do + create(:profile, :facial_match_proof, id: 1, pii: { ssn: ssn }, user: user, active: true) + create(:profile, :facial_match_proof, id: 2, pii: { ssn: ssn }, user: user2, active: true) + expect(subject.associated_facial_match_profiles_with_ssn.last).to eq(nil) + end + end end end From 823f6f75b226fda0ee78b3ff95c831416821406e Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:09:38 -0400 Subject: [PATCH 03/15] correct word in supposition --- spec/services/idv/duplicate_ssn_finder_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/idv/duplicate_ssn_finder_spec.rb b/spec/services/idv/duplicate_ssn_finder_spec.rb index d0ee70b131c..d187b10e435 100644 --- a/spec/services/idv/duplicate_ssn_finder_spec.rb +++ b/spec/services/idv/duplicate_ssn_finder_spec.rb @@ -187,7 +187,7 @@ end end - context 'when ssn belongs to another provider but sp has not opted in' do + context 'when ssn belongs to same provider but sp has not opted in' do before do allow(IdentityConfig.store).to receive(:eligible_one_account_providers) .and_return([]) From 4e95e0361e93f1141a00a8c28748a3dc29e09dcc Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:00:22 -0400 Subject: [PATCH 04/15] remove profile service provider from initiator to fix unrelated test --- app/services/idv/duplicate_ssn_finder.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/services/idv/duplicate_ssn_finder.rb b/app/services/idv/duplicate_ssn_finder.rb index 093c1497dd1..b7cce557c86 100644 --- a/app/services/idv/duplicate_ssn_finder.rb +++ b/app/services/idv/duplicate_ssn_finder.rb @@ -7,7 +7,6 @@ class DuplicateSsnFinder def initialize(user:, ssn:) @user = user @ssn = ssn - @sp = user.profiles.last&.initiating_service_provider_issuer end def ssn_is_unique? @@ -19,7 +18,9 @@ def ssn_is_unique? def associated_facial_match_profiles_with_ssn Profile.active.facial_match.where(ssn_signature: ssn_signatures) .where(initiating_service_provider_issuer: sp_eligible_for_one_account) - .where(initiating_service_provider_issuer: @sp) + .where( + initiating_service_provider_issuer: user.profiles.last.initiating_service_provider_issuer, + ) .where.not(user_id: user.id) end From bcb25d9aba1aa783fa49e088a22690f5c78e78a8 Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:54:02 -0400 Subject: [PATCH 05/15] derive sp issuer from current session sp --- app/jobs/resolution_proofing_job.rb | 1 + app/services/duplicate_profile_checker.rb | 2 +- app/services/idv/duplicate_ssn_finder.rb | 7 ++++--- lib/data_pull.rb | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/jobs/resolution_proofing_job.rb b/app/jobs/resolution_proofing_job.rb index fd10aa46f1d..f6c8b6026b7 100644 --- a/app/jobs/resolution_proofing_job.rb +++ b/app/jobs/resolution_proofing_job.rb @@ -59,6 +59,7 @@ def perform( ssn_is_unique = Idv::DuplicateSsnFinder.new( ssn: applicant_pii[:ssn], user: user, + issuer: current_sp&.issuer || nil, ).ssn_is_unique? callback_log_data.result[:ssn_is_unique] = ssn_is_unique diff --git a/app/services/duplicate_profile_checker.rb b/app/services/duplicate_profile_checker.rb index d8136d4ea22..46adfd333eb 100644 --- a/app/services/duplicate_profile_checker.rb +++ b/app/services/duplicate_profile_checker.rb @@ -15,7 +15,7 @@ def check_for_duplicate_profiles cacher = Pii::Cacher.new(user, user_session) pii = cacher.fetch(profile.id) - duplicate_ssn_finder = Idv::DuplicateSsnFinder.new(user:, ssn: pii[:ssn]) + duplicate_ssn_finder = Idv::DuplicateSsnFinder.new(user:, ssn: pii[:ssn], issuer: sp.issuer) associated_profiles = duplicate_ssn_finder.associated_facial_match_profiles_with_ssn if !duplicate_ssn_finder.ial2_profile_ssn_is_unique? ids = associated_profiles.map(&:id) diff --git a/app/services/idv/duplicate_ssn_finder.rb b/app/services/idv/duplicate_ssn_finder.rb index b7cce557c86..efdd57c29b7 100644 --- a/app/services/idv/duplicate_ssn_finder.rb +++ b/app/services/idv/duplicate_ssn_finder.rb @@ -2,11 +2,12 @@ module Idv class DuplicateSsnFinder - attr_reader :ssn, :user + attr_reader :ssn, :user, :issuer - def initialize(user:, ssn:) + def initialize(user:, ssn:, issuer:) @user = user @ssn = ssn + @issuer = issuer end def ssn_is_unique? @@ -19,7 +20,7 @@ def associated_facial_match_profiles_with_ssn Profile.active.facial_match.where(ssn_signature: ssn_signatures) .where(initiating_service_provider_issuer: sp_eligible_for_one_account) .where( - initiating_service_provider_issuer: user.profiles.last.initiating_service_provider_issuer, + initiating_service_provider_issuer: issuer, ) .where.not(user_id: user.id) end diff --git a/lib/data_pull.rb b/lib/data_pull.rb index 30612465cac..6a17d5e1334 100644 --- a/lib/data_pull.rb +++ b/lib/data_pull.rb @@ -205,7 +205,7 @@ def run(args:, config:) require 'data_requests/deployed' ssns = args - ssn_finders = ssns.map { |ssn| Idv::DuplicateSsnFinder.new(user: nil, ssn: ssn) } + ssn_finders = ssns.map { |ssn| Idv::DuplicateSsnFinder.new(user: nil, ssn: ssn, issuer: nil) } ssn_signatures = ssn_finders.flat_map do |ssn_finder| ssn_finder.ssn_signatures end From 138d7a3970a5bb6caefaa6b20a7d1691c2debc07 Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:40:11 -0400 Subject: [PATCH 06/15] update spec to include passed issuer parameter --- spec/services/idv/duplicate_ssn_finder_spec.rb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spec/services/idv/duplicate_ssn_finder_spec.rb b/spec/services/idv/duplicate_ssn_finder_spec.rb index d187b10e435..bb035178be0 100644 --- a/spec/services/idv/duplicate_ssn_finder_spec.rb +++ b/spec/services/idv/duplicate_ssn_finder_spec.rb @@ -6,7 +6,7 @@ let(:user) { create(:user) } let(:sp) { 'urn:gov:gsa:openidconnect:inactive:sp:test' } - subject { described_class.new(ssn: ssn, user: user) } + subject { described_class.new(ssn: ssn, user: user, issuer: sp) } before do allow(IdentityConfig.store).to receive(:eligible_one_account_providers) @@ -72,8 +72,9 @@ describe '#associated_facial_match_profiles_with_ssn' do let(:ssn) { '123-45-6789' } let(:user) { create(:user) } + let(:sp) { 'urn:gov:gsa:openidconnect:inactive:sp:test' } - subject { described_class.new(ssn: ssn, user: user) } + subject { described_class.new(ssn: ssn, user: user, issuer: sp) } before do allow(IdentityConfig.store).to receive(:eligible_one_account_providers) @@ -111,8 +112,9 @@ describe '#ial2_profile_ssn_is_unique?' do let(:ssn) { '123-45-6789' } let(:user) { create(:user) } + let(:sp) { 'urn:gov:gsa:openidconnect:inactive:sp:test' } - subject { described_class.new(ssn: ssn, user: user) } + subject { described_class.new(ssn: ssn, user: user, issuer: sp) } before do allow(IdentityConfig.store).to receive(:eligible_one_account_providers) @@ -150,8 +152,9 @@ let(:ssn) { '123-45-6789' } let(:user) { create(:user) } let(:user2) { create(:user) } + let(:sp) { 'urn:gov:gsa:openidconnect:inactive:sp:test' } - subject { described_class.new(ssn: ssn, user: user) } + subject { described_class.new(ssn: ssn, user: user, issuer: sp) } before do allow(IdentityConfig.store).to receive(:eligible_one_account_providers) From 8fbee7f2c3e8bc3b4eedef83f44d9e8c3fdafb6c Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Tue, 15 Jul 2025 13:30:15 -0400 Subject: [PATCH 07/15] join sp_return_logs to profile as an interation on building the query --- app/models/profile.rb | 3 +++ app/models/sp_return_log.rb | 1 + app/services/idv/duplicate_ssn_finder.rb | 15 +++++++-------- yarn.lock | 6 +++--- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/app/models/profile.rb b/app/models/profile.rb index b6dc89b7d37..90d496e15b5 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -15,6 +15,9 @@ class Profile < ApplicationRecord optional: true # rubocop:enable Rails/InverseOf has_many :gpo_confirmation_codes, dependent: :destroy + # rubocop:disable Rails/HasManyOrHasOneDependent + has_many :sp_return_logs + # rubocop:enable Rails/HasManyOrHasOneDependent has_one :in_person_enrollment, dependent: :destroy validates :active, uniqueness: { scope: :user_id, if: :active? } diff --git a/app/models/sp_return_log.rb b/app/models/sp_return_log.rb index 30d9df3a23b..1a1e8ddd233 100644 --- a/app/models/sp_return_log.rb +++ b/app/models/sp_return_log.rb @@ -10,5 +10,6 @@ class SpReturnLog < ApplicationRecord class_name: 'ServiceProvider', foreign_key: 'profile_requested_issuer', primary_key: 'issuer' + belongs_to :profile # rubocop:enable Rails/InverseOf end diff --git a/app/services/idv/duplicate_ssn_finder.rb b/app/services/idv/duplicate_ssn_finder.rb index efdd57c29b7..6d7f6d05ce7 100644 --- a/app/services/idv/duplicate_ssn_finder.rb +++ b/app/services/idv/duplicate_ssn_finder.rb @@ -11,18 +11,17 @@ def initialize(user:, ssn:, issuer:) end def ssn_is_unique? - Profile.where(ssn_signature: ssn_signatures) - .where(initiating_service_provider_issuer: sp_eligible_for_one_account) - .where.not(user_id: user.id).empty? + Profile.where(ssn_signature: ssn_signatures).where.not(user_id: user.id).empty? end def associated_facial_match_profiles_with_ssn - Profile.active.facial_match.where(ssn_signature: ssn_signatures) - .where(initiating_service_provider_issuer: sp_eligible_for_one_account) - .where( - initiating_service_provider_issuer: issuer, - ) + Profile.joins(:sp_return_logs) + .active + .facial_match + .where(ssn_signature: ssn_signatures) + .where(sp_return_logs: { issuer: sp_eligible_for_one_account }) .where.not(user_id: user.id) + .distinct end def ial2_profile_ssn_is_unique? diff --git a/yarn.lock b/yarn.lock index d8886cf569d..45e7e638180 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2272,9 +2272,9 @@ camelcase@^6.0.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001688: - version "1.0.30001689" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz#67ca960dd5f443903e19949aeacc9d28f6e10910" - integrity sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g== + version "1.0.30001727" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz" + integrity sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q== chai-as-promised@^7.1.1: version "7.1.1" From 4a10c5f152d03a01ec31ffda8d616c4f49ebc312 Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Wed, 16 Jul 2025 13:03:42 -0400 Subject: [PATCH 08/15] leverage identities --- app/models/profile.rb | 2 +- app/services/idv/duplicate_ssn_finder.rb | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/models/profile.rb b/app/models/profile.rb index 90d496e15b5..7a4a2b532a3 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -16,7 +16,7 @@ class Profile < ApplicationRecord # rubocop:enable Rails/InverseOf has_many :gpo_confirmation_codes, dependent: :destroy # rubocop:disable Rails/HasManyOrHasOneDependent - has_many :sp_return_logs + has_many :identities, class_name: 'ServiceProviderIdentity' # rubocop:enable Rails/HasManyOrHasOneDependent has_one :in_person_enrollment, dependent: :destroy diff --git a/app/services/idv/duplicate_ssn_finder.rb b/app/services/idv/duplicate_ssn_finder.rb index 6d7f6d05ce7..67be782bcf4 100644 --- a/app/services/idv/duplicate_ssn_finder.rb +++ b/app/services/idv/duplicate_ssn_finder.rb @@ -15,11 +15,13 @@ def ssn_is_unique? end def associated_facial_match_profiles_with_ssn - Profile.joins(:sp_return_logs) + Profile.joins("INNER JOIN identities ON identities.user_id = profiles.user_id") .active .facial_match .where(ssn_signature: ssn_signatures) - .where(sp_return_logs: { issuer: sp_eligible_for_one_account }) + .where(identities: { service_provider: sp_eligible_for_one_account }) + .where(identities: { deleted_at: nil }) + .where(identities: { ial: 2 }) .where.not(user_id: user.id) .distinct end From bf7029289a46b7f8532bed8d784abf668b46b113 Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Mon, 21 Jul 2025 09:25:39 -0400 Subject: [PATCH 09/15] add testing fix lint error --- app/services/idv/duplicate_ssn_finder.rb | 2 +- .../duplicate_profile_checker_spec.rb | 20 ++++++ .../services/idv/duplicate_ssn_finder_spec.rb | 65 +++++++++++++++++-- 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/app/services/idv/duplicate_ssn_finder.rb b/app/services/idv/duplicate_ssn_finder.rb index 67be782bcf4..1afa4fcafe6 100644 --- a/app/services/idv/duplicate_ssn_finder.rb +++ b/app/services/idv/duplicate_ssn_finder.rb @@ -15,7 +15,7 @@ def ssn_is_unique? end def associated_facial_match_profiles_with_ssn - Profile.joins("INNER JOIN identities ON identities.user_id = profiles.user_id") + Profile.joins('INNER JOIN identities ON identities.user_id = profiles.user_id') .active .facial_match .where(ssn_signature: ssn_signatures) diff --git a/spec/services/duplicate_profile_checker_spec.rb b/spec/services/duplicate_profile_checker_spec.rb index b037f2a08a2..53645bed25b 100644 --- a/spec/services/duplicate_profile_checker_spec.rb +++ b/spec/services/duplicate_profile_checker_spec.rb @@ -60,10 +60,30 @@ profile.save end + let(:identity) do + build( + :service_provider_identity, + service_provider: sp.issuer, + session_uuid: SecureRandom.uuid, + ial: 2, + ) + end + + let(:identity2) do + build( + :service_provider_identity, + service_provider: sp.issuer, + session_uuid: SecureRandom.uuid, + ial: 2, + ) + end + before do session[:encrypted_profiles] = { profile.id.to_s => SessionEncryptor.new.kms_encrypt(active_pii.to_json), } + user.identities << identity + user2.identities << identity2 end it 'creates a new duplicate profile confirmation entry' do diff --git a/spec/services/idv/duplicate_ssn_finder_spec.rb b/spec/services/idv/duplicate_ssn_finder_spec.rb index bb035178be0..7a8092e6595 100644 --- a/spec/services/idv/duplicate_ssn_finder_spec.rb +++ b/spec/services/idv/duplicate_ssn_finder_spec.rb @@ -5,7 +5,6 @@ let(:ssn) { '123-45-6789' } let(:user) { create(:user) } let(:sp) { 'urn:gov:gsa:openidconnect:inactive:sp:test' } - subject { described_class.new(ssn: ssn, user: user, issuer: sp) } before do @@ -72,8 +71,24 @@ describe '#associated_facial_match_profiles_with_ssn' do let(:ssn) { '123-45-6789' } let(:user) { create(:user) } + let(:user2) { create(:user) } let(:sp) { 'urn:gov:gsa:openidconnect:inactive:sp:test' } - + let(:identity) do + build( + :service_provider_identity, + service_provider: sp, + session_uuid: SecureRandom.uuid, + ial: 2, + ) + end + let(:identity2) do + build( + :service_provider_identity, + service_provider: sp, + session_uuid: SecureRandom.uuid, + ial: 2, + ) + end subject { described_class.new(ssn: ssn, user: user, issuer: sp) } before do @@ -85,8 +100,10 @@ context 'when ssn is taken by different profile by and is IAL2' do it 'returns list different profile' do create(:profile, :facial_match_proof, pii: { ssn: ssn }, user: user, active: true) + user.identities << identity + create(:profile, :facial_match_proof, pii: { ssn: ssn }, user: user2, active: true) + user2.identities << identity2 - create(:profile, :facial_match_proof, pii: { ssn: ssn }, active: true) expect(subject.associated_facial_match_profiles_with_ssn.size).to eq(1) end end @@ -112,7 +129,24 @@ describe '#ial2_profile_ssn_is_unique?' do let(:ssn) { '123-45-6789' } let(:user) { create(:user) } + let(:user2) { create(:user) } let(:sp) { 'urn:gov:gsa:openidconnect:inactive:sp:test' } + let(:identity) do + build( + :service_provider_identity, + service_provider: sp, + session_uuid: SecureRandom.uuid, + ial: 2, + ) + end + let(:identity2) do + build( + :service_provider_identity, + service_provider: sp, + session_uuid: SecureRandom.uuid, + ial: 2, + ) + end subject { described_class.new(ssn: ssn, user: user, issuer: sp) } @@ -124,8 +158,9 @@ context 'when ssn is taken by different profile by and is IAL2' do it 'returns false' do create(:profile, :facial_match_proof, pii: { ssn: ssn }, user: user, active: true) - - create(:profile, :facial_match_proof, pii: { ssn: ssn }, active: true) + user.identities << identity + create(:profile, :facial_match_proof, pii: { ssn: ssn }, user: user2, active: true) + user2.identities << identity2 expect(subject.ial2_profile_ssn_is_unique?).to eq false end end @@ -153,7 +188,22 @@ let(:user) { create(:user) } let(:user2) { create(:user) } let(:sp) { 'urn:gov:gsa:openidconnect:inactive:sp:test' } - + let(:identity) do + build( + :service_provider_identity, + service_provider: sp, + session_uuid: SecureRandom.uuid, + ial: 2, + ) + end + let(:identity2) do + build( + :service_provider_identity, + service_provider: sp, + session_uuid: SecureRandom.uuid, + ial: 2, + ) + end subject { described_class.new(ssn: ssn, user: user, issuer: sp) } before do @@ -166,8 +216,9 @@ context 'when ssn belongs to another profile with the same sp' do it 'returns matching profile id' do create(:profile, :facial_match_proof, id: 1, pii: { ssn: ssn }, user: user, active: true) - + user.identities << identity create(:profile, :facial_match_proof, id: 2, pii: { ssn: ssn }, user: user2, active: true) + user2.identities << identity2 expect(subject.associated_facial_match_profiles_with_ssn.last.id).to eq(2) end end From a7fae57c67a14f2be836b1fc21fa85b8b3d48b29 Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:35:15 -0400 Subject: [PATCH 10/15] removes belongs link to profile --- app/models/sp_return_log.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/sp_return_log.rb b/app/models/sp_return_log.rb index 1a1e8ddd233..30d9df3a23b 100644 --- a/app/models/sp_return_log.rb +++ b/app/models/sp_return_log.rb @@ -10,6 +10,5 @@ class SpReturnLog < ApplicationRecord class_name: 'ServiceProvider', foreign_key: 'profile_requested_issuer', primary_key: 'issuer' - belongs_to :profile # rubocop:enable Rails/InverseOf end From 8ddb1e9cb47ff438973e0b43cfcb05cd0a5357a6 Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Thu, 24 Jul 2025 08:31:42 -0400 Subject: [PATCH 11/15] remove unnecessary model affiliation --- app/models/profile.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/models/profile.rb b/app/models/profile.rb index 7a4a2b532a3..b6dc89b7d37 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -15,9 +15,6 @@ class Profile < ApplicationRecord optional: true # rubocop:enable Rails/InverseOf has_many :gpo_confirmation_codes, dependent: :destroy - # rubocop:disable Rails/HasManyOrHasOneDependent - has_many :identities, class_name: 'ServiceProviderIdentity' - # rubocop:enable Rails/HasManyOrHasOneDependent has_one :in_person_enrollment, dependent: :destroy validates :active, uniqueness: { scope: :user_id, if: :active? } From 857818bdb740d50b9c7ebcb0aee179a9e5ab74cf Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Wed, 30 Jul 2025 09:31:48 -0400 Subject: [PATCH 12/15] add test cases --- .../duplicate_profile_checker_spec.rb | 2 - .../services/idv/duplicate_ssn_finder_spec.rb | 103 +++++++++++------- 2 files changed, 63 insertions(+), 42 deletions(-) diff --git a/spec/services/duplicate_profile_checker_spec.rb b/spec/services/duplicate_profile_checker_spec.rb index 53645bed25b..44dd844f744 100644 --- a/spec/services/duplicate_profile_checker_spec.rb +++ b/spec/services/duplicate_profile_checker_spec.rb @@ -64,7 +64,6 @@ build( :service_provider_identity, service_provider: sp.issuer, - session_uuid: SecureRandom.uuid, ial: 2, ) end @@ -73,7 +72,6 @@ build( :service_provider_identity, service_provider: sp.issuer, - session_uuid: SecureRandom.uuid, ial: 2, ) end diff --git a/spec/services/idv/duplicate_ssn_finder_spec.rb b/spec/services/idv/duplicate_ssn_finder_spec.rb index 7a8092e6595..3f6a4f4c7be 100644 --- a/spec/services/idv/duplicate_ssn_finder_spec.rb +++ b/spec/services/idv/duplicate_ssn_finder_spec.rb @@ -4,7 +4,7 @@ describe '#ssn_is_unique?' do let(:ssn) { '123-45-6789' } let(:user) { create(:user) } - let(:sp) { 'urn:gov:gsa:openidconnect:inactive:sp:test' } + let(:sp) { 'urn:gov:gsa:openidconnect:sp:test' } subject { described_class.new(ssn: ssn, user: user, issuer: sp) } before do @@ -72,7 +72,7 @@ let(:ssn) { '123-45-6789' } let(:user) { create(:user) } let(:user2) { create(:user) } - let(:sp) { 'urn:gov:gsa:openidconnect:inactive:sp:test' } + let(:sp) { 'urn:gov:gsa:openidconnect:sp:test' } let(:identity) do build( :service_provider_identity, @@ -130,7 +130,7 @@ let(:ssn) { '123-45-6789' } let(:user) { create(:user) } let(:user2) { create(:user) } - let(:sp) { 'urn:gov:gsa:openidconnect:inactive:sp:test' } + let(:sp) { 'urn:gov:gsa:openidconnect:sp:test' } let(:identity) do build( :service_provider_identity, @@ -185,14 +185,26 @@ describe '#associated_facial_match_profiles_with_ssn' do let(:ssn) { '123-45-6789' } + let(:ssn2) { '123-45-9999' } let(:user) { create(:user) } let(:user2) { create(:user) } - let(:sp) { 'urn:gov:gsa:openidconnect:inactive:sp:test' } + let(:user3) { create(:user) } + let(:user4) { create(:user) } + let(:user5) { create(:user) } + let(:user6) { create(:user) } + let(:sp) { 'urn:gov:gsa:openidconnect:sp:test' } + let(:sp2) { 'urn:gov:gsa:openidconnect:sp:test2' } let(:identity) do build( :service_provider_identity, service_provider: sp, - session_uuid: SecureRandom.uuid, + ial: 2, + ) + end + let(:identity1a) do + build( + :service_provider_identity, + service_provider: sp2, ial: 2, ) end @@ -200,7 +212,34 @@ build( :service_provider_identity, service_provider: sp, - session_uuid: SecureRandom.uuid, + ial: 2, + ) + end + let(:identity2a) do + build( + :service_provider_identity, + service_provider: sp2, + ial: 2, + ) + end + let(:identity3) do + build( + :service_provider_identity, + service_provider: sp, + ial: 2, + ) + end + let(:identity4) do + build( + :service_provider_identity, + service_provider: sp, + ial: 2, + ) + end + let(:identity4a) do + build( + :service_provider_identity, + service_provider: sp2, ial: 2, ) end @@ -209,47 +248,31 @@ before do allow(IdentityConfig.store).to receive(:eligible_one_account_providers) .and_return([ - 'urn:gov:gsa:openidconnect:inactive:sp:test', - 'urn:gov:gsa:openidconnect:inactive:sp:test2', + 'urn:gov:gsa:openidconnect:sp:test', + 'urn:gov:gsa:openidconnect:sp:test2', ]) - end - context 'when ssn belongs to another profile with the same sp' do - it 'returns matching profile id' do - create(:profile, :facial_match_proof, id: 1, pii: { ssn: ssn }, user: user, active: true) - user.identities << identity - create(:profile, :facial_match_proof, id: 2, pii: { ssn: ssn }, user: user2, active: true) - user2.identities << identity2 - expect(subject.associated_facial_match_profiles_with_ssn.last.id).to eq(2) - end + create(:profile, :facial_match_proof, id: 1, pii: { ssn: ssn }, user: user, active: true) + user.identities << [identity, identity1a] + create(:profile, :facial_match_proof, id: 2, pii: { ssn: ssn }, user: user2, active: true) + user2.identities << [identity2, identity2a] + create(:profile, :facial_match_proof, id: 3, pii: { ssn: ssn }, user: user3, active: true) + user3.identities << [identity3] + create(:profile, :facial_match_proof, id: 4, pii: { ssn: ssn2 }, user: user4, active: true) + user4.identities << [identity4, identity4a] end - context 'when ssn belongs to another profile with a different sp' do - it 'does not return matching profile' do - sp2 = 'urn:gov:gsa:openidconnect:inactive:sp:test2' - create(:profile, :facial_match_proof, id: 1, pii: { ssn: ssn }, user: user, active: true) - - create( - :profile, - :facial_match_proof, - id: 2, - pii: { ssn: ssn }, - user: user2, - active: true, - initiating_service_provider_issuer: sp2, - ) - expect(subject.associated_facial_match_profiles_with_ssn.last).to eq(nil) + context 'when ssn belongs to profiles with matching opted in sp' do + it 'returns matching profile ids' do + expect(subject.associated_facial_match_profiles_with_ssn.count).to eq(2) + expect(subject.associated_facial_match_profiles_with_ssn[0].id).to eq(2) + expect(subject.associated_facial_match_profiles_with_ssn[1].id).to eq(3) end end - context 'when ssn belongs to same provider but sp has not opted in' do - before do - allow(IdentityConfig.store).to receive(:eligible_one_account_providers) - .and_return([]) - end - + context 'when a profile has a unique ssn but belongs to matching opted in sp' do + subject { described_class.new(ssn: ssn2, user: user4, issuer: sp) } it 'does not return matching profile' do - create(:profile, :facial_match_proof, id: 1, pii: { ssn: ssn }, user: user, active: true) - create(:profile, :facial_match_proof, id: 2, pii: { ssn: ssn }, user: user2, active: true) + expect(subject.associated_facial_match_profiles_with_ssn.last).to eq(nil) end end From f38487079d127d98fe8f12db97042d44987a5a4a Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Wed, 30 Jul 2025 09:36:43 -0400 Subject: [PATCH 13/15] remove extra users --- spec/services/idv/duplicate_ssn_finder_spec.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/services/idv/duplicate_ssn_finder_spec.rb b/spec/services/idv/duplicate_ssn_finder_spec.rb index 3f6a4f4c7be..20f005dcf3f 100644 --- a/spec/services/idv/duplicate_ssn_finder_spec.rb +++ b/spec/services/idv/duplicate_ssn_finder_spec.rb @@ -190,8 +190,6 @@ let(:user2) { create(:user) } let(:user3) { create(:user) } let(:user4) { create(:user) } - let(:user5) { create(:user) } - let(:user6) { create(:user) } let(:sp) { 'urn:gov:gsa:openidconnect:sp:test' } let(:sp2) { 'urn:gov:gsa:openidconnect:sp:test2' } let(:identity) do From 58cd50e03cdce73288b479fb9cdf514ab56c7f8f Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Wed, 30 Jul 2025 09:40:48 -0400 Subject: [PATCH 14/15] replace changes to yarn lock --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 45e7e638180..d8886cf569d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2272,9 +2272,9 @@ camelcase@^6.0.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001688: - version "1.0.30001727" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz" - integrity sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q== + version "1.0.30001689" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz#67ca960dd5f443903e19949aeacc9d28f6e10910" + integrity sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g== chai-as-promised@^7.1.1: version "7.1.1" From 07586440aab1b46bb37a81362151ef03d34b3adc Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Wed, 30 Jul 2025 10:22:14 -0400 Subject: [PATCH 15/15] fix tests to receive sp placeholder --- spec/services/idv/duplicate_ssn_finder_spec.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/spec/services/idv/duplicate_ssn_finder_spec.rb b/spec/services/idv/duplicate_ssn_finder_spec.rb index 20f005dcf3f..426a9f323df 100644 --- a/spec/services/idv/duplicate_ssn_finder_spec.rb +++ b/spec/services/idv/duplicate_ssn_finder_spec.rb @@ -4,7 +4,7 @@ describe '#ssn_is_unique?' do let(:ssn) { '123-45-6789' } let(:user) { create(:user) } - let(:sp) { 'urn:gov:gsa:openidconnect:sp:test' } + let(:sp) { OidcAuthHelper::OIDC_FACIAL_MATCH_ISSUER } subject { described_class.new(ssn: ssn, user: user, issuer: sp) } before do @@ -72,7 +72,7 @@ let(:ssn) { '123-45-6789' } let(:user) { create(:user) } let(:user2) { create(:user) } - let(:sp) { 'urn:gov:gsa:openidconnect:sp:test' } + let(:sp) { OidcAuthHelper::OIDC_FACIAL_MATCH_ISSUER } let(:identity) do build( :service_provider_identity, @@ -130,7 +130,7 @@ let(:ssn) { '123-45-6789' } let(:user) { create(:user) } let(:user2) { create(:user) } - let(:sp) { 'urn:gov:gsa:openidconnect:sp:test' } + let(:sp) { OidcAuthHelper::OIDC_FACIAL_MATCH_ISSUER } let(:identity) do build( :service_provider_identity, @@ -190,7 +190,7 @@ let(:user2) { create(:user) } let(:user3) { create(:user) } let(:user4) { create(:user) } - let(:sp) { 'urn:gov:gsa:openidconnect:sp:test' } + let(:sp) { OidcAuthHelper::OIDC_FACIAL_MATCH_ISSUER } let(:sp2) { 'urn:gov:gsa:openidconnect:sp:test2' } let(:identity) do build( @@ -246,7 +246,7 @@ before do allow(IdentityConfig.store).to receive(:eligible_one_account_providers) .and_return([ - 'urn:gov:gsa:openidconnect:sp:test', + OidcAuthHelper::OIDC_FACIAL_MATCH_ISSUER, 'urn:gov:gsa:openidconnect:sp:test2', ]) create(:profile, :facial_match_proof, id: 1, pii: { ssn: ssn }, user: user, active: true) @@ -270,7 +270,6 @@ context 'when a profile has a unique ssn but belongs to matching opted in sp' do subject { described_class.new(ssn: ssn2, user: user4, issuer: sp) } it 'does not return matching profile' do - expect(subject.associated_facial_match_profiles_with_ssn.last).to eq(nil) end end