use std::collections::HashMap;
use rustrails_model::{
errors::{ErrorType, Errors},
validations::{LengthValidator, NumericalityValidator, PresenceValidator, ValidationSet},
};
use sea_orm::{ConnectionTrait, Schema};
use serde_json::{Value, json};
use crate::{
Querying, RecordState,
base::test_support::{TestUser, seed_users, test_user},
validations::UniquenessValidator,
};
use rustrails_support::{database, runtime};
macro_rules! ignored_tests {
($reason:literal => [$($name:ident),+ $(,)?]) => {
$(
#[test]
#[ignore = $reason]
fn $name() {}
)+
};
}
fn run_sync_validation_test(seed: bool, test: impl FnOnce() + Send + 'static) {
std::thread::spawn(move || {
let _rt = runtime::init_runtime();
database::establish("sqlite::memory:").expect("sqlite in-memory connection should succeed");
runtime::block_on(async {
let db = database::db();
let schema = Schema::new(db.get_database_backend());
db.execute(&schema.create_table_from_entity(test_user::Entity))
.await
.expect("test_users table should be created");
if seed {
seed_users(&db).await;
}
});
test();
})
.join()
.unwrap();
}
fn run_seeded_sync_validation_test(test: impl FnOnce() + Send + 'static) {
run_sync_validation_test(true, test);
}
fn run_validations(
set: &ValidationSet,
attrs: impl IntoIterator<Item = (&'static str, Value)>,
) -> Errors {
let attrs = attrs
.into_iter()
.map(|(name, value)| (name.to_owned(), value))
.collect::<HashMap<_, _>>();
let mut errors = Errors::new();
let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
errors
}
fn run_validations_without_attrs(set: &ValidationSet) -> Errors {
let mut errors = Errors::new();
let _ = set.validate(&|_| None, &mut errors);
errors
}
#[test]
fn test_valid_uses_create_context_when_new() {
run_seeded_sync_validation_test(|| {
let candidate = TestUser {
name: "Alice Clone".to_owned(),
email: "alice@example.com".to_owned(),
state: RecordState::New,
..Default::default()
};
let is_unique = UniquenessValidator::new().validate_unique_sync(
"email",
&json!("alice@example.com"),
&candidate,
);
assert!(!is_unique);
});
}
#[test]
fn test_valid_uses_update_context_when_persisted() {
run_seeded_sync_validation_test(|| {
let alice = TestUser::find_sync(1).expect("seeded Alice row should exist");
let is_unique = UniquenessValidator::new().validate_unique_sync(
"email",
&json!("alice@example.com"),
&alice,
);
assert!(is_unique);
});
}
#[test]
fn test_valid_using_special_context() {
run_seeded_sync_validation_test(|| {
let candidate = TestUser {
name: "Alice Clone".to_owned(),
email: "ALICE@EXAMPLE.COM".to_owned(),
state: RecordState::New,
..Default::default()
};
let is_unique = UniquenessValidator::new()
.case_insensitive()
.validate_unique_sync("email", &json!("ALICE@EXAMPLE.COM"), &candidate);
assert!(!is_unique);
});
}
#[test]
fn test_invalid_using_multiple_contexts() {
let mut set = ValidationSet::new();
set.add("name", PresenceValidator::new());
set.add("name", LengthValidator::new().minimum(3));
let errors = run_validations(&set, [("name", json!(""))]);
assert_eq!(errors.on("name").len(), 2);
assert_eq!(
errors.messages_for("name"),
vec!["can't be blank", "is too short (minimum is 3 characters)",]
);
}
#[test]
fn test_validate() {
let mut set = ValidationSet::new();
set.add("name", PresenceValidator::new());
let missing = run_validations_without_attrs(&set);
assert_eq!(missing.on("name")[0].error_type, ErrorType::Blank);
let present = run_validations(&set, [("name", json!("Alice"))]);
assert!(present.is_empty());
}
ignored_tests!(
"Rails-specific: validation contexts, bang validation exceptions, and create! error propagation are not implemented by rustrails_model ValidationSet" => [
test_invalid_record_exception,
test_validate_with_bang,
test_validate_with_bang_and_context,
test_exception_on_create_bang_many,
test_exception_on_create_bang_with_block,
test_exception_on_create_bang_many_with_block,
test_save_without_validation,
]
);
ignored_tests!(
"Rails-specific: acceptance validators and Active Record attribute-method generation are outside the generic ValidationSet API" => [
test_validates_acceptance_of_with_non_existent_table,
test_validates_acceptance_of_with_undefined_attribute_methods,
test_validates_acceptance_of_as_database_column,
test_acceptance_validator_doesnt_require_db_connection,
]
);
ignored_tests!(
"Rails-specific: Active Record type-casting and custom getter behavior are not modeled by the generic TestUser validation helpers" => [
test_throw_away_typing,
test_numericality_validator_wont_be_affected_by_custom_getter,
]
);
#[test]
fn test_validators() {
let validator = UniquenessValidator::new()
.scope(vec!["account_id".to_owned()])
.case_insensitive()
.message("already used");
assert_eq!(validator.scope, vec!["account_id"]);
assert!(!validator.case_sensitive);
assert_eq!(validator.message.as_deref(), Some("already used"));
}
#[test]
fn test_numericality_validation_with_mutation() {
let mut set = ValidationSet::new();
set.add("wibble", NumericalityValidator::new().only_integer());
let mut wibble = "123-4567".to_owned();
wibble.retain(|ch| ch != '-');
let errors = run_validations(&set, [("wibble", json!(wibble))]);
assert!(errors.is_empty());
}
#[test]
#[ignore = "Rails-specific: raw-value numericality semantics are not exposed by the generic ValidationSet API"]
fn test_numericality_validation_checks_against_raw_value() {}