diff --git a/cli/src/commands/send.rs b/cli/src/commands/send.rs index 7ad6095e..83654439 100644 --- a/cli/src/commands/send.rs +++ b/cli/src/commands/send.rs @@ -1,5 +1,6 @@ use std::process::ExitCode; +use anyhow::Context; use clap::Args; use xhandler::prelude::*; @@ -85,7 +86,8 @@ pub enum SendResult { impl SendArgs { pub async fn run(&self) -> anyhow::Result { - let xml_content = std::fs::read_to_string(&self.input_file)?; + let xml_content = std::fs::read_to_string(&self.input_file) + .with_context(|| format!("no se puede leer el archivo XML: {}", self.input_file))?; let default_cdr = self .output_file .clone() @@ -145,7 +147,10 @@ impl SendArgs { file_content: xml_content.to_string(), }; - let result = sender.send_file(&ubl_file).await?; + let result = sender + .send_file(&ubl_file) + .await + .map_err(|e| anyhow::anyhow!("error al enviar documento a SUNAT: {e}"))?; match result.response { SendFileAggregatedResponse::Cdr(cdr_base64, metadata) => { diff --git a/cli/src/commands/sign.rs b/cli/src/commands/sign.rs index 6b6d4907..bbcd493a 100644 --- a/cli/src/commands/sign.rs +++ b/cli/src/commands/sign.rs @@ -1,6 +1,7 @@ use std::io::Read; use std::process::ExitCode; +use anyhow::Context; use clap::Args; use xhandler::prelude::*; @@ -41,7 +42,8 @@ impl SignArgs { std::io::stdin().read_to_string(&mut buf)?; buf } else { - std::fs::read_to_string(&self.input_file)? + std::fs::read_to_string(&self.input_file) + .with_context(|| format!("no se puede leer el archivo XML: {}", self.input_file))? }; let key_pair = self.resolve_key_pair()?; @@ -60,7 +62,8 @@ impl SignArgs { pub fn resolve_key_pair(&self) -> anyhow::Result { let private_key_pem = match &self.private_key { - Some(path) => std::fs::read_to_string(path)?, + Some(path) => std::fs::read_to_string(path) + .with_context(|| format!("no se puede leer la llave privada: {path}"))?, None if self.beta => BETA_PRIVATE_KEY.to_string(), None => { anyhow::bail!( @@ -69,7 +72,8 @@ impl SignArgs { } }; let certificate_pem = match &self.certificate { - Some(path) => std::fs::read_to_string(path)?, + Some(path) => std::fs::read_to_string(path) + .with_context(|| format!("no se puede leer el certificado: {path}"))?, None if self.beta => BETA_CERTIFICATE.to_string(), None => { anyhow::bail!( @@ -77,10 +81,13 @@ impl SignArgs { ) } }; - Ok(RsaKeyPair::from_pkcs1_pem_and_certificate( - &private_key_pem, - &certificate_pem, - )?) + RsaKeyPair::from_pkcs1_pem_and_certificate(&private_key_pem, &certificate_pem).map_err( + |e| { + anyhow::anyhow!( + "error al leer certificados: {e}\n Verificar que --private-key sea PKCS#1 PEM y --certificate sea X.509 PEM" + ) + }, + ) } pub fn sign_xml(&self, xml_content: &str, key_pair: &RsaKeyPair) -> anyhow::Result> { diff --git a/cli/src/input.rs b/cli/src/input.rs index bba1996e..22ad9854 100644 --- a/cli/src/input.rs +++ b/cli/src/input.rs @@ -1,6 +1,7 @@ use std::io::Read; use std::path::Path; +use anyhow::Context; use serde::Deserialize; use xhandler::prelude::*; @@ -42,8 +43,16 @@ impl InputFormat { pub fn parse(&self, content: &str) -> anyhow::Result { match self { - InputFormat::Json => Ok(serde_json::from_str(content)?), - InputFormat::Yaml => Ok(serde_yaml::from_str(content)?), + InputFormat::Json => serde_json::from_str(content).map_err(|e| { + anyhow::anyhow!( + "Error en el archivo JSON:\n {e}\n Valores validos de 'kind': Invoice, CreditNote, DebitNote, DespatchAdvice, Perception, Retention, SummaryDocuments, VoidedDocuments" + ) + }), + InputFormat::Yaml => serde_yaml::from_str(content).map_err(|e| { + anyhow::anyhow!( + "Error en el archivo YAML:\n {e}\n Valores validos de 'kind': Invoice, CreditNote, DebitNote, DespatchAdvice, Perception, Retention, SummaryDocuments, VoidedDocuments" + ) + }), } } } @@ -61,7 +70,8 @@ pub fn read_input(file: &str, format: Option<&str>) -> anyhow::Result InputFormat::Json, Some("yaml") => InputFormat::Yaml, diff --git a/cli/tests/input_parsing.rs b/cli/tests/input_parsing.rs index 5f71da66..1778732a 100644 --- a/cli/tests/input_parsing.rs +++ b/cli/tests/input_parsing.rs @@ -1,4 +1,4 @@ -use openubl_cli::input::{read_input, DocumentInput}; +use openubl_cli::input::{read_input, DocumentInput, InputFormat}; const RESOURCES: &str = "tests/resources"; @@ -158,6 +158,111 @@ fn parse_invalid_kind_fails() { let result = read_input(temp_file.to_str().unwrap(), None); assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("Valores validos de 'kind'"), + "expected valid kinds hint but got: {err}" + ); let _ = std::fs::remove_file(&temp_file); } + +#[test] +fn parse_wrong_field_type_shows_friendly_error() { + let yaml = "kind: Invoice\nspec:\n serie_numero: 123\n"; + let result = InputFormat::Yaml.parse(yaml); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("Error en el archivo YAML"), + "expected YAML error context but got: {err}" + ); +} + +#[test] +fn parse_missing_required_field_shows_friendly_error() { + let yaml = "kind: Invoice\nspec:\n serie_numero: F001-1\n"; + let result = InputFormat::Yaml.parse(yaml); + // This may succeed with defaults or fail depending on required fields. + // If it fails, it should have a friendly message. + if let Err(e) = result { + let err = e.to_string(); + assert!( + err.contains("Error en el archivo YAML"), + "expected YAML error context but got: {err}" + ); + } +} + +#[test] +fn parse_empty_content_shows_friendly_error() { + let result = InputFormat::Yaml.parse(""); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("Error en el archivo YAML"), + "expected YAML error context but got: {err}" + ); +} + +#[test] +fn parse_malformed_yaml_shows_friendly_error() { + let yaml = "kind: Invoice\nspec:\n - bad:\n indentation error"; + let result = InputFormat::Yaml.parse(yaml); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("Error en el archivo YAML"), + "expected YAML error context but got: {err}" + ); +} + +#[test] +fn parse_malformed_json_shows_friendly_error() { + let json = "{not valid json"; + let result = InputFormat::Json.parse(json); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("Error en el archivo JSON"), + "expected JSON error context but got: {err}" + ); +} + +#[test] +fn file_not_found_shows_friendly_error() { + let result = read_input("nonexistent_file.yaml", None); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("no se puede leer el archivo"), + "expected file read context but got: {err}" + ); +} + +#[test] +fn parse_invalid_date_shows_format_hint() { + let yaml = r#"kind: Invoice +spec: + serie_numero: F001-1 + fecha_emision: "not-a-date" + proveedor: + ruc: "12345678912" + razon_social: "Test S.A.C." + cliente: + tipo_documento_identidad: "6" + numero_documento_identidad: "12121212121" + nombre: "Test" + detalles: + - descripcion: Item1 + cantidad: 1 + precio: 100 +"#; + let result = InputFormat::Yaml.parse(yaml); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("fecha invalida") || err.contains("DD-MM-YYYY"), + "expected date format hint but got: {err}" + ); +} diff --git a/xbuilder/src/serde_date.rs b/xbuilder/src/serde_date.rs index 75ad1875..4dda6297 100644 --- a/xbuilder/src/serde_date.rs +++ b/xbuilder/src/serde_date.rs @@ -8,7 +8,11 @@ pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result NaiveDate::parse_from_str(&s, DD_MM_YYYY) .or_else(|_| NaiveDate::parse_from_str(&s, ISO_8601)) .map(Some) - .map_err(serde::de::Error::custom), + .map_err(|_| { + serde::de::Error::custom(format!( + "fecha invalida: '{s}' (usar formato DD-MM-YYYY o YYYY-MM-DD)" + )) + }), } } }