use std::collections::HashMap;
use sea_orm::{ConnectionTrait, Schema};
use serde_json::{Value, json};
use crate::{
Persistence, Querying, Record, RecordError, RecordState,
base::test_support::{TestUser, seed_users, test_user},
dirty::DirtyTracking,
persistence::AsyncPersistence,
};
use rustrails_support::{database, runtime};
macro_rules! ignored_tests {
($reason:literal => [$($name:ident),+ $(,)?]) => {
$(
#[test]
#[ignore = $reason]
fn $name() {}
)+
};
}
fn user_attrs(name: &str, email: &str) -> HashMap<String, Value> {
HashMap::from([
("name".to_owned(), json!(name)),
("email".to_owned(), json!(email)),
])
}
fn build_user(name: &str, email: &str) -> TestUser {
TestUser {
name: name.to_owned(),
email: email.to_owned(),
..Default::default()
}
}
fn run_persistence_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_empty_persistence_test(test: impl FnOnce() + Send + 'static) {
run_persistence_test(false, test);
}
fn run_seeded_persistence_test(test: impl FnOnce() + Send + 'static) {
run_persistence_test(true, test);
}
mod tracked_touch_support {
use std::collections::HashMap;
use sea_orm::{
ActiveModelTrait, ActiveValue::NotSet, ActiveValue::Set, ConnectionTrait, EntityTrait,
Schema,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{Record, RecordState, persistence::AsyncPersistence, querying::AsyncQuerying};
use rustrails_support::{database, runtime};
pub mod tracked_user {
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "tracked_users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub email: String,
pub updated_at: String,
pub touched_at: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
fn default_record_state() -> RecordState {
RecordState::New
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrackedUser {
pub id: Option<i64>,
pub name: String,
pub email: String,
pub updated_at: String,
pub touched_at: String,
#[serde(skip, default = "default_record_state")]
pub state: RecordState,
}
impl Default for TrackedUser {
fn default() -> Self {
Self {
id: None,
name: String::new(),
email: String::new(),
updated_at: "original-updated".to_owned(),
touched_at: "original-touched".to_owned(),
state: RecordState::New,
}
}
}
impl Record for TrackedUser {
type Entity = tracked_user::Entity;
fn table_name() -> &'static str {
"tracked_users"
}
fn id(&self) -> Option<i64> {
self.id
}
fn record_state(&self) -> RecordState {
self.state
}
fn set_record_state(&mut self, state: RecordState) {
self.state = state;
}
fn from_sea_model(model: <Self::Entity as EntityTrait>::Model) -> Self {
Self {
id: Some(i64::from(model.id)),
name: model.name,
email: model.email,
updated_at: model.updated_at,
touched_at: model.touched_at,
state: RecordState::Persisted,
}
}
fn to_active_model(&self) -> <Self::Entity as EntityTrait>::ActiveModel
where
<Self::Entity as EntityTrait>::ActiveModel: ActiveModelTrait,
{
tracked_user::ActiveModel {
id: match self.id.and_then(|value| i32::try_from(value).ok()) {
Some(value) => Set(value),
None => NotSet,
},
name: Set(self.name.clone()),
email: Set(self.email.clone()),
updated_at: Set(self.updated_at.clone()),
touched_at: Set(self.touched_at.clone()),
}
}
}
impl AsyncQuerying for TrackedUser {}
impl AsyncPersistence for TrackedUser {}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PromotedTrackedUser {
pub id: Option<i64>,
pub name: String,
pub email: String,
pub updated_at: String,
pub touched_at: String,
pub nickname: String,
#[serde(rename = "type")]
pub record_type: String,
#[serde(skip, default = "default_record_state")]
pub state: RecordState,
}
impl Default for PromotedTrackedUser {
fn default() -> Self {
Self {
id: None,
name: String::new(),
email: String::new(),
updated_at: String::new(),
touched_at: String::new(),
nickname: "guest".to_owned(),
record_type: "PromotedTrackedUser".to_owned(),
state: RecordState::New,
}
}
}
impl Record for PromotedTrackedUser {
type Entity = tracked_user::Entity;
fn table_name() -> &'static str {
"tracked_users"
}
fn id(&self) -> Option<i64> {
self.id
}
fn record_state(&self) -> RecordState {
self.state
}
fn set_record_state(&mut self, state: RecordState) {
self.state = state;
}
fn from_sea_model(model: <Self::Entity as EntityTrait>::Model) -> Self {
Self {
id: Some(i64::from(model.id)),
name: model.name,
email: model.email,
updated_at: model.updated_at,
touched_at: model.touched_at,
..Self::default()
}
}
fn to_active_model(&self) -> <Self::Entity as EntityTrait>::ActiveModel
where
<Self::Entity as EntityTrait>::ActiveModel: ActiveModelTrait,
{
tracked_user::ActiveModel {
id: match self.id.and_then(|value| i32::try_from(value).ok()) {
Some(value) => Set(value),
None => NotSet,
},
name: Set(self.name.clone()),
email: Set(self.email.clone()),
updated_at: Set(self.updated_at.clone()),
touched_at: Set(self.touched_at.clone()),
}
}
}
impl AsyncQuerying for PromotedTrackedUser {}
impl AsyncPersistence for PromotedTrackedUser {}
pub fn tracked_attrs(name: &str, email: &str) -> HashMap<String, Value> {
HashMap::from([
("name".to_owned(), Value::String(name.to_owned())),
("email".to_owned(), Value::String(email.to_owned())),
])
}
pub fn run_tracked_persistence_test(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(tracked_user::Entity))
.await
.expect("tracked_users table should be created");
});
test();
})
.join()
.unwrap();
}
}
mod minimalistic_support {
use sea_orm::{
ActiveModelTrait, ActiveValue::NotSet, ActiveValue::Set, ConnectionTrait, EntityTrait,
Schema,
};
use serde::{Deserialize, Serialize};
use crate::{Record, RecordState, persistence::AsyncPersistence, querying::AsyncQuerying};
use rustrails_support::{database, runtime};
pub mod minimalistic {
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "minimalistics")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
fn default_record_state() -> RecordState {
RecordState::New
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Minimalistic {
pub id: Option<i64>,
#[serde(skip, default = "default_record_state")]
pub state: RecordState,
}
impl Default for Minimalistic {
fn default() -> Self {
Self {
id: None,
state: RecordState::New,
}
}
}
impl Record for Minimalistic {
type Entity = minimalistic::Entity;
fn table_name() -> &'static str {
"minimalistics"
}
fn id(&self) -> Option<i64> {
self.id
}
fn record_state(&self) -> RecordState {
self.state
}
fn set_record_state(&mut self, state: RecordState) {
self.state = state;
}
fn from_sea_model(model: <Self::Entity as EntityTrait>::Model) -> Self {
Self {
id: Some(i64::from(model.id)),
state: RecordState::Persisted,
}
}
fn to_active_model(&self) -> <Self::Entity as EntityTrait>::ActiveModel
where
<Self::Entity as EntityTrait>::ActiveModel: ActiveModelTrait,
{
minimalistic::ActiveModel {
id: match self.id.and_then(|value| i32::try_from(value).ok()) {
Some(value) => Set(value),
None => NotSet,
},
}
}
}
impl AsyncQuerying for Minimalistic {}
impl AsyncPersistence for Minimalistic {}
pub fn run_minimalistic_persistence_test(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(minimalistic::Entity))
.await
.expect("minimalistics table should be created");
});
test();
})
.join()
.unwrap();
}
}
ignored_tests!(
"Rails-specific: adapter-specific auto-populated columns and composite primary key fixtures are not represented by TestUser" => [
test_populates_non_primary_key_autoincremented_column,
test_populates_autoincremented_id_pk_regardless_of_its_position_in_columns_list,
test_populates_non_primary_key_autoincremented_column_for_a_cpk_model,
]
);
mod postgresql_adapter_defaults {
ignored_tests!(
"Rails-specific: PostgreSQL default-expression columns are not represented by TestUser" => [
test_fills_auto_populated_columns_on_creation,
]
);
}
mod sqlite3_adapter_defaults {
ignored_tests!(
"Rails-specific: SQLite default-expression columns beyond the primary key are not represented by TestUser" => [
test_fills_auto_populated_columns_on_creation,
]
);
}
mod mysql_adapter_defaults {
ignored_tests!(
"Rails-specific: MySQL auto-populated columns beyond the primary key are not represented by TestUser" => [
test_fills_auto_populated_columns_on_creation,
]
);
}
ignored_tests!(
"Rails-specific: Active Record bulk update overloads for arrays, duplicate ids, scoped class updates, and validation-returning objects are not exposed by rustrails-record" => [
test_update_many,
test_update_many_with_duplicated_ids,
test_update_many_with_invalid_id,
test_update_many_with_active_record_base_object,
test_update_many_with_array_of_active_record_base_objects,
test_class_level_update_without_ids,
test_class_level_update_is_affected_by_scoping,
test_returns_object_even_if_validations_failed,
test_update_many_bang,
test_update_many_with_duplicated_ids_bang,
test_update_many_with_invalid_id_bang,
test_update_many_with_active_record_base_object_bang,
test_update_many_with_array_of_active_record_base_objects_bang,
test_class_level_update_without_ids_bang,
test_class_level_update_is_affected_by_scoping_bang,
test_raises_error_when_validations_failed,
]
);
#[test]
fn test_delete_all() {
run_seeded_persistence_test(|| {
let deleted = TestUser::destroy_all_sync(HashMap::new())
.expect("bulk delete should remove every row for TestUser");
assert_eq!(deleted, 3);
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
0
);
});
}
ignored_tests!(
"Rails-specific: counter helpers, callback ordering, becomes/STI, and composite-key destroy semantics are outside the generic TestUser persistence API" => [
test_increment_attribute,
test_increment_aliased_attribute,
test_increment_nil_attribute,
test_increment_attribute_by,
test_increment_updates_counter_in_db_using_offset,
test_increment_with_touch_updates_timestamps,
test_increment_with_touch_an_attribute_updates_timestamps,
test_increment_with_no_arg,
test_increment_new_record,
test_increment_destroyed_record,
test_decrement_attribute,
test_decrement_attribute_by,
test_decrement_with_touch_updates_timestamps,
test_decrement_with_touch_an_attribute_updates_timestamps,
test_update_sti_type,
test_preserve_original_sti_type,
test_update_sti_subclass_type,
test_becomes_default_sti_subclass,
test_update_after_create,
test_update_attribute_after_update,
test_update_attribute_in_before_validation_respects_callback_chain,
test_update_attribute_does_not_run_sql_if_attribute_is_not_changed,
test_update_does_not_run_sql_if_record_has_not_changed,
]
);
#[test]
fn test_create() {
run_empty_persistence_test(|| {
let user = TestUser::create_sync(user_attrs("New Topic", "new-topic@example.com"))
.expect("create_sync should insert a new TestUser row");
let reloaded =
TestUser::find_sync(user.id().expect("newly created user should have an id"))
.expect("find_sync should reload the inserted row");
assert_eq!(reloaded.name, "New Topic");
assert_eq!(reloaded.email, "new-topic@example.com");
assert!(user.persisted());
});
}
#[test]
fn test_create_prefetched_pk() {
run_empty_persistence_test(|| {
let user = TestUser::create_sync(HashMap::from([
("id".to_owned(), json!(123_456)),
("name".to_owned(), json!("Prefetched")),
("email".to_owned(), json!("prefetched@example.com")),
]))
.expect("create_sync should preserve an explicit primary key");
assert_eq!(user.id(), Some(123_456));
assert!(user.persisted());
});
}
ignored_tests!(
"Rails-specific: rustrails-record does not layer model validations or duplicate-object persistence semantics onto TestUser" => [
test_create_model_with_uuid_pk_populates_id,
test_create_model_with_custom_named_uuid_pk_populates_id,
test_build_through_factory_with_block,
test_build_many_through_factory_with_block,
test_create_columns_not_equal_attributes,
test_create_through_factory_with_block,
test_create_many_through_factory_with_block,
]
);
#[test]
fn test_build() {
let user = build_user("New Topic", "new-topic@example.com");
assert_eq!(user.name, "New Topic");
assert_eq!(user.email, "new-topic@example.com");
assert!(user.new_record());
assert!(!user.persisted());
}
#[test]
fn test_build_many() {
let users = vec![
build_user("first", "first@example.com"),
build_user("second", "second@example.com"),
];
assert_eq!(users.len(), 2);
assert_eq!(users[0].name, "first");
assert_eq!(users[1].name, "second");
assert!(users.iter().all(|user| !user.persisted()));
}
#[test]
fn test_save_valid_record() {
run_empty_persistence_test(|| {
let mut user = build_user("New Topic", "new-topic@example.com");
user.save_bang_sync()
.expect("save_bang_sync should persist a valid TestUser");
assert!(user.persisted());
assert!(user.id().is_some());
});
}
ignored_tests!(
"Rails-specific: rustrails-record does not layer model validations or duplicate-object persistence semantics onto TestUser" => [
test_save_invalid_record,
test_save_with_duping_of_destroyed_object,
]
);
#[test]
fn test_save_for_record_with_only_primary_key() {
minimalistic_support::run_minimalistic_persistence_test(|| {
let mut minimalistic = minimalistic_support::Minimalistic::default();
minimalistic
.save_sync()
.expect("save_sync should persist a primary-key-only record");
assert!(minimalistic.persisted());
assert!(minimalistic.id().is_some());
assert_eq!(
minimalistic_support::Minimalistic::count_sync()
.expect("count_sync should succeed for primary-key-only rows"),
1
);
});
}
#[test]
fn test_save_for_record_with_only_primary_key_that_is_provided() {
minimalistic_support::run_minimalistic_persistence_test(|| {
let minimalistic = minimalistic_support::Minimalistic::create_sync(HashMap::from([(
"id".to_owned(),
json!(2),
)]))
.expect("create_sync should persist an explicitly keyed primary-key-only record");
assert_eq!(minimalistic.id(), Some(2));
assert!(minimalistic.persisted());
assert_eq!(
minimalistic_support::Minimalistic::find_sync(2)
.expect("find_sync should reload the explicit primary key")
.id(),
Some(2)
);
});
}
#[test]
fn test_save_destroyed_object() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("New Topic", "new-topic@example.com"))
.expect("create_sync should seed the row to destroy");
user.destroy_sync().expect("destroy_sync should succeed");
let error = user
.save_bang_sync()
.expect_err("saving a destroyed record should fail");
assert!(matches!(error, RecordError::NotSaved));
});
}
#[test]
fn test_save_null_string_attributes() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row to update");
user.update_attributes_sync(HashMap::from([
("name".to_owned(), json!("null")),
("email".to_owned(), json!("null@example.com")),
]))
.expect("update_attributes_sync should persist literal string values");
user.reload_sync().expect("reload_sync should succeed");
assert_eq!(user.name, "null");
assert_eq!(user.email, "null@example.com");
});
}
ignored_tests!(
"Rails-specific: TestUser stores String fields rather than nullable string columns, so nil-string persistence semantics do not apply" => [
test_save_nil_string_attributes,
]
);
#[test]
fn test_create_many() {
run_empty_persistence_test(|| {
let users = TestUser::insert_all_sync(vec![
user_attrs("first", "first@example.com"),
user_attrs("second", "second@example.com"),
])
.expect("insert_all_sync should create multiple rows");
assert_eq!(users.len(), 2);
assert_eq!(users[0].name, "first");
assert_eq!(users[1].name, "second");
assert!(users.iter().all(|user| user.persisted()));
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
2
);
});
}
#[test]
fn test_update_object() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Another Topic", "topic@example.com"))
.expect("create_sync should seed the row to update");
let id = user.id().expect("seeded row should have an id");
user.name = "Updated topic".to_owned();
user.save_sync()
.expect("save_sync should update persisted rows");
let reloaded = TestUser::find_sync(id).expect("find_sync should reload the updated row");
assert_eq!(reloaded.name, "Updated topic");
});
}
ignored_tests!(
"Rails-specific: unknown-column updates and STI transitions are not represented by the generic TestUser helpers" => [
test_update_columns_not_equal_attributes,
]
);
#[test]
fn test_update_for_record_with_only_primary_key() {
minimalistic_support::run_minimalistic_persistence_test(|| {
let id = minimalistic_support::Minimalistic::create_sync(HashMap::new())
.expect("create_sync should persist a primary-key-only record")
.id()
.expect("primary-key-only record should receive an id");
let mut minimalistic = minimalistic_support::Minimalistic::find_sync(id)
.expect("find_sync should reload the persisted primary-key-only record");
minimalistic
.save_sync()
.expect("save_sync should allow updating a primary-key-only record with no changes");
assert_eq!(minimalistic.id(), Some(id));
assert!(minimalistic.persisted());
assert_eq!(
minimalistic_support::Minimalistic::count_sync()
.expect("count_sync should succeed for primary-key-only rows"),
1
);
});
}
fn assert_becomes_projects_shared_attributes_and_preserves_record_state() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_touch_support::TrackedUser::create_sync(
tracked_touch_support::tracked_attrs("Alice", "alice@example.com"),
)
.expect("create_sync should seed the tracked row");
user.set_record_state(RecordState::Destroyed);
let promoted = user
.becomes::<tracked_touch_support::PromotedTrackedUser>()
.expect("becomes should project into the promoted type");
assert_eq!(promoted.id, user.id);
assert_eq!(promoted.name, user.name);
assert_eq!(promoted.email, user.email);
assert_eq!(promoted.record_state(), RecordState::Destroyed);
assert_eq!(promoted.nickname, "guest");
assert_eq!(promoted.record_type, "PromotedTrackedUser");
});
}
#[test]
fn test_becomes() {
assert_becomes_projects_shared_attributes_and_preserves_record_state();
}
#[test]
fn test_becomes_preserve_record_status() {
assert_becomes_projects_shared_attributes_and_preserves_record_state();
}
#[test]
fn test_becomes_wont_break_mutation_tracking() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_touch_support::TrackedUser::create_sync(
tracked_touch_support::tracked_attrs("Alice", "alice@example.com"),
)
.expect("create_sync should seed the tracked row");
user.name = "Alicia".to_owned();
user.readonly_bang();
let promoted = user
.becomes::<tracked_touch_support::PromotedTrackedUser>()
.expect("becomes should preserve dirty tracking for shared fields");
assert!(promoted.attribute_changed("name"));
assert_eq!(promoted.attribute_was("name"), Some(json!("Alice")));
assert!(crate::dirty::readonly(&promoted));
});
}
#[test]
fn test_becomes_initializes_missing_attributes() {
tracked_touch_support::run_tracked_persistence_test(|| {
let user = tracked_touch_support::TrackedUser::create_sync(
tracked_touch_support::tracked_attrs("Alice", "alice@example.com"),
)
.expect("create_sync should seed the tracked row");
let promoted = user
.becomes::<tracked_touch_support::PromotedTrackedUser>()
.expect("becomes should fill promoted defaults for missing fields");
assert_eq!(promoted.nickname, "guest");
assert_eq!(promoted.record_type, "PromotedTrackedUser");
});
}
#[test]
fn test_becomes_same_class_makes_clone() {
tracked_touch_support::run_tracked_persistence_test(|| {
let user = tracked_touch_support::TrackedUser::create_sync(
tracked_touch_support::tracked_attrs("Alice", "alice@example.com"),
)
.expect("create_sync should seed the tracked row");
let mut clone = user
.becomes::<tracked_touch_support::TrackedUser>()
.expect("becomes should clone into the same type");
assert_eq!(clone, user);
clone.name = "Alicia".to_owned();
assert_eq!(user.name, "Alice");
assert_eq!(clone.name, "Alicia");
});
}
#[test]
fn test_destroy_many() {
run_seeded_persistence_test(|| {
let destroyed = TestUser::destroy_all_sync(HashMap::from([(
"email".to_owned(),
json!("alice@example.com"),
)]))
.expect("destroy_all_sync should remove matching rows");
assert_eq!(destroyed, 1);
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
2
);
assert!(matches!(TestUser::find_sync(1), Err(RecordError::NotFound)));
});
}
#[test]
fn test_destroy_many_with_invalid_id() {
run_seeded_persistence_test(|| {
let destroyed =
TestUser::destroy_all_sync(HashMap::from([("id".to_owned(), json!(99_999))]))
.expect("destroy_all_sync should succeed even when nothing matches");
assert_eq!(destroyed, 0);
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
3
);
});
}
ignored_tests!(
"Rails-specific: callback execution, scoping interactions, destroy! variants, association freezing semantics, and class-level id-array helpers are not exposed by rustrails-record" => [
test_delete_doesnt_run_callbacks,
test_delete_isnt_affected_by_scoping,
test_destroy_bang,
test_destroy_for_a_failed_to_destroy_cpk_record,
test_delete_new_record,
test_delete_record_with_associations,
test_destroy_record_with_associations,
test_class_level_destroy_is_affected_by_scoping,
test_class_level_delete_is_affected_by_scoping,
]
);
#[test]
fn test_delete() {
run_empty_persistence_test(|| {
let id = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row to delete")
.id()
.expect("seeded row should have an id");
TestUser::delete_sync(id).expect("delete_sync should remove the row by id");
assert!(matches!(
TestUser::find_sync(id),
Err(RecordError::NotFound)
));
});
}
#[test]
fn test_destroy_new_record() {
run_empty_persistence_test(|| {
let mut user = TestUser::default();
let error = user
.destroy_sync()
.expect_err("destroying a new record should fail");
assert!(matches!(error, RecordError::NotSaved));
assert!(user.new_record());
assert!(!user.persisted());
assert!(!user.destroyed());
});
}
#[test]
fn test_class_level_delete_with_invalid_ids() {
run_empty_persistence_test(|| {
let error = TestUser::delete_sync(99_999).expect_err("deleting a missing id should fail");
assert!(matches!(error, RecordError::NotFound));
});
}
ignored_tests!(
"Rails-specific: SQL-expression update values are not part of rustrails-record" => [
test_update_all_with_custom_sql_as_value,
]
);
#[test]
fn test_destroy() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row to destroy");
let id = user.id().expect("seeded row should have an id");
user.destroy_sync()
.expect("destroy_sync should delete the row");
assert!(user.destroyed());
assert!(matches!(
TestUser::find_sync(id),
Err(RecordError::NotFound)
));
});
}
#[test]
fn test_find_raises_record_not_found_exception() {
run_empty_persistence_test(|| {
let error = TestUser::find_sync(99_999).expect_err("missing ids should return not found");
assert!(matches!(error, RecordError::NotFound));
});
}
#[test]
fn test_destroy_raises_record_not_found_exception() {
run_empty_persistence_test(|| {
let mut user = TestUser::persisted(99_999, "Ghost", "ghost@example.com");
let error = user
.destroy_sync()
.expect_err("destroying a missing row should return not found");
assert!(matches!(error, RecordError::NotFound));
});
}
#[test]
fn test_update_all() {
run_seeded_persistence_test(|| {
let updated = TestUser::update_all_sync(
HashMap::new(),
HashMap::from([("name".to_owned(), json!("bulk updated"))]),
)
.expect("update_all_sync should update every seeded row");
assert_eq!(updated, 3);
let users = TestUser::all_sync().expect("all_sync should reload all seeded rows");
assert!(users.iter().all(|user| user.name == "bulk updated"));
});
}
#[test]
fn test_update_all_with_hash() {
run_seeded_persistence_test(|| {
let updated = TestUser::update_all_sync(
HashMap::new(),
HashMap::from([
("name".to_owned(), json!("bulk updated with hash")),
("email".to_owned(), json!("updated@example.com")),
]),
)
.expect("update_all_sync should accept multiple column updates");
assert_eq!(updated, 3);
let users = TestUser::all_sync().expect("all_sync should reload all seeded rows");
assert!(users.iter().all(
|user| user.name == "bulk updated with hash" && user.email == "updated@example.com"
));
});
}
ignored_tests!(
"Rails-specific: Active Record's update_attribute/update_column/update_columns overloads, callback-specific bang cases, custom touch keyword shapes, per-attribute readonly enforcement, and default-scope semantics are outside the generic TestUser API" => [
test_update_attribute,
test_update_attribute_for_updated_at_on,
test_update_attribute_for_updated_at_on_bang,
test_update_attribute_for_aborted_callback_bang,
test_update_column,
test_update_column_touch_option_with_specific_time,
test_update_column_should_not_use_setter_method,
test_update_column_with_model_having_primary_key_other_than_id,
test_update_column_should_not_modify_updated_at,
test_update_column_with_default_scope,
test_update_columns,
test_update_columns_touch_option_with_specific_time,
test_update_columns_should_not_use_setter_method,
test_update_columns_with_model_having_primary_key_other_than_id,
test_update_columns_should_not_modify_updated_at,
test_update_columns_with_one_changed_and_one_updated,
test_update_columns_changing_id,
test_update_columns_returns_boolean,
test_update_columns_with_default_scope,
]
);
fn assert_update_attribute_bang_persists_single_change() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row to update");
let id = user.id().expect("seeded row should have an id");
user.name = "Alicia".to_owned();
user.save_bang_sync()
.expect("save_bang_sync should persist a single changed attribute");
assert_eq!(user.name, "Alicia");
assert!(!user.changed());
assert!(!user.attribute_changed("name"));
assert_eq!(user.attribute_was("name"), None);
user.reload_sync().expect("reload_sync should succeed");
assert_eq!(user.id(), Some(id));
assert_eq!(user.name, "Alicia");
assert_eq!(user.email, "alice@example.com");
});
}
#[test]
fn test_update_attribute_for_readonly_attribute() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row to update");
let id = user.id().expect("seeded row should have an id");
user.readonly_bang();
let error = user
.update_attributes_sync(HashMap::from([("name".to_owned(), json!("Alicia"))]))
.expect_err("readonly updates should fail");
assert!(matches!(error, RecordError::ReadOnlyRecord));
assert!(user.readonly());
assert_eq!(
TestUser::find_sync(id)
.expect("readonly failure should leave the row intact")
.name,
"Alice"
);
crate::dirty::clear_state(&user);
});
}
#[test]
fn test_update_attribute_for_readonly_attribute_bang() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row to update");
let id = user.id().expect("seeded row should have an id");
user.readonly_bang();
user.name = "Alicia".to_owned();
let error = user
.save_bang_sync()
.expect_err("readonly bang updates should fail");
assert!(matches!(error, RecordError::ReadOnlyRecord));
assert!(user.readonly());
assert_eq!(
TestUser::find_sync(id)
.expect("readonly bang failure should leave the row intact")
.name,
"Alice"
);
crate::dirty::clear_state(&user);
});
}
#[test]
fn test_update_attribute_with_one_updated() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row to update");
let id = user.id().expect("seeded row should have an id");
user.update_attributes_sync(HashMap::from([("name".to_owned(), json!("Alicia"))]))
.expect("update_attributes_sync should persist a single attribute change");
assert_eq!(user.name, "Alicia");
assert!(!user.changed());
assert!(!user.attribute_changed("name"));
assert_eq!(user.attribute_was("name"), None);
user.reload_sync().expect("reload_sync should succeed");
assert_eq!(user.id(), Some(id));
assert_eq!(user.name, "Alicia");
assert_eq!(user.email, "alice@example.com");
});
}
#[test]
fn test_update_attribute_with_one_updated_bang() {
assert_update_attribute_bang_persists_single_change();
}
#[test]
fn test_update_attribute_bang() {
assert_update_attribute_bang_persists_single_change();
}
#[test]
fn test_update_bang() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row to update");
let id = user.id().expect("seeded row should have an id");
user.name = "Alicia".to_owned();
user.email = "alicia@example.com".to_owned();
user.save_bang_sync()
.expect("save_bang_sync should persist bang updates for persisted rows");
assert!(!user.changed());
user.reload_sync().expect("reload_sync should succeed");
assert_eq!(user.id(), Some(id));
assert_eq!(user.name, "Alicia");
assert_eq!(user.email, "alicia@example.com");
});
}
fn assert_touch_updates_default_timestamp_only() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_touch_support::TrackedUser::create_sync(
tracked_touch_support::tracked_attrs("Alice", "alice@example.com"),
)
.expect("create_sync should seed the tracked row");
let id = user.id().expect("tracked row should have an id");
let touched_before = user.touched_at.clone();
let updated_before = user.updated_at.clone();
database::with_db(|db| runtime::block_on(user.touch(&[], db)))
.expect("touch should update updated_at");
assert_ne!(user.updated_at, updated_before);
assert_eq!(user.touched_at, touched_before);
assert_eq!(
tracked_touch_support::TrackedUser::find_sync(id)
.expect("find_sync should reload the touched row")
.touched_at,
touched_before
);
});
}
#[test]
fn test_update_column_touch_option() {
assert_touch_updates_default_timestamp_only();
}
#[test]
fn test_update_columns_touch_option_updates_timestamps() {
assert_touch_updates_default_timestamp_only();
}
#[test]
fn test_update_column_for_readonly_attribute() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_touch_support::TrackedUser::create_sync(
tracked_touch_support::tracked_attrs("Alice", "alice@example.com"),
)
.expect("create_sync should seed the tracked row");
user.readonly_bang();
let error = database::with_db(|db| runtime::block_on(user.touch(&[], db)))
.expect_err("readonly touch should fail");
assert!(matches!(error, RecordError::ReadOnlyRecord));
crate::dirty::clear_state(&user);
});
}
fn assert_touch_does_not_leave_the_object_dirty() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_touch_support::TrackedUser::create_sync(
tracked_touch_support::tracked_attrs("Alice", "alice@example.com"),
)
.expect("create_sync should seed the tracked row");
database::with_db(|db| runtime::block_on(user.touch(&[], db)))
.expect("touch should update updated_at");
assert!(!user.changed());
assert!(!user.attribute_changed("updated_at"));
assert!(user.previous_changes().contains_key("updated_at"));
});
}
#[test]
fn test_update_column_should_not_leave_the_object_dirty() {
assert_touch_does_not_leave_the_object_dirty();
}
#[test]
fn test_update_columns_should_not_leave_the_object_dirty() {
assert_touch_does_not_leave_the_object_dirty();
}
#[test]
fn test_update_columns_touch_option_explicit_column_names() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_touch_support::TrackedUser::create_sync(
tracked_touch_support::tracked_attrs("Alice", "alice@example.com"),
)
.expect("create_sync should seed the tracked row");
let updated_before = user.updated_at.clone();
let touched_before = user.touched_at.clone();
database::with_db(|db| runtime::block_on(user.touch(&["touched_at"], db)))
.expect("touch should update requested timestamp columns");
assert_ne!(user.updated_at, updated_before);
assert_ne!(user.touched_at, touched_before);
assert_eq!(
user.previous_changes()
.get("touched_at")
.map(|(_, new)| new),
Some(&json!(user.touched_at))
);
});
}
fn assert_touch_rejects_new_record(columns: Vec<&'static str>) {
tracked_touch_support::run_tracked_persistence_test(move || {
let mut user = tracked_touch_support::TrackedUser::default();
let error = database::with_db(|db| runtime::block_on(user.touch(columns.as_slice(), db)))
.expect_err("touch should reject new records");
assert!(matches!(error, RecordError::NotSaved));
});
}
#[test]
fn test_update_column_should_raise_exception_if_new_record() {
assert_touch_rejects_new_record(vec![]);
}
#[test]
fn test_update_columns_should_raise_exception_if_new_record() {
assert_touch_rejects_new_record(vec!["touched_at"]);
}
#[test]
fn test_touch_rejects_destroyed_records() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_touch_support::TrackedUser::create_sync(
tracked_touch_support::tracked_attrs("Alice", "alice@example.com"),
)
.expect("create_sync should seed the tracked row");
user.set_record_state(RecordState::Destroyed);
let error = database::with_db(|db| runtime::block_on(user.touch(&[], db)))
.expect_err("touch should reject destroyed records");
assert!(matches!(error, RecordError::NotSaved));
});
}
fn assert_touch_preserves_other_unsaved_dirty_changes() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_touch_support::TrackedUser::create_sync(
tracked_touch_support::tracked_attrs("Alice", "alice@example.com"),
)
.expect("create_sync should seed the tracked row");
user.name = "Alicia".to_owned();
database::with_db(|db| runtime::block_on(user.touch(&["touched_at"], db)))
.expect("touch should persist only timestamp fields");
assert!(user.attribute_changed("name"));
assert!(!user.attribute_changed("updated_at"));
assert!(!user.attribute_changed("touched_at"));
assert_eq!(user.attribute_was("name"), Some(json!("Alice")));
});
}
#[test]
fn test_update_column_with_one_changed_and_one_updated() {
assert_touch_preserves_other_unsaved_dirty_changes();
}
#[test]
fn test_touch_preserves_other_unsaved_dirty_changes() {
assert_touch_preserves_other_unsaved_dirty_changes();
}
#[test]
fn test_update_columns_touch_option_not_overwrite_explicit_attribute() {
assert_touch_preserves_other_unsaved_dirty_changes();
}
#[test]
fn test_update_columns_touch_option_not_overwrite_explicit_attribute_with_string_key() {
assert_touch_preserves_other_unsaved_dirty_changes();
}
#[test]
fn test_update_columns_with_one_readonly_attribute() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_touch_support::TrackedUser::create_sync(
tracked_touch_support::tracked_attrs("Alice", "alice@example.com"),
)
.expect("create_sync should seed the tracked row");
user.readonly_bang();
let error = database::with_db(|db| runtime::block_on(user.touch(&["touched_at"], db)))
.expect_err("readonly touch should fail");
assert!(matches!(error, RecordError::ReadOnlyRecord));
crate::dirty::clear_state(&user);
});
}
#[test]
fn test_update() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("The First Topic", "first@example.com"))
.expect("create_sync should seed the row to update");
user.update_attributes_sync(HashMap::from([
("name".to_owned(), json!("The First Topic Updated")),
("email".to_owned(), json!("updated@example.com")),
]))
.expect("update_attributes_sync should persist multiple attribute changes");
user.reload_sync().expect("reload_sync should succeed");
assert_eq!(user.name, "The First Topic Updated");
assert_eq!(user.email, "updated@example.com");
});
}
ignored_tests!(
"Rails-specific: Active Record update accepts nil/argument overloads and validation exceptions that rustrails-record does not model" => [
test_update_parameters,
]
);
#[test]
fn test_update_raises_record_not_found_exception() {
run_empty_persistence_test(|| {
let mut user = TestUser::persisted(99_999, "Ghost", "ghost@example.com");
user.name = "Updated Ghost".to_owned();
let error = user
.save_sync()
.expect_err("updating a missing row should return not found");
assert!(matches!(error, RecordError::NotFound));
});
}
#[test]
fn test_update_attributes_rejects_unknown_fields() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row to update");
let error = user
.update_attributes_sync(HashMap::from([("missing".to_owned(), json!(true))]))
.expect_err("unknown fields should fail updates");
assert!(matches!(error, RecordError::Invalid(_)));
});
}
#[test]
fn test_update_attributes_preserves_unspecified_fields() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
.expect("create_sync should seed the row to update");
user.update_attributes_sync(HashMap::from([("name".to_owned(), json!("Dani"))]))
.expect("update_attributes_sync should merge partial updates");
assert_eq!(user.name, "Dani");
assert_eq!(user.email, "dana@example.com");
});
}
#[test]
fn test_update_attributes_on_new_record_inserts_and_persists() {
run_empty_persistence_test(|| {
let mut user = TestUser::default();
user.update_attributes_sync(HashMap::from([
("name".to_owned(), json!("Dana")),
("email".to_owned(), json!("dana@example.com")),
]))
.expect("update_attributes_sync should insert new rows");
let reloaded = TestUser::find_sync(user.id().expect("inserted row should have an id"))
.expect("find_sync should reload the inserted row");
assert_eq!(reloaded.name, "Dana");
assert_eq!(reloaded.email, "dana@example.com");
assert!(user.persisted());
});
}
#[test]
fn test_update_attributes_with_type_mismatch_returns_invalid() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
.expect("create_sync should seed the row to update");
let error = user
.update_attributes_sync(HashMap::from([("id".to_owned(), json!("oops"))]))
.expect_err("type mismatches should fail updates");
assert!(matches!(error, RecordError::Invalid(_)));
});
}
#[test]
fn test_update_attributes_on_destroyed_record_returns_not_saved() {
run_empty_persistence_test(|| {
let mut user = TestUser::persisted(1, "Dana", "dana@example.com");
user.set_record_state(crate::RecordState::Destroyed);
let error = user
.update_attributes_sync(HashMap::from([("name".to_owned(), json!("Dani"))]))
.expect_err("destroyed records cannot be updated");
assert!(matches!(error, RecordError::NotSaved));
});
}
#[test]
fn test_delete_missing_id_returns_not_found() {
run_empty_persistence_test(|| {
let error = TestUser::delete_sync(999).expect_err("missing deletes should fail");
assert!(matches!(error, RecordError::NotFound));
});
}
#[test]
fn test_class_level_destroy_only_removes_matching_rows() {
run_seeded_persistence_test(|| {
let deleted =
TestUser::destroy_all_sync(HashMap::from([("name".to_owned(), json!("Carol"))]))
.expect("destroy_all_sync should delete matching rows");
assert_eq!(deleted, 1);
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
2
);
assert!(matches!(TestUser::find_sync(3), Err(RecordError::NotFound)));
assert_eq!(
TestUser::find_sync(1)
.expect("nonmatching rows should remain")
.name,
"Alice"
);
});
}
#[test]
fn test_class_level_delete_leaves_other_rows_untouched() {
run_seeded_persistence_test(|| {
TestUser::delete_sync(1).expect("delete_sync should remove the target row");
assert!(matches!(TestUser::find_sync(1), Err(RecordError::NotFound)));
assert_eq!(
TestUser::find_sync(2)
.expect("other rows should remain")
.name,
"Bob"
);
assert_eq!(
TestUser::count_sync().expect("count_sync should succeed"),
2
);
});
}
#[test]
fn test_reload_without_id_returns_not_saved() {
run_empty_persistence_test(|| {
let mut user = TestUser::default();
let error = user
.reload_sync()
.expect_err("reload_sync should reject unsaved rows");
assert!(matches!(error, RecordError::NotSaved));
});
}
#[test]
fn test_reload_after_row_is_deleted_returns_not_found() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
.expect("create_sync should seed the row to reload");
let id = user.id().expect("seeded row should have an id");
TestUser::delete_sync(id).expect("delete_sync should remove the row");
let error = user
.reload_sync()
.expect_err("reload_sync should fail when the row is gone");
assert!(matches!(error, RecordError::NotFound));
});
}
#[test]
fn test_reload_restores_persisted_state_from_database() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
.expect("create_sync should seed the row to reload");
user.set_record_state(crate::RecordState::Destroyed);
user.reload_sync()
.expect("reload_sync should restore the persisted row state");
assert_eq!(user.state, crate::RecordState::Persisted);
assert_eq!(user.name, "Dana");
assert_eq!(user.email, "dana@example.com");
});
}
#[test]
fn test_becomes_includes_changed_attributes() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_touch_support::TrackedUser::create_sync(
tracked_touch_support::tracked_attrs("Alice", "alice@example.com"),
)
.expect("create_sync should seed the tracked row");
user.name = "Alicia".to_owned();
let promoted = user
.becomes::<tracked_touch_support::PromotedTrackedUser>()
.expect("becomes should preserve shared dirty attributes");
assert_eq!(promoted.changed_attributes(), vec!["name".to_owned()]);
});
}
ignored_tests!(
"Rails-specific: schema-cache reloads, validation error bags, extra projected attributes, and dup semantics are not modeled by the local tracked fixtures" => [
test_becomes_after_reload_schema_from_cache,
test_becomes_errors_base,
test_becomes_includes_errors,
test_becomes_keeps_extra_attributes,
test_duped_becomes_persists_changes_from_the_original,
]
);
ignored_tests!(
"Rails-specific: timestamp overrides, inherited table-name remapping, projection-aware reloads, query-cache behavior, and query-constraint configuration are not represented by TestUser" => [
test_create_with_custom_timestamps,
test_persist_inherited_class_with_different_table_name,
test_instantiate_creates_a_new_instance,
test_reload_removes_custom_selects,
test_reload_via_querycache,
test_save_touch_false,
test_reset_column_information_resets_children,
test_update_uses_query_constraints_config,
test_save_uses_query_constraints_config,
test_reload_uses_query_constraints_config,
test_destroy_uses_query_constraints_config,
test_delete_uses_query_constraints_config,
test_update_attribute_uses_query_constraints_config,
test_it_is_possible_to_update_parts_of_the_query_constraints_config,
test_query_constraints_list_is_nil_if_primary_key_is_nil,
test_query_constraints_list_is_nil_for_non_cpk_model,
test_query_constraints_list_equals_to_composite_primary_key,
test_child_keeps_parents_query_constraints,
test_child_keeps_parents_query_constraints_derived_from_composite_pk,
test_query_constraints_raises_an_error_when_no_columns_provided,
test_child_class_with_query_constraints_overrides_parents,
]
);
ignored_tests!(
"Rails-specific: bulk delete helpers over id arrays and composite-primary-key destroy semantics are not exposed by rustrails-record" => [
test_delete_many,
test_destroy_with_single_composite_primary_key,
test_destroy_with_multiple_composite_primary_keys,
test_destroy_with_invalid_ids_for_a_model_that_expects_composite_keys,
]
);
#[cfg(test)]
mod wave3_persistence_ports {
use super::*;
fn build_case(name: &str, email: &str) -> TestUser {
build_user(name, email)
}
fn persisted_case(id: i64, name: &str, email: &str) -> TestUser {
TestUser::persisted(id, name, email)
}
macro_rules! build_user_case {
($name:ident, $display:expr, $email:expr) => {
#[test]
fn $name() {
let user = build_case($display, $email);
assert_eq!(user.id(), None);
assert_eq!(user.name, $display);
assert_eq!(user.email, $email);
assert!(user.new_record());
assert!(!user.persisted());
}
};
}
build_user_case!(build_user_preserves_ascii_attributes, "Alice", "alice@example.com");
build_user_case!(build_user_preserves_unicode_attributes, "こんにちは", "unicode@example.com");
build_user_case!(build_user_preserves_empty_name, "", "blank@example.com");
build_user_case!(build_user_preserves_symbols_in_email, "Ops", "ops+alerts@example.com");
macro_rules! persisted_user_case {
($name:ident, $id:expr, $display:expr, $email:expr) => {
#[test]
fn $name() {
let user = persisted_case($id, $display, $email);
assert_eq!(user.id(), Some($id));
assert_eq!(user.name, $display);
assert_eq!(user.email, $email);
assert!(!user.new_record());
assert!(user.persisted());
}
};
}
persisted_user_case!(persisted_helper_sets_identifier_and_state, 9, "Alice", "alice@example.com");
persisted_user_case!(persisted_helper_accepts_zero_identifier, 0, "Zero", "zero@example.com");
persisted_user_case!(persisted_helper_preserves_unicode_name, 27, "Ægir", "aegir@example.com");
persisted_user_case!(persisted_helper_preserves_blank_email, 44, "No Mail", "");
#[test]
fn table_name_for_test_user_matches_fixture_table() {
assert_eq!(TestUser::table_name(), "test_users");
}
#[test]
fn table_name_for_tracked_user_matches_fixture_table() {
assert_eq!(tracked_touch_support::TrackedUser::table_name(), "tracked_users");
}
#[test]
fn table_name_for_promoted_tracked_user_matches_fixture_table() {
assert_eq!(
tracked_touch_support::PromotedTrackedUser::table_name(),
"tracked_users"
);
}
#[test]
fn table_name_for_minimalistic_matches_fixture_table() {
assert_eq!(minimalistic_support::Minimalistic::table_name(), "minimalistics");
}
macro_rules! dirty_test_user_unsaved_case {
($name:ident, $field:ident, $from:expr, $to:expr) => {
#[test]
fn $name() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row");
user.$field = $to.to_owned();
assert!(user.changed());
assert!(user.attribute_changed(stringify!($field)));
assert_eq!(user.attribute_was(stringify!($field)), Some(json!($from)));
assert_eq!(
user.changes().get(stringify!($field)),
Some(&(json!($from), json!($to)))
);
});
}
};
}
dirty_test_user_unsaved_case!(dirty_test_user_name_change_is_tracked, name, "Alice", "Alicia");
dirty_test_user_unsaved_case!(dirty_test_user_email_change_is_tracked, email, "alice@example.com", "alicia@example.com");
macro_rules! dirty_test_user_revert_case {
($name:ident, $field:ident, $original:expr, $updated:expr) => {
#[test]
fn $name() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row");
user.$field = $updated.to_owned();
user.$field = $original.to_owned();
assert!(!user.changed());
assert!(!user.attribute_changed(stringify!($field)));
assert_eq!(user.attribute_was(stringify!($field)), None);
});
}
};
}
dirty_test_user_revert_case!(dirty_test_user_name_revert_clears_changes, name, "Alice", "Alicia");
dirty_test_user_revert_case!(dirty_test_user_email_revert_clears_changes, email, "alice@example.com", "alicia@example.com");
macro_rules! dirty_test_user_restore_case {
($name:ident, $field:ident, $original:expr, $updated:expr) => {
#[test]
fn $name() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row");
user.$field = $updated.to_owned();
user.restore_attributes();
assert_eq!(user.$field, $original.to_owned());
assert!(!user.changed());
assert!(!user.attribute_changed(stringify!($field)));
});
}
};
}
dirty_test_user_restore_case!(dirty_test_user_restore_resets_name, name, "Alice", "Alicia");
dirty_test_user_restore_case!(dirty_test_user_restore_resets_email, email, "alice@example.com", "alicia@example.com");
macro_rules! dirty_test_user_clear_case {
($name:ident, $field:ident, $updated:expr) => {
#[test]
fn $name() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row");
user.$field = $updated.to_owned();
user.clear_changes();
assert_eq!(user.$field, $updated.to_owned());
assert!(!user.changed());
assert_eq!(user.attribute_was(stringify!($field)), None);
});
}
};
}
dirty_test_user_clear_case!(dirty_test_user_clear_changes_keeps_name_value, name, "Alicia");
dirty_test_user_clear_case!(dirty_test_user_clear_changes_keeps_email_value, email, "alicia@example.com");
macro_rules! dirty_test_user_previous_case {
($name:ident, $field:ident, $updated:expr, $old_json:expr, $new_json:expr) => {
#[test]
fn $name() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row");
user.$field = $updated.to_owned();
user.save_sync().expect("save_sync should persist the updated row");
assert!(!user.changed());
assert_eq!(
user.previous_changes().get(stringify!($field)),
Some(&($old_json, $new_json))
);
});
}
};
}
dirty_test_user_previous_case!(
dirty_test_user_save_captures_previous_name_change,
name,
"Alicia",
json!("Alice"),
json!("Alicia")
);
dirty_test_user_previous_case!(
dirty_test_user_save_captures_previous_email_change,
email,
"alicia@example.com",
json!("alice@example.com"),
json!("alicia@example.com")
);
fn tracked_user() -> tracked_touch_support::TrackedUser {
tracked_touch_support::TrackedUser::create_sync(tracked_touch_support::tracked_attrs(
"Alice",
"alice@example.com",
))
.expect("create_sync should seed the tracked row")
}
macro_rules! tracked_unsaved_case {
($name:ident, $field:ident, $from:expr, $to:expr) => {
#[test]
fn $name() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_user();
user.$field = $to.to_owned();
assert!(user.changed());
assert!(user.attribute_changed(stringify!($field)));
assert_eq!(user.attribute_was(stringify!($field)), Some(json!($from)));
});
}
};
}
tracked_unsaved_case!(tracked_name_change_is_tracked, name, "Alice", "Alicia");
tracked_unsaved_case!(tracked_email_change_is_tracked, email, "alice@example.com", "alicia@example.com");
tracked_unsaved_case!(tracked_updated_at_change_is_tracked, updated_at, "original-updated", "manual-updated");
tracked_unsaved_case!(tracked_touched_at_change_is_tracked, touched_at, "original-touched", "manual-touched");
macro_rules! tracked_restore_case {
($name:ident, $field:ident, $original:expr, $updated:expr) => {
#[test]
fn $name() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_user();
user.$field = $updated.to_owned();
user.restore_attributes();
assert_eq!(user.$field, $original.to_owned());
assert!(!user.changed());
});
}
};
}
tracked_restore_case!(tracked_restore_resets_name, name, "Alice", "Alicia");
tracked_restore_case!(tracked_restore_resets_email, email, "alice@example.com", "alicia@example.com");
tracked_restore_case!(tracked_restore_resets_updated_at, updated_at, "original-updated", "manual-updated");
tracked_restore_case!(tracked_restore_resets_touched_at, touched_at, "original-touched", "manual-touched");
macro_rules! tracked_previous_case {
($name:ident, $field:ident, $updated:expr, $old_json:expr, $new_json:expr) => {
#[test]
fn $name() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_user();
user.$field = $updated.to_owned();
user.save_sync().expect("save_sync should persist tracked updates");
assert!(!user.changed());
assert_eq!(
user.previous_changes().get(stringify!($field)),
Some(&($old_json, $new_json))
);
});
}
};
}
tracked_previous_case!(tracked_previous_changes_capture_name, name, "Alicia", json!("Alice"), json!("Alicia"));
tracked_previous_case!(
tracked_previous_changes_capture_email,
email,
"alicia@example.com",
json!("alice@example.com"),
json!("alicia@example.com")
);
tracked_previous_case!(
tracked_previous_changes_capture_updated_at,
updated_at,
"manual-updated",
json!("original-updated"),
json!("manual-updated")
);
tracked_previous_case!(
tracked_previous_changes_capture_touched_at,
touched_at,
"manual-touched",
json!("original-touched"),
json!("manual-touched")
);
macro_rules! changed_attributes_case {
($name:ident, $body:block, $expected:expr) => {
#[test]
fn $name() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row");
let user = &mut user;
$body
assert_eq!(user.changed_attributes(), $expected);
});
}
};
}
changed_attributes_case!(
changed_attributes_are_sorted_when_email_changes_before_name,
{
user.email = "alicia@example.com".to_owned();
user.name = "Alicia".to_owned();
},
vec!["email".to_owned(), "name".to_owned()]
);
changed_attributes_case!(
changed_attributes_are_sorted_when_name_changes_before_email,
{
user.name = "Alicia".to_owned();
user.email = "alicia@example.com".to_owned();
},
vec!["email".to_owned(), "name".to_owned()]
);
changed_attributes_case!(
changed_attributes_include_only_name_when_single_field_changes,
{
user.name = "Alicia".to_owned();
},
vec!["name".to_owned()]
);
changed_attributes_case!(
changed_attributes_include_only_email_when_single_field_changes,
{
user.email = "alicia@example.com".to_owned();
},
vec!["email".to_owned()]
);
macro_rules! tracked_changed_attributes_case {
($name:ident, $body:block, $expected:expr) => {
#[test]
fn $name() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_user();
let user = &mut user;
$body
assert_eq!(user.changed_attributes(), $expected);
});
}
};
}
tracked_changed_attributes_case!(
tracked_changed_attributes_sort_name_and_updated_at,
{
user.updated_at = "manual-updated".to_owned();
user.name = "Alicia".to_owned();
},
vec!["name".to_owned(), "updated_at".to_owned()]
);
tracked_changed_attributes_case!(
tracked_changed_attributes_sort_email_and_touched_at,
{
user.touched_at = "manual-touched".to_owned();
user.email = "alicia@example.com".to_owned();
},
vec!["email".to_owned(), "touched_at".to_owned()]
);
#[test]
fn readonly_bang_marks_new_test_user_readonly() {
let mut user = build_user("Alice", "alice@example.com");
user.readonly_bang();
assert!(user.readonly());
crate::dirty::clear_state(&user);
}
#[test]
fn readonly_bang_marks_persisted_test_user_readonly() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row");
user.readonly_bang();
assert!(user.readonly());
crate::dirty::clear_state(&user);
});
}
#[test]
fn readonly_bang_marks_new_tracked_user_readonly() {
let mut user = tracked_touch_support::TrackedUser::default();
user.readonly_bang();
assert!(user.readonly());
crate::dirty::clear_state(&user);
}
#[test]
fn readonly_bang_marks_persisted_tracked_user_readonly() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_user();
user.readonly_bang();
assert!(user.readonly());
crate::dirty::clear_state(&user);
});
}
#[test]
fn clear_state_clears_readonly_for_test_user() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row");
user.readonly_bang();
crate::dirty::clear_state(&user);
assert!(!user.readonly());
});
}
#[test]
fn clear_state_clears_readonly_for_tracked_user() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_user();
user.readonly_bang();
crate::dirty::clear_state(&user);
assert!(!user.readonly());
});
}
#[test]
fn readonly_save_failure_preserves_flag_for_test_user() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row");
user.readonly_bang();
user.name = "Alicia".to_owned();
let error = user.save_sync().expect_err("readonly save should fail");
assert!(matches!(error, RecordError::ReadOnlyRecord));
assert!(user.readonly());
crate::dirty::clear_state(&user);
});
}
#[test]
fn readonly_update_failure_preserves_flag_for_test_user() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row");
user.readonly_bang();
let error = user
.update_attributes_sync(HashMap::from([("name".to_owned(), json!("Alicia"))]))
.expect_err("readonly update should fail");
assert!(matches!(error, RecordError::ReadOnlyRecord));
assert!(user.readonly());
crate::dirty::clear_state(&user);
});
}
#[test]
fn readonly_destroy_failure_preserves_flag_for_test_user() {
run_empty_persistence_test(|| {
let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
.expect("create_sync should seed the row");
user.readonly_bang();
let error = user.destroy_sync().expect_err("readonly destroy should fail");
assert!(matches!(error, RecordError::ReadOnlyRecord));
assert!(user.readonly());
crate::dirty::clear_state(&user);
});
}
#[test]
fn readonly_touch_failure_preserves_flag_for_tracked_user() {
tracked_touch_support::run_tracked_persistence_test(|| {
let mut user = tracked_user();
user.readonly_bang();
let error = database::with_db(|db| runtime::block_on(user.touch(&[], db)))
.expect_err("readonly touch should fail");
assert!(matches!(error, RecordError::ReadOnlyRecord));
assert!(user.readonly());
crate::dirty::clear_state(&user);
});
}
}