#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use tokio::sync::OnceCell;
use umbral::orm::write::WriteError;
use umbral::orm::{DynError, DynQuerySet};
use umbral_core::db;
#[derive(Debug, Clone, sqlx::FromRow, Serialize, Deserialize, umbral::orm::Model)]
#[umbral(table = "dyn_err_widget")]
pub struct Widget {
pub id: i64,
#[umbral(string)]
pub name: String,
pub stock: i64,
}
static BOOT: OnceCell<()> = OnceCell::const_new();
async fn boot() {
BOOT.get_or_init(|| async {
let settings = umbral::Settings::from_env().expect("figment defaults");
let pool = db::connect_sqlite("sqlite::memory:")
.await
.expect("in-memory sqlite");
umbral::App::builder()
.settings(settings)
.database("default", pool.clone())
.model::<Widget>()
.build()
.expect("App::build");
sqlx::query(
"CREATE TABLE dyn_err_widget (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
stock INTEGER NOT NULL
)",
)
.execute(&pool)
.await
.expect("CREATE TABLE");
})
.await;
}
fn meta() -> umbral::migrate::ModelMeta {
umbral::migrate::registered_models()
.into_iter()
.find(|m| m.table == "dyn_err_widget")
.expect("registered")
}
#[tokio::test]
async fn form_coercion_failure_surfaces_as_dyn_error_write_with_field_name() {
boot().await;
let mut form = std::collections::HashMap::new();
form.insert("name".to_string(), "widget-a".to_string());
form.insert("stock".to_string(), "not a number".to_string());
let err = DynQuerySet::for_meta(&meta())
.insert_form(&form, &[])
.await
.expect_err("non-numeric stock should fail coercion");
match err {
DynError::Write(WriteError::Validator { field, message }) => {
assert_eq!(field, "stock", "field must point at the offending column");
assert!(
!message.is_empty(),
"validator message must carry a hint of the parse failure"
);
}
DynError::Write(other) => {
panic!("expected WriteError::Validator on form coercion failure, got {other:?}")
}
DynError::Sqlx(e) => panic!(
"form coercion failure must NOT flatten to sqlx::Error \
(gaps2 #12 regression). got: {e:?}"
),
}
}
#[tokio::test]
async fn update_form_coercion_failure_also_surfaces_as_dyn_error_write() {
boot().await;
let mut bad = std::collections::HashMap::new();
bad.insert("stock".to_string(), "still not a number".to_string());
let err = DynQuerySet::for_meta(&meta())
.filter_eq_string("id", "999")
.update_form(&bad, &[])
.await
.expect_err("non-numeric stock should fail coercion on update");
match err {
DynError::Write(WriteError::Validator { field, .. }) => {
assert_eq!(field, "stock");
}
other => panic!("expected DynError::Write(Validator), got: {other:?}"),
}
}
#[tokio::test]
async fn dyn_error_lifts_via_from_for_both_arms() {
let sqlx_err: sqlx::Error = sqlx::Error::Protocol("synthetic".to_string());
let lifted: DynError = sqlx_err.into();
assert!(matches!(lifted, DynError::Sqlx(_)));
let write_err = WriteError::Validator {
field: "foo".to_string(),
message: "synthetic".to_string(),
};
let lifted: DynError = write_err.into();
assert!(matches!(lifted, DynError::Write(_)));
}