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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions cli/src/commands/send.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::process::ExitCode;

use anyhow::Context;
use clap::Args;

use xhandler::prelude::*;
Expand Down Expand Up @@ -85,7 +86,8 @@ pub enum SendResult {

impl SendArgs {
pub async fn run(&self) -> anyhow::Result<ExitCode> {
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()
Expand Down Expand Up @@ -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) => {
Expand Down
21 changes: 14 additions & 7 deletions cli/src/commands/sign.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::io::Read;
use std::process::ExitCode;

use anyhow::Context;
use clap::Args;

use xhandler::prelude::*;
Expand Down Expand Up @@ -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()?;
Expand All @@ -60,7 +62,8 @@ impl SignArgs {

pub fn resolve_key_pair(&self) -> anyhow::Result<RsaKeyPair> {
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!(
Expand All @@ -69,18 +72,22 @@ 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!(
"se requiere --certificate (o usar --beta para certificados de prueba)"
)
}
};
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<Vec<u8>> {
Expand Down
16 changes: 13 additions & 3 deletions cli/src/input.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::io::Read;
use std::path::Path;

use anyhow::Context;
use serde::Deserialize;

use xhandler::prelude::*;
Expand Down Expand Up @@ -42,8 +43,16 @@ impl InputFormat {

pub fn parse(&self, content: &str) -> anyhow::Result<DocumentInput> {
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"
)
}),
}
}
}
Expand All @@ -61,7 +70,8 @@ pub fn read_input(file: &str, format: Option<&str>) -> anyhow::Result<DocumentIn
(buf, fmt)
} else {
let path = Path::new(file);
let content = std::fs::read_to_string(path)?;
let content = std::fs::read_to_string(path)
.with_context(|| format!("no se puede leer el archivo: {}", path.display()))?;
let fmt = match format {
Some("json") => InputFormat::Json,
Some("yaml") => InputFormat::Yaml,
Expand Down
107 changes: 106 additions & 1 deletion cli/tests/input_parsing.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use openubl_cli::input::{read_input, DocumentInput};
use openubl_cli::input::{read_input, DocumentInput, InputFormat};

const RESOURCES: &str = "tests/resources";

Expand Down Expand Up @@ -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}"
);
}
12 changes: 10 additions & 2 deletions xbuilder/src/serde_date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<NaiveDate, D::Erro
let s = String::deserialize(d)?;
NaiveDate::parse_from_str(&s, DD_MM_YYYY)
.or_else(|_| NaiveDate::parse_from_str(&s, ISO_8601))
.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)"
))
})
}

pub mod option {
Expand All @@ -24,7 +28,11 @@ pub mod option {
Some(s) => 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)"
))
}),
}
}
}
Expand Down