#[path = "async_rule_fixture.rs"]
mod fixture;
use ferro_rs::{required, string};
use ferro_rs::{rules, unique, AsyncValidationError, AsyncValidator};
use fixture::{init_test_db, seed_widget};
use serde_json::json;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn duplicate_value_is_validation_error() {
init_test_db().await;
seed_widget(1, "taken").await;
let data = json!({"slug": "taken"});
let result = AsyncValidator::new(&data)
.rules("slug", rules![required(), string()])
.async_rule("slug", unique("widgets", "slug"))
.validate_async()
.await;
match result {
Err(AsyncValidationError::Validation(ref e)) => {
assert!(
e.has("slug"),
"expected a field error for 'slug', got: {e:?}"
);
}
Err(AsyncValidationError::Infra(ref fe)) => {
panic!("expected Validation error, got Infra: {fe}");
}
Ok(()) => panic!("expected Err(Validation), got Ok(())"),
}
}
#[tokio::test]
#[serial]
async fn free_value_passes() {
init_test_db().await;
seed_widget(1, "taken").await;
let data = json!({"slug": "free"});
let result = AsyncValidator::new(&data)
.rules("slug", rules![required(), string()])
.async_rule("slug", unique("widgets", "slug"))
.validate_async()
.await;
assert!(result.is_ok(), "expected Ok(()), got: {result:?}");
}
#[tokio::test]
#[serial]
async fn exclude_self_passes_on_edit() {
init_test_db().await;
seed_widget(1, "taken").await;
let data = json!({"slug": "taken"});
let result = AsyncValidator::new(&data)
.rules("slug", rules![required(), string()])
.async_rule("slug", unique("widgets", "slug").ignore(1_i64))
.validate_async()
.await;
assert!(
result.is_ok(),
"expected Ok(()) when excluding own row, got: {result:?}"
);
}
#[tokio::test]
#[serial]
async fn sync_failure_skips_async() {
init_test_db().await;
let data = json!({"slug": ""});
let result = AsyncValidator::new(&data)
.rules("slug", rules![required(), string()])
.async_rule("slug", unique("widgets", "slug"))
.validate_async()
.await;
match result {
Err(AsyncValidationError::Validation(ref e)) => {
assert!(e.has("slug"), "expected 'slug' error from sync rule");
let msg = e.first("slug").expect("first error on slug");
assert!(
!msg.contains("taken"),
"unique message must not appear when sync fails: {msg}"
);
}
other => panic!("expected Err(Validation), got: {other:?}"),
}
}
#[tokio::test]
#[serial]
async fn redirect_back_shape() {
init_test_db().await;
seed_widget(2, "occupied").await;
let data = json!({"slug": "occupied"});
let result = AsyncValidator::new(&data)
.rules("slug", rules![required(), string()])
.async_rule("slug", unique("widgets", "slug"))
.validate_async()
.await;
let ve = match result {
Err(AsyncValidationError::Validation(e)) => e,
other => panic!("expected Err(Validation), got: {other:?}"),
};
let action_err = ve
.with_old_input(&data)
.into_action_error("/widgets/create");
let debug_str = format!("{action_err:?}");
assert!(
!debug_str.contains("Internal"),
"expected validation redirect ActionError, got: {debug_str}"
);
}