From 161270b2d70db80c918d38898383da33b4b1eac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20G=C3=B3mez?= Date: Thu, 22 Oct 2026 07:46:17 +0200 Subject: [PATCH 1/7] =?UTF-8?q?fix(identity):=20un=20listado=20del=20DNIe?= =?UTF-8?q?=20abre=20una=20sola=20sesi=C3=B3n=20con=20la=20tarjeta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements #2093: Un listado del DNIe abre una sola sesión con la tarjeta - list lee titulares e intermedia en una sesión y guarda lo leído para el every_certificate siguiente - lo guardado se consume una vez: la tarjeta no se retiene y el puerto Token no cambia --- .../src/identity/adapters/macos_store/dnie.rs | 76 +++++++++++++------ .../adapters/macos_store/dnie/tests.rs | 38 ++++++++++ 2 files changed, 92 insertions(+), 22 deletions(-) diff --git a/rfirma-app/src-tauri/src/identity/adapters/macos_store/dnie.rs b/rfirma-app/src-tauri/src/identity/adapters/macos_store/dnie.rs index 96e2d0d4..1dba9e6a 100644 --- a/rfirma-app/src-tauri/src/identity/adapters/macos_store/dnie.rs +++ b/rfirma-app/src-tauri/src/identity/adapters/macos_store/dnie.rs @@ -57,6 +57,7 @@ struct Login { shown_attempts: Option, depth: usize, cut: Option, + last_read: Option<(Store, Vec)>, } impl DnieToken { @@ -133,30 +134,61 @@ impl DnieToken { .map_err(|error| token_error(&error)) } - fn read( - &self, - store: &Store, - with_the_intermediate: bool, - ) -> Result, TokenError> { - let mut session = match self.opener.open() { - Ok(session) => session, - Err(error) if is_no_readable_dnie(&error) => return Ok(Vec::new()), - Err(error) => return Err(token_error(&error)), + /// Titulares y CA intermedia en una sola sesión; todo lo leído se queda para la cadena del mismo listado. + fn read_holders(&self, store: &Store) -> Result, TokenError> { + let Some(mut session) = self.open_if_readable()? else { + return Ok(Vec::new()); }; - let mut found: Vec = session - .certificates() - .map_err(|error| token_error(&error))? - .into_iter() - .map(|certificate| in_the_store(store, &certificate.label, certificate.der)) - .collect(); - if with_the_intermediate { - let intermediate = session - .certificate(INTERMEDIATE_CA_LABEL) - .map_err(|error| token_error(&error))?; - found.extend(intermediate.map(|der| in_the_store(store, INTERMEDIATE_CA_LABEL, der))); + let holders = holders_of(store, &mut session)?; + let intermediate = session + .certificate(INTERMEDIATE_CA_LABEL) + .ok() + .flatten() + .map(|der| in_the_store(store, INTERMEDIATE_CA_LABEL, der)); + self.login().last_read = intermediate.map(|intermediate| { + let mut every = holders.clone(); + every.push(intermediate); + (store.clone(), every) + }); + Ok(holders) + } + + fn read_every_certificate(&self, store: &Store) -> Result, TokenError> { + if let Some((kept, every)) = self.login().last_read.take() { + if &kept == store { + return Ok(every); + } } + let Some(mut session) = self.open_if_readable()? else { + return Ok(Vec::new()); + }; + let mut found = holders_of(store, &mut session)?; + let intermediate = session + .certificate(INTERMEDIATE_CA_LABEL) + .map_err(|error| token_error(&error))?; + found.extend(intermediate.map(|der| in_the_store(store, INTERMEDIATE_CA_LABEL, der))); Ok(found) } + + fn open_if_readable(&self) -> Result>, TokenError> { + match self.opener.open() { + Ok(session) => Ok(Some(session)), + Err(error) if is_no_readable_dnie(&error) => Ok(None), + Err(error) => Err(token_error(&error)), + } + } +} + +fn holders_of( + store: &Store, + session: &mut Session, +) -> Result, TokenError> { + Ok(session + .certificates() + .map_err(|error| token_error(&error))? + .into_iter() + .map(|certificate| in_the_store(store, &certificate.label, certificate.der)) + .collect()) } /// La versión de rdnie que fija `Cargo.lock`. @@ -264,11 +296,11 @@ fn has_a_length_the_dnie_admits(secret: &ProtectedSecret) -> bool { impl Token for DnieToken { fn list(&self, store: &Store) -> Result, TokenError> { - self.read(store, false) + self.read_holders(store) } fn every_certificate(&self, store: &Store) -> Result, TokenError> { - self.read(store, true) + self.read_every_certificate(store) } fn list_authenticated( diff --git a/rfirma-app/src-tauri/src/identity/adapters/macos_store/dnie/tests.rs b/rfirma-app/src-tauri/src/identity/adapters/macos_store/dnie/tests.rs index 6a220fde..2925128f 100644 --- a/rfirma-app/src-tauri/src/identity/adapters/macos_store/dnie/tests.rs +++ b/rfirma-app/src-tauri/src/identity/adapters/macos_store/dnie/tests.rs @@ -1,3 +1,6 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; + use openssl::asn1::Asn1Time; use openssl::bn::BigNum; use openssl::hash::MessageDigest; @@ -250,6 +253,41 @@ fn the_intermediate_ca_completes_the_chain_of_both_certificates_for_a_site() { } } +#[test] +fn a_full_listing_with_holders_and_chain_opens_the_card_once() { + for (version, card) in the_two_versions() { + let police = OfThePolice::new(); + let opened = Arc::new(AtomicUsize::new(0)); + let counter = Arc::clone(&opened); + let dnie = the_dnie(move || { + counter.fetch_add(1, Ordering::SeqCst); + police.on(card()) + }); + + let offered = certificates_with_their_chains(&dnie, &[dnie_store()]) + .expect("el DNIe deberia listarse"); + + assert_eq!(offered.len(), 2, "{version}"); + assert_eq!(opened.load(Ordering::SeqCst), 1, "{version}"); + } +} + +#[test] +fn the_chain_of_a_later_listing_is_read_from_the_card_again() { + let police = OfThePolice::new(); + let opened = Arc::new(AtomicUsize::new(0)); + let counter = Arc::clone(&opened); + let dnie = the_dnie(move || { + counter.fetch_add(1, Ordering::SeqCst); + police.on(EmulatedDnie::dnie_3()) + }); + + certificates_with_their_chains(&dnie, &[dnie_store()]).expect("el DNIe deberia listarse"); + certificates_with_their_chains(&dnie, &[dnie_store()]).expect("el DNIe deberia listarse"); + + assert_eq!(opened.load(Ordering::SeqCst), 2); +} + #[test] fn the_signing_panel_offers_only_the_signing_certificate_of_the_dnie() { for (version, card) in the_two_versions() { From abdc4e9772d6b283ab9f7cd35f81dd0150513082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20G=C3=B3mez?= Date: Thu, 22 Oct 2026 07:53:03 +0200 Subject: [PATCH 2/7] =?UTF-8?q?perf(identity):=20resolver=20el=20certifica?= =?UTF-8?q?do=20elegido=20contra=20el=20=C3=BAltimo=20listado,=20sin=20abr?= =?UTF-8?q?ir=20almacenes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements #2092: Resolver el certificado elegido contra el último listado, sin abrir almacenes - chosen lee de LastListing y solo lista si no hay ningún listado - los listados guardan cada certificado con sus emisores - quitar un .p12 lo saca del último listado --- .../src-tauri/src/identity/adapters/tauri.rs | 1 + .../src/identity/application/certificates.rs | 17 +++- .../application/certificates/tests/copies.rs | 1 + .../application/certificates/tests/removal.rs | 31 +++++++ .../certificates/tests/system_install.rs | 2 + .../src/identity/application/readers.rs | 36 +++++++- .../src/identity/application/readers/tests.rs | 84 +++++++++++++++++++ rfirma-app/src-tauri/src/identity/mod.rs | 11 ++- .../src-tauri/tests/installed_p12_removal.rs | 1 + 9 files changed, 175 insertions(+), 9 deletions(-) diff --git a/rfirma-app/src-tauri/src/identity/adapters/tauri.rs b/rfirma-app/src-tauri/src/identity/adapters/tauri.rs index df94039f..4de0a58d 100644 --- a/rfirma-app/src-tauri/src/identity/adapters/tauri.rs +++ b/rfirma-app/src-tauri/src/identity/adapters/tauri.rs @@ -118,6 +118,7 @@ pub fn remove_certificate(id: String, identity: State<'_, IdentityRoot>) -> Resu &id, &identity.listed, &identity.installed_copies, + &identity.last_listing, )?, ) } diff --git a/rfirma-app/src-tauri/src/identity/application/certificates.rs b/rfirma-app/src-tauri/src/identity/application/certificates.rs index 75573f3f..849570cc 100644 --- a/rfirma-app/src-tauri/src/identity/application/certificates.rs +++ b/rfirma-app/src-tauri/src/identity/application/certificates.rs @@ -60,7 +60,7 @@ pub fn listed_rows( memory: &dyn CertificateMemory, last: &LastListing, ) -> Result, TokenError> { - let found = token.list_across(stores)?; + let found = with_their_chains(token, token.list_across(stores)?); last.keep(&found); Ok(rows_keeping_handles( on_the_desktop(found), @@ -95,10 +95,17 @@ pub fn certificates_with_their_chains( token: &dyn Token, stores: &[Store], ) -> Result, TokenError> { - let found = holders_only(token.list_across(stores)?); + Ok(with_their_chains( + token, + holders_only(token.list_across(stores)?), + )) +} + +/// Cada certificado con los emisores que su propio almacén aporta. +pub fn with_their_chains(token: &dyn Token, found: Vec) -> Vec { let mut neighbours: HashMap> = HashMap::new(); - Ok(found + found .into_iter() .map(|certificate| { let store = certificate.reference().store(); @@ -108,7 +115,7 @@ pub fn certificates_with_their_chains( let issuers = issuers_of(&certificate, in_the_store); certificate.with_its_issuers(issuers) }) - .collect()) + .collect() } /// Lo que se enseña en el escritorio: sin autoridades ni el de autenticación del DNIe, como AutoFirma. @@ -422,6 +429,7 @@ pub fn remove_installed( handle: &str, listed: &ListedCertificates, installed_copies: &ListedCertificates, + last: &LastListing, ) -> Result<(), InstallError> { let reference = match installed_copies.get(handle) { Some(installed) => installed, @@ -440,6 +448,7 @@ pub fn remove_installed( keyring.pin()? }; token.remove_certificate(installed_dir, &reference, &pin)?; + last.forget(&reference); if memory.remembered_certificate().as_ref() == Some(&reference) { let _ = memory.forget_the_certificate(); } diff --git a/rfirma-app/src-tauri/src/identity/application/certificates/tests/copies.rs b/rfirma-app/src-tauri/src/identity/application/certificates/tests/copies.rs index 620c888a..85c137aa 100644 --- a/rfirma-app/src-tauri/src/identity/application/certificates/tests/copies.rs +++ b/rfirma-app/src-tauri/src/identity/application/certificates/tests/copies.rs @@ -338,6 +338,7 @@ fn a_row_signed_with_a_remembered_browser_copy_still_removes_its_installed_copy( &rows[0].id, &listed, &installed_copies, + &crate::identity::application::readers::LastListing::default(), ) .expect("la copia instalada deberia poder quitarse aunque la elegida sea de firefox"); } diff --git a/rfirma-app/src-tauri/src/identity/application/certificates/tests/removal.rs b/rfirma-app/src-tauri/src/identity/application/certificates/tests/removal.rs index b7797a2c..a99b8032 100644 --- a/rfirma-app/src-tauri/src/identity/application/certificates/tests/removal.rs +++ b/rfirma-app/src-tauri/src/identity/application/certificates/tests/removal.rs @@ -134,6 +134,7 @@ fn removing_refuses_a_handle_that_is_not_from_the_last_listing() { "00000000000000000000000000000000", &listed, &ListedCertificates::new(), + &crate::identity::application::readers::LastListing::default(), ) .expect_err("no es de la ultima busqueda"); @@ -158,6 +159,7 @@ fn removing_refuses_a_certificate_from_somewhere_else() { &handle, &listed, &ListedCertificates::new(), + &crate::identity::application::readers::LastListing::default(), ) .expect_err("no viene de un .p12 instalado"); @@ -183,6 +185,7 @@ fn removing_asks_the_keyring_for_the_pin_and_propagates_its_refusal() { &handle, &listed, &ListedCertificates::new(), + &crate::identity::application::readers::LastListing::default(), ) .expect_err("sin PIN todavia no hay nada que quitar"); @@ -209,6 +212,7 @@ fn removing_propagates_the_token_refusal() { &handle, &listed, &ListedCertificates::new(), + &crate::identity::application::readers::LastListing::default(), ) .expect_err("el token se ha negado"); @@ -236,6 +240,7 @@ fn removing_the_remembered_certificate_forgets_it() { &handle, &listed, &ListedCertificates::new(), + &crate::identity::application::readers::LastListing::default(), ) .expect("deberia quitarse"); @@ -265,8 +270,34 @@ fn removing_a_different_certificate_leaves_the_remembered_one_alone() { &handle, &listed, &ListedCertificates::new(), + &crate::identity::application::readers::LastListing::default(), ) .expect("deberia quitarse"); assert_eq!(memory.remembered_certificate(), Some(remembered)); } + +#[test] +fn a_removed_p12_leaves_the_last_listing_at_once() { + let home = tempfile::tempdir().expect("deberia haber directorio temporal"); + let installed = home.path().join("certificates"); + let reference = an_installed_reference(&installed, 0x01); + let last = crate::identity::application::readers::LastListing::default(); + last.keep(&[TokenCertificate::new(reference.clone(), Vec::new())]); + let listed = ListedCertificates::new(); + let handle = listed.mint(reference); + + remove_installed( + &RemovalOutcome(Ok(())), + &FixedPinKeyring("1234"), + &a_memory(home.path()), + &installed, + &handle, + &listed, + &ListedCertificates::new(), + &last, + ) + .expect("deberia quitarse"); + + assert_eq!(last.found().map(|found| found.len()), Some(0)); +} diff --git a/rfirma-app/src-tauri/src/identity/application/certificates/tests/system_install.rs b/rfirma-app/src-tauri/src/identity/application/certificates/tests/system_install.rs index 237b3dfd..e14d79d3 100644 --- a/rfirma-app/src-tauri/src/identity/application/certificates/tests/system_install.rs +++ b/rfirma-app/src-tauri/src/identity/application/certificates/tests/system_install.rs @@ -221,6 +221,7 @@ fn removing_a_macos_store_certificate_asks_the_system_and_forgets_it() { &handle, &listed, &ListedCertificates::new(), + &crate::identity::application::readers::LastListing::default(), ); assert_eq!(removed, Ok(())); @@ -243,6 +244,7 @@ fn a_macos_store_certificate_rfirma_did_not_install_is_not_removed() { &handle, &listed, &ListedCertificates::new(), + &crate::identity::application::readers::LastListing::default(), ) .expect_err("no lo instalo rFirma"); diff --git a/rfirma-app/src-tauri/src/identity/application/readers.rs b/rfirma-app/src-tauri/src/identity/application/readers.rs index 2f314b6a..aa038246 100644 --- a/rfirma-app/src-tauri/src/identity/application/readers.rs +++ b/rfirma-app/src-tauri/src/identity/application/readers.rs @@ -7,9 +7,10 @@ use std::thread::JoinHandle; use crate::desktop::domain::channel::Channel; use crate::desktop::domain::platform::Platform; use crate::identity::application::certificates::{ - on_the_desktop, rows_keeping_handles, ListedCertificates, + on_the_desktop, rows_keeping_handles, usable_certificate, with_their_chains, ListedCertificates, }; use crate::identity::domain::certificate::{CertificateRef, ListedCertificate, TokenCertificate}; +use crate::identity::domain::error::TokenError; use crate::identity::domain::readers::{ ready_card_among, status_of, Listing, Reader, ReaderStatus, ReadyCard, }; @@ -28,6 +29,18 @@ impl LastListing { *lock(&self.found) = Some(found.to_vec()); } + /// El último listado completo, si ya hubo alguno. + pub fn found(&self) -> Option> { + lock(&self.found).clone() + } + + /// Saca del último listado el certificado que ya no está, sin abrir ningún almacén. + pub fn forget(&self, reference: &CertificateRef) { + if let Some(found) = lock(&self.found).as_mut() { + found.retain(|certificate| certificate.reference() != reference); + } + } + /// Los certificados del último listado de los almacenes que no enseñan tarjetas, si ya hubo alguno. fn other_than_cards(&self, installed_dir: &Path) -> Option> { lock(&self.found).as_ref().map(|found| { @@ -40,6 +53,25 @@ impl LastListing { } } +/// El certificado del asa, tomado del último listado; solo si no hay ninguno lista los almacenes y lo guarda (ADR-0011). +pub fn chosen_from_the_last_listing( + token: &dyn Token, + stores: &[Store], + last: &LastListing, + listed: &ListedCertificates, + handle: &str, +) -> Result { + let found = match last.found() { + Some(found) => found, + None => { + let found = with_their_chains(token, token.list_across(stores)?); + last.keep(&found); + found + } + }; + usable_certificate(&found, handle, listed).cloned() +} + /// Si esta plataforma y este canal pueden vigilar lectores (ADR-0048). pub fn watches_the_readers(platform: Platform, channel: Channel) -> bool { match platform { @@ -187,7 +219,7 @@ fn listed_in(token: &dyn Token, stores: &[Store]) -> Vec { if stores.is_empty() { return Vec::new(); } - token.list_across(stores).unwrap_or_default() + with_their_chains(token, token.list_across(stores).unwrap_or_default()) } /// Si el almacén se vuelve a listar al cambiar los lectores: uno de tarjeta, o el de Windows o macOS, que copian las suyas. diff --git a/rfirma-app/src-tauri/src/identity/application/readers/tests.rs b/rfirma-app/src-tauri/src/identity/application/readers/tests.rs index 5c2955f4..f8600d22 100644 --- a/rfirma-app/src-tauri/src/identity/application/readers/tests.rs +++ b/rfirma-app/src-tauri/src/identity/application/readers/tests.rs @@ -6,6 +6,7 @@ use crate::identity::application::certificates::listed_rows; use crate::identity::application::tests::{NoMemory, NoToken, TestAuthority}; use crate::identity::domain::algorithm::SignatureAlgorithm; use crate::identity::domain::certificate::CertificateRef; +use crate::identity::domain::error::Situation; use crate::identity::domain::error::TokenError; use crate::identity::domain::protected_secret::ProtectedSecret; use crate::identity::domain::secret::StoreSecret; @@ -550,3 +551,86 @@ fn another_card_on_macos_that_no_named_module_reads_is_unreadable() { vec![ReaderStatus::Reading, ReaderStatus::Unreadable] ); } + +fn chosen_by( + arranged: &Arranged, + last: &LastListing, + handle: &str, +) -> Result { + chosen_from_the_last_listing( + &arranged.token, + &arranged.stores, + last, + &arranged.listed, + handle, + ) +} + +fn some_handle(arranged: &Arranged) -> String { + arranged.listed_in_full()[0].id.clone() +} + +#[test] +fn after_a_full_listing_the_chosen_certificate_is_resolved_without_opening_a_store() { + let arranged = Arranged::new(); + let handle = some_handle(&arranged); + arranged.token.asked(); + + chosen_by(&arranged, &arranged.last, &handle).expect("esta en el último listado"); + + assert_eq!(arranged.token.asked(), Vec::::new()); +} + +#[test] +fn with_no_listing_yet_the_chosen_certificate_lists_once_and_the_next_does_not() { + let arranged = Arranged::new(); + let handle = some_handle(&arranged); + let nothing_listed_yet = LastListing::default(); + arranged.token.asked(); + + chosen_by(&arranged, ¬hing_listed_yet, &handle).expect("lista y lo encuentra"); + assert!(!arranged.token.asked().is_empty()); + + chosen_by(&arranged, ¬hing_listed_yet, &handle).expect("ya está guardado"); + assert_eq!(arranged.token.asked(), Vec::::new()); +} + +#[test] +fn a_card_that_came_in_through_the_readers_resolves_without_listing_and_fails_by_name_once_out() { + let (card, token) = WindowsWithACard::with_the_dnie(); + let arranged = Arranged::over(token, vec![the_windows_store()]); + let heard = followed(&arranged, &card, &[true]); + let handle = heard + .iter() + .filter_map(|news| news.certificates.as_ref()) + .flatten() + .find(|row| row.label.contains(ON_THE_DNIE)) + .expect("la tarjeta tiene fila") + .id + .clone(); + arranged.token.asked(); + + chosen_by(&arranged, &arranged.last, &handle).expect("entró por la lista en caliente"); + assert_eq!(arranged.token.asked(), Vec::::new()); + + followed(&arranged, &card, &[true, false]); + arranged.token.asked(); + let failure = chosen_by(&arranged, &arranged.last, &handle).expect_err("la tarjeta se fue"); + + assert_eq!(failure.situation(), Situation::CertificateNotFound); + assert_eq!(arranged.token.asked(), Vec::::new()); +} + +#[test] +fn a_forgotten_certificate_is_no_longer_resolved_and_nothing_is_listed() { + let arranged = Arranged::new(); + let handle = some_handle(&arranged); + let reference = arranged.listed.get(&handle).expect("asa"); + arranged.token.asked(); + + arranged.last.forget(&reference); + + let failure = chosen_by(&arranged, &arranged.last, &handle).expect_err("ya no está"); + assert_eq!(failure.situation(), Situation::CertificateNotFound); + assert_eq!(arranged.token.asked(), Vec::::new()); +} diff --git a/rfirma-app/src-tauri/src/identity/mod.rs b/rfirma-app/src-tauri/src/identity/mod.rs index 21e1305a..67c08c4f 100644 --- a/rfirma-app/src-tauri/src/identity/mod.rs +++ b/rfirma-app/src-tauri/src/identity/mod.rs @@ -120,10 +120,15 @@ impl IdentityRoot { application::certificates::usable_certificate(found, handle, &self.listed) } - /// El certificado del asa, listado de nuevo y comprobado antes de usarlo. + /// El certificado del asa, tomado del último listado y comprobado antes de usarlo. pub fn chosen(&self, handle: &str) -> Result { - let found = self.certificates()?; - self.usable(&found, handle).cloned() + application::readers::chosen_from_the_last_listing( + self.token.as_ref(), + &self.all_stores(), + &self.last_listing, + &self.listed, + handle, + ) } /// Apunta el certificado con el que se acaba de firmar. diff --git a/rfirma-app/src-tauri/tests/installed_p12_removal.rs b/rfirma-app/src-tauri/tests/installed_p12_removal.rs index 29b42258..3b06b403 100644 --- a/rfirma-app/src-tauri/tests/installed_p12_removal.rs +++ b/rfirma-app/src-tauri/tests/installed_p12_removal.rs @@ -148,6 +148,7 @@ fn remove( handle, listed, &ListedCertificates::new(), + &rfirma_lib::identity::application::readers::LastListing::default(), )?) } From 572bc8a0fd06b7c3aa8738b283c5a55a147141e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20G=C3=B3mez?= Date: Thu, 22 Oct 2026 08:38:48 +0200 Subject: [PATCH 3/7] =?UTF-8?q?fix(pr):=20atiende=20la=20revisi=C3=B3n=20d?= =?UTF-8?q?e=20#2102?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La lista del foco guarda la parte de tarjeta actual bajo el mismo bloqueo, el foco no lista durante un listado completo y una prueba fija que IdentityRoot::chosen no lista. --- .../src/identity/application/readers.rs | 28 ++-- .../application/readers/tests/chosen.rs | 141 ++++++++++++++++++ .../application/readers/tests/focus.rs | 57 +++++++ .../readers/{tests.rs => tests/mod.rs} | 139 +---------------- rfirma-app/src-tauri/src/identity/tests.rs | 69 ++++++++- .../src/signing/useCertificateListing.test.ts | 39 +++++ .../src/signing/useCertificateListing.ts | 5 + 7 files changed, 329 insertions(+), 149 deletions(-) create mode 100644 rfirma-app/src-tauri/src/identity/application/readers/tests/chosen.rs create mode 100644 rfirma-app/src-tauri/src/identity/application/readers/tests/focus.rs rename rfirma-app/src-tauri/src/identity/application/readers/{tests.rs => tests/mod.rs} (79%) diff --git a/rfirma-app/src-tauri/src/identity/application/readers.rs b/rfirma-app/src-tauri/src/identity/application/readers.rs index 89dffd10..d3b21d18 100644 --- a/rfirma-app/src-tauri/src/identity/application/readers.rs +++ b/rfirma-app/src-tauri/src/identity/application/readers.rs @@ -42,11 +42,18 @@ impl LastListing { } } - /// Los certificados del último listado que son de una tarjeta, si ya hubo alguno. - fn on_a_card(&self, installed_dir: &Path) -> Option> { - lock(&self.found) - .as_ref() - .map(|found| only_on_a_card(found, installed_dir)) + /// Guarda lo de los almacenes que no son de tarjeta junto con las tarjetas que tenga ahora, y lo devuelve. + fn replace_other_than_cards( + &self, + mut found: Vec, + installed_dir: &Path, + ) -> Vec { + let mut last = lock(&self.found); + if let Some(before) = last.as_deref() { + found.extend(only_on_a_card(before, installed_dir)); + } + *last = Some(found.clone()); + found } /// Los certificados del último listado de los almacenes que no enseñan tarjetas, si ya hubo alguno. @@ -172,7 +179,7 @@ pub struct CardListing<'a> { impl CardListing<'_> { /// Al recuperar el foco: abre solo los almacenes que no son de tarjeta y toma el resto del último listado. pub fn relisted_other_than_cards(&self) -> Result, TokenError> { - let Some(on_the_cards) = self.last.on_a_card(self.installed_dir) else { + if self.last.found().is_none() { return listed_rows( self.token, &self.stores, @@ -182,20 +189,21 @@ impl CardListing<'_> { self.memory, self.last, ); - }; + } let others: Vec = self .stores .iter() .filter(|store| store.class_under(self.installed_dir) != StoreClass::Card) .cloned() .collect(); - let mut found = if others.is_empty() { + let found = if others.is_empty() { Vec::new() } else { with_their_chains(self.token, self.token.list_across(&others)?) }; - found.extend(on_the_cards); - self.last.keep(&found); + let found = self + .last + .replace_other_than_cards(found, self.installed_dir); Ok(rows_marking_the_installed( self.token, found, diff --git a/rfirma-app/src-tauri/src/identity/application/readers/tests/chosen.rs b/rfirma-app/src-tauri/src/identity/application/readers/tests/chosen.rs new file mode 100644 index 00000000..9dc2fdf1 --- /dev/null +++ b/rfirma-app/src-tauri/src/identity/application/readers/tests/chosen.rs @@ -0,0 +1,141 @@ +use super::*; + +pub(super) fn chosen_by( + arranged: &Arranged, + last: &LastListing, + handle: &str, +) -> Result { + chosen_from_the_last_listing( + &arranged.token, + &arranged.stores, + last, + &arranged.listed, + handle, + ) +} + +fn some_handle(arranged: &Arranged) -> String { + arranged.listed_in_full()[0].id.clone() +} + +#[test] +fn after_a_full_listing_the_chosen_certificate_is_resolved_without_opening_a_store() { + let arranged = Arranged::new(); + let handle = some_handle(&arranged); + arranged.token.asked(); + + chosen_by(&arranged, &arranged.last, &handle).expect("esta en el último listado"); + + assert_eq!(arranged.token.asked(), Vec::::new()); +} + +#[test] +fn with_no_listing_yet_the_chosen_certificate_lists_once_and_the_next_does_not() { + let arranged = Arranged::new(); + let handle = some_handle(&arranged); + let nothing_listed_yet = LastListing::default(); + arranged.token.asked(); + + chosen_by(&arranged, ¬hing_listed_yet, &handle).expect("lista y lo encuentra"); + assert!(!arranged.token.asked().is_empty()); + + chosen_by(&arranged, ¬hing_listed_yet, &handle).expect("ya está guardado"); + assert_eq!(arranged.token.asked(), Vec::::new()); +} + +#[test] +fn a_card_that_came_in_through_the_readers_resolves_without_listing_and_fails_by_name_once_out() { + let (card, token) = WindowsWithACard::with_the_dnie(); + let arranged = Arranged::over(token, vec![the_windows_store()]); + let heard = followed(&arranged, &card, &[true]); + let handle = heard + .iter() + .filter_map(|news| news.certificates.as_ref()) + .flatten() + .find(|row| row.label.contains(ON_THE_DNIE)) + .expect("la tarjeta tiene fila") + .id + .clone(); + arranged.token.asked(); + + chosen_by(&arranged, &arranged.last, &handle).expect("entró por la lista en caliente"); + assert_eq!(arranged.token.asked(), Vec::::new()); + + followed(&arranged, &card, &[true, false]); + arranged.token.asked(); + let failure = chosen_by(&arranged, &arranged.last, &handle).expect_err("la tarjeta se fue"); + + assert_eq!(failure.situation(), Situation::CertificateNotFound); + assert_eq!(arranged.token.asked(), Vec::::new()); +} + +#[test] +fn a_forgotten_certificate_is_no_longer_resolved_and_nothing_is_listed() { + let arranged = Arranged::new(); + let handle = some_handle(&arranged); + let reference = arranged.listed.get(&handle).expect("asa"); + arranged.token.asked(); + + arranged.last.forget(&reference); + + let failure = chosen_by(&arranged, &arranged.last, &handle).expect_err("ya no está"); + assert_eq!(failure.situation(), Situation::CertificateNotFound); + assert_eq!(arranged.token.asked(), Vec::::new()); +} + +#[test] +fn a_card_put_in_while_the_focus_lists_is_still_resolved_afterwards() { + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::{Arc, Barrier}; + + let card_in = Arc::new(AtomicBool::new(false)); + let hold_the_nss = Arc::new(AtomicBool::new(false)); + let (inside, release) = (Arc::new(Barrier::new(2)), Arc::new(Barrier::new(2))); + let token = { + let (card_in, hold_the_nss) = (card_in.clone(), hold_the_nss.clone()); + let (inside, release) = (inside.clone(), release.clone()); + Counting::showing(move |store| { + if *store == Store::module(CARD) { + if !card_in.load(Ordering::SeqCst) { + return Vec::new(); + } + return vec![TokenCertificate::new( + CertificateRef::new(store.clone(), "token", "FIRMA", vec![0x02]), + TestAuthority::root("Firmante de la tarjeta").der(), + )]; + } else if hold_the_nss.swap(false, Ordering::SeqCst) { + inside.wait(); + release.wait(); + } + vec![a_certificate_in(store)] + }) + }; + let arranged = Arranged::over( + token, + vec![ + Store::module(CARD), + Store::nss( + SOFTOKEN, + Path::new("/casa/ada/.mozilla/firefox/ada.default"), + ), + ], + ); + arranged.listed_in_full(); + hold_the_nss.store(true, Ordering::SeqCst); + + let handle = std::thread::scope(|scope| { + let focus = scope.spawn(|| arranged.card_listing().relisted_other_than_cards()); + inside.wait(); + card_in.store(true, Ordering::SeqCst); + let (rows, _) = arranged.card_listing().relisted(); + release.wait(); + focus.join().unwrap().expect("deberia listar"); + rows.expect("la lista del lector") + .into_iter() + .find(|row| row.holder_name == "Firmante de la tarjeta") + .expect("la tarjeta tiene fila") + .id + }); + + chosen_by(&arranged, &arranged.last, &handle).expect("la tarjeta sigue en el último listado"); +} diff --git a/rfirma-app/src-tauri/src/identity/application/readers/tests/focus.rs b/rfirma-app/src-tauri/src/identity/application/readers/tests/focus.rs new file mode 100644 index 00000000..89ffae8b --- /dev/null +++ b/rfirma-app/src-tauri/src/identity/application/readers/tests/focus.rs @@ -0,0 +1,57 @@ +use super::chosen::chosen_by; +use super::*; + +#[test] +fn regaining_the_focus_opens_only_the_stores_that_are_not_cards_and_keeps_handles_and_listing() { + let arranged = Arranged::new(); + let before = arranged.listed_in_full(); + arranged.token.asked(); + + let rows = arranged + .card_listing() + .relisted_other_than_cards() + .expect("deberia listar"); + + assert_eq!( + arranged.token.asked(), + vec![Store::nss( + SOFTOKEN, + Path::new("/casa/ada/.mozilla/firefox/ada.default"), + )] + ); + let mut ids: Vec<_> = rows.iter().map(|row| row.id.clone()).collect(); + let mut kept: Vec<_> = before.iter().map(|row| row.id.clone()).collect(); + ids.sort(); + kept.sort(); + assert_eq!(ids, kept); + chosen_by(&arranged, &arranged.last, &kept[0]).expect("sigue en el último listado"); + assert_eq!(arranged.token.asked(), Vec::::new()); +} + +#[test] +fn regaining_the_focus_lists_the_windows_store_with_the_others() { + let (_, token) = WindowsWithACard::with_the_dnie(); + let arranged = Arranged::over(token, vec![the_windows_store()]); + arranged.listed_in_full(); + arranged.token.asked(); + + arranged + .card_listing() + .relisted_other_than_cards() + .expect("deberia listar"); + + assert_eq!(arranged.token.asked(), vec![the_windows_store()]); +} + +#[test] +fn regaining_the_focus_with_no_listing_yet_lists_every_store() { + let arranged = Arranged::new(); + + let rows = arranged + .card_listing() + .relisted_other_than_cards() + .expect("deberia listar"); + + assert_eq!(arranged.token.asked().len(), 2); + assert_eq!(rows.len(), 2); +} diff --git a/rfirma-app/src-tauri/src/identity/application/readers/tests.rs b/rfirma-app/src-tauri/src/identity/application/readers/tests/mod.rs similarity index 79% rename from rfirma-app/src-tauri/src/identity/application/readers/tests.rs rename to rfirma-app/src-tauri/src/identity/application/readers/tests/mod.rs index 8e7e5f7d..c29f2706 100644 --- a/rfirma-app/src-tauri/src/identity/application/readers/tests.rs +++ b/rfirma-app/src-tauri/src/identity/application/readers/tests/mod.rs @@ -209,61 +209,6 @@ fn a_full_listing_after_the_readers_relisted_keeps_the_handles_they_announced() } } -#[test] -fn regaining_the_focus_opens_only_the_stores_that_are_not_cards_and_keeps_handles_and_listing() { - let arranged = Arranged::new(); - let before = arranged.listed_in_full(); - arranged.token.asked(); - - let rows = arranged - .card_listing() - .relisted_other_than_cards() - .expect("deberia listar"); - - assert_eq!( - arranged.token.asked(), - vec![Store::nss( - SOFTOKEN, - Path::new("/casa/ada/.mozilla/firefox/ada.default"), - )] - ); - let mut ids: Vec<_> = rows.iter().map(|row| row.id.clone()).collect(); - let mut kept: Vec<_> = before.iter().map(|row| row.id.clone()).collect(); - ids.sort(); - kept.sort(); - assert_eq!(ids, kept); - chosen_by(&arranged, &arranged.last, &kept[0]).expect("sigue en el último listado"); - assert_eq!(arranged.token.asked(), Vec::::new()); -} - -#[test] -fn regaining_the_focus_lists_the_windows_store_with_the_others() { - let (_, token) = WindowsWithACard::with_the_dnie(); - let arranged = Arranged::over(token, vec![the_windows_store()]); - arranged.listed_in_full(); - arranged.token.asked(); - - arranged - .card_listing() - .relisted_other_than_cards() - .expect("deberia listar"); - - assert_eq!(arranged.token.asked(), vec![the_windows_store()]); -} - -#[test] -fn regaining_the_focus_with_no_listing_yet_lists_every_store() { - let arranged = Arranged::new(); - - let rows = arranged - .card_listing() - .relisted_other_than_cards() - .expect("deberia listar"); - - assert_eq!(arranged.token.asked().len(), 2); - assert_eq!(rows.len(), 2); -} - #[test] fn a_window_that_mounts_late_reads_the_last_status_announced() { let now = ReaderNow::default(); @@ -607,85 +552,5 @@ fn another_card_on_macos_that_no_named_module_reads_is_unreadable() { ); } -fn chosen_by( - arranged: &Arranged, - last: &LastListing, - handle: &str, -) -> Result { - chosen_from_the_last_listing( - &arranged.token, - &arranged.stores, - last, - &arranged.listed, - handle, - ) -} - -fn some_handle(arranged: &Arranged) -> String { - arranged.listed_in_full()[0].id.clone() -} - -#[test] -fn after_a_full_listing_the_chosen_certificate_is_resolved_without_opening_a_store() { - let arranged = Arranged::new(); - let handle = some_handle(&arranged); - arranged.token.asked(); - - chosen_by(&arranged, &arranged.last, &handle).expect("esta en el último listado"); - - assert_eq!(arranged.token.asked(), Vec::::new()); -} - -#[test] -fn with_no_listing_yet_the_chosen_certificate_lists_once_and_the_next_does_not() { - let arranged = Arranged::new(); - let handle = some_handle(&arranged); - let nothing_listed_yet = LastListing::default(); - arranged.token.asked(); - - chosen_by(&arranged, ¬hing_listed_yet, &handle).expect("lista y lo encuentra"); - assert!(!arranged.token.asked().is_empty()); - - chosen_by(&arranged, ¬hing_listed_yet, &handle).expect("ya está guardado"); - assert_eq!(arranged.token.asked(), Vec::::new()); -} - -#[test] -fn a_card_that_came_in_through_the_readers_resolves_without_listing_and_fails_by_name_once_out() { - let (card, token) = WindowsWithACard::with_the_dnie(); - let arranged = Arranged::over(token, vec![the_windows_store()]); - let heard = followed(&arranged, &card, &[true]); - let handle = heard - .iter() - .filter_map(|news| news.certificates.as_ref()) - .flatten() - .find(|row| row.label.contains(ON_THE_DNIE)) - .expect("la tarjeta tiene fila") - .id - .clone(); - arranged.token.asked(); - - chosen_by(&arranged, &arranged.last, &handle).expect("entró por la lista en caliente"); - assert_eq!(arranged.token.asked(), Vec::::new()); - - followed(&arranged, &card, &[true, false]); - arranged.token.asked(); - let failure = chosen_by(&arranged, &arranged.last, &handle).expect_err("la tarjeta se fue"); - - assert_eq!(failure.situation(), Situation::CertificateNotFound); - assert_eq!(arranged.token.asked(), Vec::::new()); -} - -#[test] -fn a_forgotten_certificate_is_no_longer_resolved_and_nothing_is_listed() { - let arranged = Arranged::new(); - let handle = some_handle(&arranged); - let reference = arranged.listed.get(&handle).expect("asa"); - arranged.token.asked(); - - arranged.last.forget(&reference); - - let failure = chosen_by(&arranged, &arranged.last, &handle).expect_err("ya no está"); - assert_eq!(failure.situation(), Situation::CertificateNotFound); - assert_eq!(arranged.token.asked(), Vec::::new()); -} +mod chosen; +mod focus; diff --git a/rfirma-app/src-tauri/src/identity/tests.rs b/rfirma-app/src-tauri/src/identity/tests.rs index b41e1275..916b641e 100644 --- a/rfirma-app/src-tauri/src/identity/tests.rs +++ b/rfirma-app/src-tauri/src/identity/tests.rs @@ -8,10 +8,12 @@ use crate::identity::domain::keyring::KeyringError; use crate::identity::domain::secret::StoreSecret; use crate::signing::ports::Signer; -/// Un token que solo recuerda con qué secreto se le pidió firmar o aceptar, y responde `reply` a `secret_of`. +/// Un token que enseña `shows`, cuenta los listados, recuerda con qué secreto se le pidió firmar o aceptar y responde `reply` a `secret_of`. struct RecordingToken { reply: StoreSecret, received_secret: Mutex>>, + shows: Vec, + lists: Arc, } impl RecordingToken { @@ -19,6 +21,15 @@ impl RecordingToken { Self { reply, received_secret: Mutex::new(None), + shows: Vec::new(), + lists: Arc::new(AtomicUsize::new(0)), + } + } + + fn showing(shows: Vec) -> Self { + Self { + shows, + ..Self::replying(StoreSecret::TypedOnScreen) } } @@ -29,7 +40,8 @@ impl RecordingToken { impl Token for RecordingToken { fn list(&self, _store: &Store) -> Result, TokenError> { - Ok(Vec::new()) + self.lists.fetch_add(1, Ordering::SeqCst); + Ok(self.shows.clone()) } fn every_certificate(&self, _store: &Store) -> Result, TokenError> { @@ -316,3 +328,56 @@ fn accepting_the_secret_of_an_installed_certificate_also_uses_the_keyring_pin() assert_eq!(token.received_secret(), Some(b"pin-del-llavero".to_vec())); assert_eq!(calls.load(Ordering::SeqCst), 1); } + +/// Un diálogo que ninguna de estas pruebas debería abrir. +struct UnreachablePrompter; + +impl SecretPrompter for UnreachablePrompter { + fn prompt_secret( + &self, + _request: &ports::SecretPromptRequest, + ) -> Result { + panic!("esta prueba no deberia pedir ningun secreto") + } +} + +#[test] +fn after_a_full_listing_the_chosen_certificate_opens_no_store() { + let installed = tempfile::tempdir().expect("directorio temporal"); + let token = RecordingToken::showing(vec![crate::identity::application::tests::a_certificate( + "FIRMA", + &crate::identity::application::tests::TestAuthority::root("Firmante de pruebas").der(), + )]); + let lists = token.lists.clone(); + let (keyring, _) = CountingKeyring::with_pin("pin"); + let identity = IdentityRoot { + token: Box::new(token), + stores: vec![Store::module("/usr/lib/softhsm/libsofthsm2.so")], + installed_certificates: installed.path().to_path_buf(), + listed: ListedCertificates::new(), + installed_copies: ListedCertificates::new(), + last_listing: LastListing::default(), + reader_now: ReaderNow::default(), + memory: Arc::new(crate::identity::application::tests::NoMemory), + folder: Arc::new(adapters::folder::RealInstalledFolder), + prompter: Arc::new(UnreachablePrompter), + keyring: factory_of(keyring), + }; + let handle = application::certificates::listed_rows( + identity.token.as_ref(), + &identity.all_stores(), + identity.installed_certificates(), + &identity.listed, + &identity.installed_copies, + identity.memory.as_ref(), + &identity.last_listing, + ) + .expect("deberia listar")[0] + .id + .clone(); + lists.store(0, Ordering::SeqCst); + + identity.chosen(&handle).expect("esta en el ultimo listado"); + + assert_eq!(lists.load(Ordering::SeqCst), 0); +} diff --git a/rfirma-app/src/signing/useCertificateListing.test.ts b/rfirma-app/src/signing/useCertificateListing.test.ts index 37b3e367..ffc685de 100644 --- a/rfirma-app/src/signing/useCertificateListing.test.ts +++ b/rfirma-app/src/signing/useCertificateListing.test.ts @@ -155,6 +155,45 @@ describe("useCertificateListing · el foco", () => { expect(result.current.listing).toEqual({ kind: "listed", certificates: [onToken] }); }); + + it("leaves the first search alone when the window regains focus before it answers", async () => { + let answer: (found: Certificate[]) => void = () => {}; + const store = aStore([], { + list: () => new Promise((resolve) => (answer = resolve)), + listOtherThanCards: vi.fn(async () => Promise.reject(new Error("sin llavero"))), + }); + const { result } = renderHook(() => useCertificateListing(store)); + + await act(async () => { + window.dispatchEvent(new Event("focus")); + }); + await act(async () => answer([onToken])); + + expect(result.current.listing).toEqual({ kind: "listed", certificates: [onToken] }); + expect(store.listOtherThanCards).not.toHaveBeenCalled(); + }); + + it("keeps what looking again brings when the window regains focus meanwhile", async () => { + let answer: (found: Certificate[]) => void = () => {}; + const store = aStore([onToken], { listOtherThanCards: vi.fn(async () => [onToken]) }); + const { result } = await listed(store); + store.list = () => new Promise((resolve) => (answer = resolve)); + + let looking = Promise.resolve(); + act(() => { + looking = result.current.lookAgain(); + }); + await act(async () => { + window.dispatchEvent(new Event("focus")); + }); + await act(async () => { + answer([onToken, dnie]); + await looking; + }); + + expect(result.current.listing).toEqual({ kind: "listed", certificates: [onToken, dnie] }); + expect(store.listOtherThanCards).not.toHaveBeenCalled(); + }); }); describe("useCertificateListing · los lectores", () => { diff --git a/rfirma-app/src/signing/useCertificateListing.ts b/rfirma-app/src/signing/useCertificateListing.ts index b1a3aef1..950eb897 100644 --- a/rfirma-app/src/signing/useCertificateListing.ts +++ b/rfirma-app/src/signing/useCertificateListing.ts @@ -22,15 +22,19 @@ export function useCertificateListing(store: CertificateStore) { const [listing, setListing] = useState({ kind: "loading" }); const [reader, setReader] = useState(UNAVAILABLE_READER); const latestListing = useRef(0); + const fullListingsUnderWay = useRef(0); const lookAgain = useCallback(async () => { const mine = ++latestListing.current; + fullListingsUnderWay.current++; setListing({ kind: "loading" }); try { const certificates = await store.list(); if (mine === latestListing.current) setListing({ kind: "listed", certificates }); } catch (thrown) { if (mine === latestListing.current) setListing({ kind: "failed", failure: classify(thrown) }); + } finally { + fullListingsUnderWay.current--; } }, [store]); @@ -51,6 +55,7 @@ export function useCertificateListing(store: CertificateStore) { useEffect(() => { const onFocus = () => { + if (fullListingsUnderWay.current > 0) return; const mine = ++latestListing.current; store.listOtherThanCards().then( (certificates) => { From 32cfe5c95cc7c24133293466379ee1eccf486b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20G=C3=B3mez?= Date: Thu, 22 Oct 2026 09:02:26 +0200 Subject: [PATCH 4/7] =?UTF-8?q?fix(pr):=20remove=5Finstalled=20recibe=20el?= =?UTF-8?q?=20=C3=BAltimo=20listado=20en=20un=20RemovalListing,=20para=20n?= =?UTF-8?q?o=20pasar=20de=20siete=20argumentos=20en=20clippy=20(#2102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src-tauri/src/identity/adapters/tauri.rs | 8 ++- .../src/identity/application/certificates.rs | 23 +++++-- .../application/certificates/tests/copies.rs | 9 ++- .../application/certificates/tests/removal.rs | 60 ++++++++++++------- .../certificates/tests/system_install.rs | 20 ++++--- 5 files changed, 79 insertions(+), 41 deletions(-) diff --git a/rfirma-app/src-tauri/src/identity/adapters/tauri.rs b/rfirma-app/src-tauri/src/identity/adapters/tauri.rs index dca6c0d1..78f4b3cc 100644 --- a/rfirma-app/src-tauri/src/identity/adapters/tauri.rs +++ b/rfirma-app/src-tauri/src/identity/adapters/tauri.rs @@ -129,9 +129,11 @@ pub fn remove_certificate(id: String, identity: State<'_, IdentityRoot>) -> Resu identity.memory.as_ref(), identity.installed_certificates(), &id, - &identity.listed, - &identity.installed_copies, - &identity.last_listing, + crate::identity::application::certificates::RemovalListing { + listed: &identity.listed, + installed_copies: &identity.installed_copies, + last: &identity.last_listing, + }, )?, ) } diff --git a/rfirma-app/src-tauri/src/identity/application/certificates.rs b/rfirma-app/src-tauri/src/identity/application/certificates.rs index 8227b289..288a8931 100644 --- a/rfirma-app/src-tauri/src/identity/application/certificates.rs +++ b/rfirma-app/src-tauri/src/identity/application/certificates.rs @@ -412,6 +412,16 @@ fn only_supported_keys( Ok(()) } +/// El último listado donde se busca el asa que se quita y del que se borra. +pub struct RemovalListing<'a> { + /// Las filas del último listado. + pub listed: &'a ListedCertificates, + /// La copia instalada de cada fila que tenga una. + pub installed_copies: &'a ListedCertificates, + /// El último listado completo. + pub last: &'a LastListing, +} + /// Quita un certificado del Almacén de rFirma: lo borra, con su clave, de la base única (ADR-0034). pub fn remove_installed( token: &dyn Token, @@ -419,13 +429,14 @@ pub fn remove_installed( memory: &dyn CertificateMemory, installed_dir: &Path, handle: &str, - listed: &ListedCertificates, - installed_copies: &ListedCertificates, - last: &LastListing, + listing: RemovalListing<'_>, ) -> Result<(), InstallError> { - let reference = match installed_copies.get(handle) { + let reference = match listing.installed_copies.get(handle) { Some(installed) => installed, - None => listed.get(handle).ok_or_else(not_from_the_last_listing)?, + None => listing + .listed + .get(handle) + .ok_or_else(not_from_the_last_listing)?, }; if !installed_by_rfirma(token, installed_dir, &reference) { return Err(TokenError::new( @@ -440,7 +451,7 @@ pub fn remove_installed( keyring.pin()? }; token.remove_certificate(installed_dir, &reference, &pin)?; - last.forget(&reference); + listing.last.forget(&reference); if memory.remembered_certificate().as_ref() == Some(&reference) { let _ = memory.forget_the_certificate(); } diff --git a/rfirma-app/src-tauri/src/identity/application/certificates/tests/copies.rs b/rfirma-app/src-tauri/src/identity/application/certificates/tests/copies.rs index 85c137aa..e3e93e0d 100644 --- a/rfirma-app/src-tauri/src/identity/application/certificates/tests/copies.rs +++ b/rfirma-app/src-tauri/src/identity/application/certificates/tests/copies.rs @@ -2,6 +2,7 @@ use std::path::Path; use super::super::{ certificate_behind, remember_the_certificate, remove_installed, rows_of, ListedCertificates, + RemovalListing, }; use super::removal::{FixedPinKeyring, RemovalOutcome}; use super::{CARD, INSTALLED, SOFTOKEN}; @@ -336,9 +337,11 @@ fn a_row_signed_with_a_remembered_browser_copy_still_removes_its_installed_copy( &memory, &installed_dir, &rows[0].id, - &listed, - &installed_copies, - &crate::identity::application::readers::LastListing::default(), + RemovalListing { + listed: &listed, + installed_copies: &installed_copies, + last: &crate::identity::application::readers::LastListing::default(), + }, ) .expect("la copia instalada deberia poder quitarse aunque la elegida sea de firefox"); } diff --git a/rfirma-app/src-tauri/src/identity/application/certificates/tests/removal.rs b/rfirma-app/src-tauri/src/identity/application/certificates/tests/removal.rs index a99b8032..7492c1cf 100644 --- a/rfirma-app/src-tauri/src/identity/application/certificates/tests/removal.rs +++ b/rfirma-app/src-tauri/src/identity/application/certificates/tests/removal.rs @@ -1,6 +1,8 @@ use std::path::Path; -use super::super::{remember_the_certificate, remove_installed, InstallError, ListedCertificates}; +use super::super::{ + remember_the_certificate, remove_installed, InstallError, ListedCertificates, RemovalListing, +}; use crate::identity::application::tests::a_certificate; use crate::identity::domain::algorithm::SignatureAlgorithm; use crate::identity::domain::certificate::{CertificateRef, TokenCertificate}; @@ -132,9 +134,11 @@ fn removing_refuses_a_handle_that_is_not_from_the_last_listing() { &a_memory(home.path()), &home.path().join("certificates"), "00000000000000000000000000000000", - &listed, - &ListedCertificates::new(), - &crate::identity::application::readers::LastListing::default(), + RemovalListing { + listed: &listed, + installed_copies: &ListedCertificates::new(), + last: &crate::identity::application::readers::LastListing::default(), + }, ) .expect_err("no es de la ultima busqueda"); @@ -157,9 +161,11 @@ fn removing_refuses_a_certificate_from_somewhere_else() { &a_memory(home.path()), &home.path().join("certificates"), &handle, - &listed, - &ListedCertificates::new(), - &crate::identity::application::readers::LastListing::default(), + RemovalListing { + listed: &listed, + installed_copies: &ListedCertificates::new(), + last: &crate::identity::application::readers::LastListing::default(), + }, ) .expect_err("no viene de un .p12 instalado"); @@ -183,9 +189,11 @@ fn removing_asks_the_keyring_for_the_pin_and_propagates_its_refusal() { &a_memory(home.path()), &installed, &handle, - &listed, - &ListedCertificates::new(), - &crate::identity::application::readers::LastListing::default(), + RemovalListing { + listed: &listed, + installed_copies: &ListedCertificates::new(), + last: &crate::identity::application::readers::LastListing::default(), + }, ) .expect_err("sin PIN todavia no hay nada que quitar"); @@ -210,9 +218,11 @@ fn removing_propagates_the_token_refusal() { &a_memory(home.path()), &installed, &handle, - &listed, - &ListedCertificates::new(), - &crate::identity::application::readers::LastListing::default(), + RemovalListing { + listed: &listed, + installed_copies: &ListedCertificates::new(), + last: &crate::identity::application::readers::LastListing::default(), + }, ) .expect_err("el token se ha negado"); @@ -238,9 +248,11 @@ fn removing_the_remembered_certificate_forgets_it() { &memory, &installed, &handle, - &listed, - &ListedCertificates::new(), - &crate::identity::application::readers::LastListing::default(), + RemovalListing { + listed: &listed, + installed_copies: &ListedCertificates::new(), + last: &crate::identity::application::readers::LastListing::default(), + }, ) .expect("deberia quitarse"); @@ -268,9 +280,11 @@ fn removing_a_different_certificate_leaves_the_remembered_one_alone() { &memory, &installed, &handle, - &listed, - &ListedCertificates::new(), - &crate::identity::application::readers::LastListing::default(), + RemovalListing { + listed: &listed, + installed_copies: &ListedCertificates::new(), + last: &crate::identity::application::readers::LastListing::default(), + }, ) .expect("deberia quitarse"); @@ -293,9 +307,11 @@ fn a_removed_p12_leaves_the_last_listing_at_once() { &a_memory(home.path()), &installed, &handle, - &listed, - &ListedCertificates::new(), - &last, + RemovalListing { + listed: &listed, + installed_copies: &ListedCertificates::new(), + last: &last, + }, ) .expect("deberia quitarse"); diff --git a/rfirma-app/src-tauri/src/identity/application/certificates/tests/system_install.rs b/rfirma-app/src-tauri/src/identity/application/certificates/tests/system_install.rs index e14d79d3..0610a3e2 100644 --- a/rfirma-app/src-tauri/src/identity/application/certificates/tests/system_install.rs +++ b/rfirma-app/src-tauri/src/identity/application/certificates/tests/system_install.rs @@ -1,7 +1,9 @@ use std::cell::RefCell; use std::path::{Path, PathBuf}; -use super::super::{install_pkcs12, remove_installed, InstallError, ListedCertificates}; +use super::super::{ + install_pkcs12, remove_installed, InstallError, ListedCertificates, RemovalListing, +}; use crate::identity::application::tests::NoToken; use crate::identity::domain::algorithm::SignatureAlgorithm; use crate::identity::domain::certificate::{CertificateRef, TokenCertificate}; @@ -219,9 +221,11 @@ fn removing_a_macos_store_certificate_asks_the_system_and_forgets_it() { &memory, &home.path().join("certificates"), &handle, - &listed, - &ListedCertificates::new(), - &crate::identity::application::readers::LastListing::default(), + RemovalListing { + listed: &listed, + installed_copies: &ListedCertificates::new(), + last: &crate::identity::application::readers::LastListing::default(), + }, ); assert_eq!(removed, Ok(())); @@ -242,9 +246,11 @@ fn a_macos_store_certificate_rfirma_did_not_install_is_not_removed() { &a_memory(home.path()), &home.path().join("certificates"), &handle, - &listed, - &ListedCertificates::new(), - &crate::identity::application::readers::LastListing::default(), + RemovalListing { + listed: &listed, + installed_copies: &ListedCertificates::new(), + last: &crate::identity::application::readers::LastListing::default(), + }, ) .expect_err("no lo instalo rFirma"); From d32be936bc5b8ec61b63e9363e495192359b0d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20G=C3=B3mez?= Date: Thu, 22 Oct 2026 09:09:49 +0200 Subject: [PATCH 5/7] =?UTF-8?q?fix(pr):=20la=20prueba=20de=20grada=20B=20d?= =?UTF-8?q?el=20borrado=20pasa=20el=20=C3=BAltimo=20listado=20como=20Remov?= =?UTF-8?q?alListing=20(#2102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rfirma-app/src-tauri/tests/installed_p12_removal.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rfirma-app/src-tauri/tests/installed_p12_removal.rs b/rfirma-app/src-tauri/tests/installed_p12_removal.rs index 3b06b403..a97e1d0d 100644 --- a/rfirma-app/src-tauri/tests/installed_p12_removal.rs +++ b/rfirma-app/src-tauri/tests/installed_p12_removal.rs @@ -146,9 +146,11 @@ fn remove( &NoMemory, installed, handle, - listed, - &ListedCertificates::new(), - &rfirma_lib::identity::application::readers::LastListing::default(), + certificates::RemovalListing { + listed, + installed_copies: &ListedCertificates::new(), + last: &rfirma_lib::identity::application::readers::LastListing::default(), + }, )?) } From 19e6cd7f94a50419c4214257d441736018ec018f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20G=C3=B3mez?= Date: Thu, 22 Oct 2026 09:26:51 +0200 Subject: [PATCH 6/7] =?UTF-8?q?fix(pr):=20la=20lista=20cerrada=20de=20?= =?UTF-8?q?=C3=B3rdenes=20cuenta=20list=5Fcertificates=5Fother=5Fthan=5Fca?= =?UTF-8?q?rds=20en=20#2102?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rfirma-app/src-tauri/src/crossing/guards/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfirma-app/src-tauri/src/crossing/guards/tests.rs b/rfirma-app/src-tauri/src/crossing/guards/tests.rs index 3f576938..dd53d7dd 100644 --- a/rfirma-app/src-tauri/src/crossing/guards/tests.rs +++ b/rfirma-app/src-tauri/src/crossing/guards/tests.rs @@ -212,7 +212,7 @@ fn the_list_of_commands_is_closed_and_this_is_how_long_it_is() { .map(|(_, source)| production_half(source).matches("#[tauri::command").count()) .sum(); - assert_eq!(orders, 66, "la lista de ordenes es cerrada a proposito"); + assert_eq!(orders, 67, "la lista de ordenes es cerrada a proposito"); } #[test] From 2dc5d5a731f4eb00a0cd36eff50398837343ac13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20G=C3=B3mez?= Date: Thu, 22 Oct 2026 09:46:09 +0200 Subject: [PATCH 7/7] =?UTF-8?q?fix(pr):=20la=20instant=C3=A1nea=20del=20co?= =?UTF-8?q?ntrato=20incluye=20list=5Fcertificates=5Fother=5Fthan=5Fcards?= =?UTF-8?q?=20en=20#2102?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rfirma-app/src-tauri/tests/contract.snapshot | 1 + 1 file changed, 1 insertion(+) diff --git a/rfirma-app/src-tauri/tests/contract.snapshot b/rfirma-app/src-tauri/tests/contract.snapshot index 4e473f1b..dd34a4b7 100644 --- a/rfirma-app/src-tauri/tests/contract.snapshot +++ b/rfirma-app/src-tauri/tests/contract.snapshot @@ -31,6 +31,7 @@ ORDENES DE TAURI (/adapters/tauri*.rs) async open_opened_folder(document: String) -> Result<(), Failure> async open_signed_folder() -> Result<(), Failure> list_certificates() -> Result, Failure> + async list_certificates_other_than_cards() -> Result, Failure> read_card_readers() -> ReaderStatusView async install_certificate() -> Result async remove_certificate(id: String) -> Result<(), Failure>