use std::{collections::HashSet, ops::Deref};
use chrono::NaiveDateTime;
use thiserror::Error;
use crate::{
db::{Entry, EntryId, Group, GroupId, GroupRef, History, MoveGroupError, Times},
Database,
};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum MergeEventType {
Created,
Deleted,
LocationUpdated,
Updated,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum MergeEventTarget {
Entry(EntryId),
Group(GroupId),
}
#[derive(Debug, Clone)]
pub struct MergeEvent {
pub target: MergeEventTarget,
pub event_type: MergeEventType,
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum MergeError {
#[error("Entries with UUID {0} have the same modification time but have diverged.")]
EntryModificationTimeNotUpdated(EntryId),
#[error("Groups with UUID {0} have the same modification time but have diverged.")]
GroupModificationTimeNotUpdated(GroupId),
#[error("Found history entries with the same timestamp ({0}) for entry {1}.")]
DuplicateHistoryEntries(NaiveDateTime, EntryId),
#[error(transparent)]
MoveGroupError(#[from] MoveGroupError),
}
#[derive(Debug, Default, Clone)]
pub struct MergeLog {
pub warnings: Vec<String>,
pub events: Vec<MergeEvent>,
}
impl Database {
pub fn merge(&mut self, other: &Database) -> Result<MergeLog, MergeError> {
let mut log = MergeLog::default();
merge_groups(self, other, &mut log)?;
Ok(log)
}
}
fn get_last_update(group: GroupRef<'_>) -> Option<NaiveDateTime> {
let last_update = group.times.last_modification.or(group.times.location_changed);
group
.entries()
.filter_map(|e| e.times.last_modification.or(e.times.location_changed))
.chain(
group
.groups()
.filter_map(|g| g.times.last_modification.or(g.times.location_changed)),
)
.chain(last_update)
.max()
}
fn merge_groups(dest_db: &mut Database, source_db: &Database, log: &mut MergeLog) -> Result<(), MergeError> {
let dest_groups = dest_db.groups.keys().cloned().collect::<HashSet<_>>();
let source_groups = source_db.groups.keys().cloned().collect::<HashSet<_>>();
let mut groups_to_add = HashSet::new();
for &id in source_groups.difference(&dest_groups) {
#[allow(clippy::unwrap_used)] let source = source_db.group(id).unwrap();
if let Some(deletion_time) = dest_db.deleted_objects.get(&id.uuid()) {
let source_last_update = get_last_update(source);
match (deletion_time, source_last_update) {
(Some(deletion_time), Some(source_last_update)) => {
if *deletion_time >= source_last_update {
continue;
}
}
(Some(_), None) => {
continue;
}
(None, Some(_)) => {
}
(None, None) => {
continue;
}
}
}
groups_to_add.insert(id);
}
let mut add_stack = Vec::new();
loop {
if add_stack.is_empty() {
if let Some(&next) = groups_to_add.iter().next() {
add_stack.push(next);
groups_to_add.remove(&next);
} else {
break;
}
}
#[allow(clippy::expect_used)] let &id = add_stack.last().expect("non-empty queue");
#[allow(clippy::expect_used)] let source = source_db.group(id).expect("source group exists");
#[allow(clippy::expect_used)] let parent_id = source.parent().expect("cannot re-add root").id();
if let Some(mut parent) = dest_db.group_mut(parent_id) {
let mut dest_group = parent.add_group_with_id(id);
dest_group.times = source.times.clone();
dest_group.name = source.name.clone();
dest_group.notes = source.notes.clone();
dest_group.icon = source.icon.clone();
dest_group.custom_data = source.custom_data.clone();
dest_group.is_expanded = source.is_expanded;
dest_group.default_autotype_sequence = source.default_autotype_sequence.clone();
dest_group.enable_autotype = source.enable_autotype;
dest_group.enable_searching = source.enable_searching;
dest_group.last_top_visible_entry = source.last_top_visible_entry;
log.events.push(MergeEvent {
target: MergeEventTarget::Group(id),
event_type: MergeEventType::Created,
});
add_stack.pop();
} else {
add_stack.push(parent_id);
groups_to_add.remove(&parent_id);
}
}
let mut to_delete = Vec::new();
for &id in dest_groups.difference(&source_groups) {
#[allow(clippy::unwrap_used)] let dest = dest_db.group_mut(id).unwrap();
if let Some(deletion_time) = source_db.deleted_objects.get(&id.uuid()) {
let dest_last_updated = get_last_update(dest.as_ref());
if let (Some(deletion_time), Some(dest_last_updated)) = (deletion_time, dest_last_updated) {
if *deletion_time < dest_last_updated {
continue;
}
}
to_delete.push(id);
dest_db.deleted_objects.insert(id.uuid(), *deletion_time);
log.events.push(MergeEvent {
target: MergeEventTarget::Group(id),
event_type: MergeEventType::Deleted,
});
}
}
merge_entries(dest_db, source_db, log)?;
while let Some(id) = to_delete.pop() {
if let Some(group) = dest_db.group_mut(id) {
group.remove();
}
}
let dest_groups = dest_db.groups.keys().cloned().collect::<HashSet<_>>();
let mut moves = Vec::new();
let root_id = dest_db.root().id();
for &id in dest_groups.intersection(&source_groups) {
#[allow(clippy::unwrap_used)] let mut dest = dest_db.group_mut(id).unwrap();
#[allow(clippy::unwrap_used)] let source = source_db.group(id).unwrap();
let dest_parent_id = dest.as_ref().parent().map(|p| p.id());
let source_parent_id = source.parent().map(|p| p.id());
if dest_parent_id != source_parent_id {
let dest_location_changed = dest.times.location_changed;
let source_location_changed = source.times.location_changed;
if let (Some(dlc), Some(slc)) = (dest_location_changed, source_location_changed) {
if slc > dlc {
let Some(parent_id) = source.parent().map(|p| p.id()) else {
log.warnings.push(format!("Cannot move root group {}", id,));
continue;
};
if !dest_groups.contains(&parent_id) {
log.warnings.push(format!(
"Cannot move group {} to group {} because the group does not exist in the destination database.",
id,
parent_id,
));
continue;
};
moves.push((id, parent_id));
dest.move_to(root_id)?;
dest.times.location_changed = Some(slc);
log.events.push(MergeEvent {
target: MergeEventTarget::Group(id),
event_type: MergeEventType::LocationUpdated,
});
}
} else {
log.warnings.push(format!(
"Cannot determine which group {} move is more recent because one of the groups does not have a location changed timestamp.",
id,
));
}
}
let dest_last_modification = dest.times.last_modification.unwrap_or_else(|| {
log.warnings.push(format!(
"Destination group {} did not have a last modification timestamp",
id
));
Times::now()
});
let source_last_modification = source.times.last_modification.unwrap_or_else(|| {
log.warnings.push(format!(
"Source group {} did not have a last modification timestamp",
id
));
Times::epoch()
});
if dest_last_modification == source_last_modification {
if have_groups_diverged(&dest, &source) {
return Err(MergeError::GroupModificationTimeNotUpdated(id));
}
continue;
}
if dest_last_modification > source_last_modification {
continue;
}
dest.name = source.name.clone();
dest.notes = source.notes.clone();
dest.icon = source.icon.clone();
dest.custom_data = source.custom_data.clone();
dest.times.last_modification = source.times.last_modification.or(dest.times.last_modification);
dest.is_expanded = source.is_expanded;
dest.default_autotype_sequence = source.default_autotype_sequence.clone();
dest.enable_autotype = source.enable_autotype;
dest.enable_searching = source.enable_searching;
dest.last_top_visible_entry = source.last_top_visible_entry;
log.events.push(MergeEvent {
target: MergeEventTarget::Group(id),
event_type: MergeEventType::Updated,
});
}
for (group_id, parent_id) in moves {
#[allow(clippy::unwrap_used)] let mut group = dest_db.group_mut(group_id).unwrap();
group.move_to(parent_id)?;
}
Ok(())
}
fn merge_entries(dest_db: &mut Database, source_db: &Database, log: &mut MergeLog) -> Result<(), MergeError> {
let dest_entries = dest_db.entries.keys().cloned().collect::<HashSet<_>>();
let source_entries = source_db.entries.keys().cloned().collect::<HashSet<_>>();
for &id in source_entries.difference(&dest_entries) {
#[allow(clippy::unwrap_used)] let source_entry = source_db.entry(id).unwrap();
if let Some(deletion_time) = dest_db.deleted_objects.get(&id.uuid()) {
let source_update_time = source_entry
.times
.last_modification
.or(source_entry.times.location_changed);
match (deletion_time, source_update_time) {
(Some(deletion_time), Some(source_update_time)) => {
if *deletion_time >= source_update_time {
continue;
}
}
(Some(_), None) => {
continue;
}
(None, Some(_)) => {
}
(None, None) => {
continue;
}
}
}
let parent_id = source_entry.parent().id();
let Some(mut parent) = dest_db.group_mut(parent_id) else {
log.warnings.push(format!(
"Cannot add entry {} because its parent group {} does not exist in the destination database.",
id, parent_id,
));
continue;
};
let mut entry = parent.add_entry_with_id(id);
*entry = source_entry.deref().clone();
log.events.push(MergeEvent {
target: MergeEventTarget::Entry(id),
event_type: MergeEventType::Created,
});
}
for &id in dest_entries.difference(&source_entries) {
#[allow(clippy::unwrap_used)] let dest_entry = dest_db.entry_mut(id).unwrap();
if let Some(deletion_time) = source_db.deleted_objects.get(&id.uuid()) {
let dest_update_time = dest_entry
.times
.last_modification
.or(dest_entry.times.location_changed);
if let (Some(deletion_time), Some(dest_update_time)) = (deletion_time, dest_update_time) {
if *deletion_time < dest_update_time {
continue;
}
}
dest_entry.remove();
dest_db.deleted_objects.insert(id.uuid(), *deletion_time);
log.events.push(MergeEvent {
target: MergeEventTarget::Entry(id),
event_type: MergeEventType::Deleted,
});
}
}
for &id in dest_entries.intersection(&source_entries) {
#[allow(clippy::unwrap_used)] let mut dest_entry = dest_db.entry_mut(id).unwrap();
#[allow(clippy::unwrap_used)] let source_entry = source_db.entry(id).unwrap();
let dest_parent_id = dest_entry.as_ref().parent().id();
let source_parent_id = source_entry.parent().id();
if dest_parent_id != source_parent_id {
let source_location_changed = source_entry.times.location_changed;
let dest_location_changed = dest_entry.times.location_changed;
if let (Some(slc), Some(dlc)) = (source_location_changed, dest_location_changed) {
if slc > dlc {
if dest_entry.move_to(source_parent_id).is_ok() {
log.events.push(MergeEvent {
target: MergeEventTarget::Entry(id),
event_type: MergeEventType::LocationUpdated,
});
dest_entry.times.location_changed = Some(slc);
} else {
log.warnings.push(format!(
"Cannot move entry {} to group {} because the group does not exist in the destination database.",
id,
source_parent_id,
));
}
}
} else {
log.warnings.push(format!(
"Cannot determine which entry {} move is more recent because one of the entries does not have a location changed timestamp.",
id,
));
}
}
let source_last_modification = source_entry.times.last_modification.unwrap_or_else(|| {
log.warnings.push(format!(
"Source entry {} did not have a last modification timestamp",
id
));
Times::epoch()
});
let dest_last_modification = dest_entry.times.last_modification.unwrap_or_else(|| {
log.warnings.push(format!(
"Destination entry {} did not have a last modification timestamp",
id
));
Times::now()
});
if dest_last_modification == source_last_modification {
if have_entries_diverged(&dest_entry, &source_entry) {
return Err(MergeError::EntryModificationTimeNotUpdated(id));
}
continue;
}
let source_history = source_entry.history.clone().unwrap_or_else(|| {
log.warnings.push(format!("Source entry {} had no history.", id));
History::default()
});
let dest_history = dest_entry.history.clone().unwrap_or_else(|| {
log.warnings
.push(format!("Destination entry {} had no history.", id));
History::default()
});
let mut merged_history = merge_history(&dest_history, &source_history, log)?;
let merged_location_timestamp = dest_entry
.times
.location_changed
.or(source_entry.times.location_changed);
if source_last_modification > dest_last_modification {
if let Some(last_history_entry) = merged_history.entries.first() {
if have_entries_diverged(&dest_entry, last_history_entry) {
let mut dest_entry_for_history = dest_entry.deref().clone();
dest_entry_for_history.history = None;
merged_history.add_entry(dest_entry_for_history);
}
}
dest_entry.times.last_modification = source_entry.times.last_modification;
dest_entry.fields = source_entry.fields.clone();
dest_entry.autotype = source_entry.autotype.clone();
dest_entry.tags = source_entry.tags.clone();
dest_entry.custom_data = source_entry.custom_data.clone();
dest_entry.icon = source_entry.icon.clone();
dest_entry.foreground_color = source_entry.foreground_color.clone();
dest_entry.background_color = source_entry.background_color.clone();
dest_entry.override_url = source_entry.override_url.clone();
dest_entry.quality_check = source_entry.quality_check;
log.events.push(MergeEvent {
target: MergeEventTarget::Entry(id),
event_type: MergeEventType::Updated,
});
}
dest_entry.history = Some(merged_history);
dest_entry.times.location_changed = merged_location_timestamp;
}
Ok(())
}
fn merge_history(dest: &History, source: &History, log: &mut MergeLog) -> Result<History, MergeError> {
let mut entries: Vec<Entry> = Vec::new();
let mut entries_dest: Vec<Entry> = dest.entries.to_vec();
let mut entries_source: Vec<Entry> = source.entries.to_vec();
for e in entries_dest.iter_mut() {
if e.times.last_modification.is_none() {
log.warnings.push(format!(
"Destination history entry {} did not have a last modification timestamp",
e.id()
));
e.times.last_modification = Some(Times::epoch());
}
}
for e in entries_source.iter_mut() {
if e.times.last_modification.is_none() {
log.warnings.push(format!(
"Source history entry {} did not have a last modification timestamp",
e.id()
));
e.times.last_modification = Some(Times::epoch());
}
}
entries_dest.sort_by_key(|e| e.times.last_modification);
entries_source.sort_by_key(|e| e.times.last_modification);
#[allow(clippy::unwrap_used)]
loop {
match (entries_dest.is_empty(), entries_source.is_empty()) {
(false, false) => {
let dest_entry = entries_dest.last().unwrap();
let source_entry = entries_source.last().unwrap();
let dest_time = dest_entry.times.last_modification.unwrap();
let source_time = source_entry.times.last_modification.unwrap();
if dest_time > source_time {
entries.push(entries_dest.pop().unwrap());
} else if source_time > dest_time {
entries.push(entries_source.pop().unwrap());
} else if have_entries_diverged(dest_entry, source_entry) {
log.warnings.push(format!(
"History entries for {} have the same modification timestamp {} but have diverged.",
dest_entry.id(),
source_time,
));
entries.push(entries_dest.pop().unwrap());
entries.push(entries_source.pop().unwrap());
} else {
entries.push(entries_dest.pop().unwrap());
entries_source.pop();
}
}
(true, false) => {
entries.push(entries_source.pop().unwrap());
}
(false, true) => {
entries.push(entries_dest.pop().unwrap());
}
(true, true) => break,
}
}
Ok(History { entries })
}
fn have_groups_diverged(a: &Group, b: &Group) -> bool {
let new_times = Times::default();
let mut a = a.clone();
a.times = new_times.clone();
a.entries.clear();
a.groups.clear();
a.parent = None;
let mut b = b.clone();
b.times = new_times.clone();
b.entries.clear();
b.groups.clear();
b.parent = None;
!a.eq(&b)
}
fn have_entries_diverged(a: &Entry, b: &Entry) -> bool {
let new_times = Times::default();
let mut a = a.clone();
a.times = new_times.clone();
a.history = None;
let mut b = b.clone();
b.times = new_times.clone();
b.history = None;
!a.eq(&b)
}
#[cfg(test)]
mod merge_tests {
use uuid::uuid;
use crate::db::{fields, EntryId, GroupId, History, Times};
use crate::Database;
const ROOT_GROUP_ID: GroupId = GroupId::from_uuid(uuid!("00000000-0000-0000-0000-000000000001"));
const GROUP1_ID: GroupId = GroupId::from_uuid(uuid!("00000000-0000-0000-0000-000000000002"));
const GROUP2_ID: GroupId = GroupId::from_uuid(uuid!("00000000-0000-0000-0000-000000000003"));
const SUBGROUP1_ID: GroupId = GroupId::from_uuid(uuid!("00000000-0000-0000-0000-000000000004"));
const SUBGROUP2_ID: GroupId = GroupId::from_uuid(uuid!("00000000-0000-0000-0000-000000000005"));
const ENTRY1_ID: EntryId = EntryId::from_uuid(uuid!("00000000-0000-0000-0000-000000000006"));
const ENTRY2_ID: EntryId = EntryId::from_uuid(uuid!("00000000-0000-0000-0000-000000000007"));
fn create_test_database() -> Database {
let mut db = Database::new_with_root_id(ROOT_GROUP_ID);
db.root_mut()
.add_group_with_id(GROUP1_ID)
.edit(|g| g.name = "group1".to_string())
.add_group_with_id(SUBGROUP1_ID)
.edit(|sg| sg.name = "subgroup1".to_string())
.add_entry_with_id(ENTRY2_ID)
.edit(|e| e.set_unprotected("Title", "entry2"));
db.root_mut()
.add_group_with_id(GROUP2_ID)
.edit(|g| g.name = "group2".to_string())
.add_group_with_id(SUBGROUP2_ID)
.edit(|sg| sg.name = "subgroup2".to_string());
db.root_mut()
.add_entry_with_id(ENTRY1_ID)
.edit(|e| e.set_unprotected("Title", "entry1"));
db
}
fn sleep() {
std::thread::sleep(std::time::Duration::from_secs(1));
}
fn assert_history_ordered(history: &History) {
let mut last_modification_time: Option<&chrono::NaiveDateTime> = None;
for entry in &history.entries {
if last_modification_time.is_none() {
last_modification_time = entry.times.last_modification.as_ref();
}
if let Some(entry_modification_time) = entry.times.last_modification.as_ref() {
if last_modification_time.unwrap() < entry_modification_time {
panic!(
"History entries are not ordered by last modification time: {:?} came after {:?}",
last_modification_time, entry_modification_time
);
}
last_modification_time = Some(entry_modification_time);
}
}
}
#[test]
fn test_idempotence() {
let mut destination_db = create_test_database();
let source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 0);
assert_eq!(destination_db.root().entries().count(), 1);
assert_eq!(destination_db.root().groups().count(), 2);
assert_eq!(destination_db.entries.len(), entry_count_before);
assert_eq!(destination_db.groups.len(), group_count_before);
assert_eq!(destination_db, source_db);
sleep();
destination_db
.entry_mut(ENTRY1_ID)
.unwrap()
.edit_tracking(|e| e.set_unprotected("Title", "entry1_updated"));
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 0);
let destination_db_just_after_merge = destination_db.clone();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 0);
assert_eq!(destination_db_just_after_merge, destination_db);
}
#[test]
fn test_add_new_entry() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
let new_entry_id = source_db
.root_mut()
.add_entry()
.edit_tracking(|e| e.set_unprotected("Title", "new_entry"))
.id();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 1);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before + 1);
assert_eq!(group_count_after, group_count_before);
let root_entries_count = destination_db.root().entries().count();
assert_eq!(root_entries_count, 2);
let new_entry = destination_db
.entry(new_entry_id)
.expect("New entry should exist");
assert_eq!(new_entry.get(fields::TITLE), Some("new_entry"));
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 0);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before + 1);
assert_eq!(group_count_after, group_count_before);
let root_entries_count = destination_db.root().entries().count();
assert_eq!(root_entries_count, 2);
}
#[test]
fn test_deleted_entry_in_destination() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
let deleted_entry_id = source_db
.root_mut()
.add_entry()
.edit_tracking(|e| {
e.set_unprotected("Title", "deleted_entry");
})
.id();
destination_db
.deleted_objects
.insert(deleted_entry_id.uuid(), Some(Times::now()));
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 0);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before);
assert!(destination_db.entry(deleted_entry_id).is_none());
}
#[test]
fn test_updated_entry_under_deleted_group() {
let mut destination_db = create_test_database();
let modified_entry_id = destination_db
.root_mut()
.add_entry()
.edit(|e| e.set_unprotected("Title", "original_title"))
.id();
let deleted_group_id = destination_db
.root_mut()
.add_group()
.edit(|g| g.name = "deleted_group".to_string())
.id();
let mut source_db = destination_db.clone();
sleep();
source_db
.entry_mut(modified_entry_id)
.unwrap()
.track_changes()
.edit(|e| {
e.set_unprotected("Title", "modified_title");
})
.move_to(deleted_group_id)
.unwrap();
sleep();
destination_db.group_mut(deleted_group_id).unwrap().remove();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 3);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before + 1);
assert!(destination_db.group(deleted_group_id).is_some());
assert!(destination_db.entry(modified_entry_id).is_some());
}
#[test]
fn test_deleted_group_in_destination() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
let deleted_group_id = source_db
.root_mut()
.add_group()
.edit(|g| g.name = "deleted_group".to_string())
.id();
destination_db
.deleted_objects
.insert(deleted_group_id.uuid(), Some(Times::now()));
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 0);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before);
assert!(destination_db.group(deleted_group_id).is_none());
}
#[test]
fn test_deleted_entry_in_source() {
let mut destination_db = create_test_database();
let deleted_entry_id = destination_db
.root_mut()
.add_entry()
.edit_tracking(|e| e.set_unprotected("Title", "deleted_entry"))
.id();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
source_db
.entry_mut(deleted_entry_id)
.unwrap()
.track_changes()
.remove();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 1);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before - 1);
assert_eq!(group_count_after, group_count_before);
assert!(destination_db.entry(deleted_entry_id).is_none());
assert!(destination_db
.deleted_objects
.contains_key(&deleted_entry_id.uuid()));
}
#[test]
fn test_deleted_group_in_source() {
let mut destination_db = create_test_database();
let deleted_group_id = destination_db
.root_mut()
.add_group()
.edit(|g| g.name = "deleted_group".to_string())
.id();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
source_db
.group_mut(deleted_group_id)
.unwrap()
.track_changes()
.remove()
.unwrap();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 1);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before - 1);
assert!(destination_db.group(deleted_group_id).is_none());
assert!(destination_db
.deleted_objects
.contains_key(&deleted_group_id.uuid()));
}
#[test]
fn test_deleted_entry_in_source_modified_in_destination() {
let mut destination_db = create_test_database();
let deleted_entry_id = destination_db
.root_mut()
.add_entry()
.edit_tracking(|e| e.set_unprotected("Title", "deleted_entry"))
.id();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
let mut source_db = destination_db.clone();
source_db
.entry_mut(deleted_entry_id)
.unwrap()
.track_changes()
.remove();
sleep();
destination_db
.entry_mut(deleted_entry_id)
.unwrap()
.edit_tracking(|e| e.set_unprotected("Title", "modified_in_destination"));
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 0);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before);
assert!(destination_db.entry(deleted_entry_id).is_some());
assert!(!destination_db
.deleted_objects
.contains_key(&deleted_entry_id.uuid()));
}
#[test]
fn test_group_subtree_deletion() {
let mut destination_db = create_test_database();
let deleted_group_id = destination_db
.root_mut()
.add_group()
.edit(|g| {
g.name = "deleted_group".to_string();
})
.id();
let deleted_subgroup_id = destination_db
.group_mut(deleted_group_id)
.unwrap()
.add_group()
.edit(|g| {
g.name = "deleted_subgroup".to_string();
})
.id();
let deleted_entry_id = destination_db
.group_mut(deleted_subgroup_id)
.unwrap()
.add_entry()
.edit_tracking(|e| {
e.set_unprotected("Title", "deleted_entry");
})
.id();
let mut source_db = destination_db.clone();
source_db
.root_mut()
.group_mut(deleted_group_id)
.unwrap()
.track_changes()
.remove()
.unwrap();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 3);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before - 1);
assert_eq!(group_count_after, group_count_before - 2);
assert!(destination_db.entry(deleted_entry_id).is_none());
assert!(destination_db.group(deleted_subgroup_id).is_none());
assert!(destination_db.group(deleted_group_id).is_none());
assert!(destination_db
.deleted_objects
.contains_key(&deleted_entry_id.uuid()));
assert!(destination_db
.deleted_objects
.contains_key(&deleted_subgroup_id.uuid()));
assert!(destination_db
.deleted_objects
.contains_key(&deleted_group_id.uuid()));
}
#[test]
fn test_group_subtree_partial_deletion() {
let mut destination_db = create_test_database();
let deleted_group_id = destination_db
.root_mut()
.add_group()
.edit(|g| {
g.name = "deleted_group".to_string();
})
.id();
let deleted_subgroup_id = destination_db
.group_mut(deleted_group_id)
.unwrap()
.add_group()
.edit(|g| {
g.name = "deleted_subgroup".to_string();
})
.id();
let deleted_entry_id = destination_db
.group_mut(deleted_subgroup_id)
.unwrap()
.add_entry()
.edit(|e| {
e.set_unprotected("Title", "deleted_entry");
})
.id();
let mut source_db = destination_db.clone();
sleep();
source_db
.group_mut(deleted_group_id)
.unwrap()
.track_changes()
.remove()
.unwrap();
sleep();
destination_db
.group_mut(deleted_group_id)
.unwrap()
.track_changes()
.edit(|g| {
g.notes = Some("modified in destination".to_string());
});
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 2);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before - 1);
assert_eq!(group_count_after, group_count_before - 1);
assert!(destination_db.entry(deleted_entry_id).is_none());
assert!(destination_db.group(deleted_subgroup_id).is_none());
assert!(destination_db.group(deleted_group_id).is_some());
assert!(destination_db
.deleted_objects
.contains_key(&deleted_entry_id.uuid()));
assert!(destination_db
.deleted_objects
.contains_key(&deleted_subgroup_id.uuid()));
assert!(!destination_db
.deleted_objects
.contains_key(&deleted_group_id.uuid()));
}
#[test]
fn test_deleted_group_in_source_modified_in_destination() {
let mut destination_db = create_test_database();
let deleted_group_id = destination_db
.root_mut()
.add_group()
.edit(|g| g.name = "deleted_group".to_string())
.id();
let mut source_db = destination_db.clone();
source_db
.group_mut(deleted_group_id)
.unwrap()
.track_changes()
.remove()
.unwrap();
sleep();
destination_db
.group_mut(deleted_group_id)
.unwrap()
.track_changes()
.edit(|g| g.notes = Some("modified_in_destination".to_string()));
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 0);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before);
assert!(destination_db.group(deleted_group_id).is_some());
assert!(!destination_db
.deleted_objects
.contains_key(&deleted_group_id.uuid()));
}
#[test]
fn test_deleted_group_has_new_entries() {
let mut destination_db = create_test_database();
let deleted_group_id = destination_db
.root_mut()
.add_group()
.edit(|g| g.name = "deleted_group".to_string())
.id();
let mut source_db = destination_db.clone();
source_db
.group_mut(deleted_group_id)
.unwrap()
.track_changes()
.remove()
.unwrap();
sleep();
let new_entry_id = destination_db
.group_mut(deleted_group_id)
.unwrap()
.add_entry()
.edit_tracking(|e| {
e.set_unprotected("Title", "new_entry_in_deleted_group");
})
.id();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 0);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before);
assert!(destination_db.group(deleted_group_id).is_some());
assert!(destination_db.entry(new_entry_id).is_some());
assert!(!destination_db
.deleted_objects
.contains_key(&deleted_group_id.uuid()));
assert!(!destination_db.deleted_objects.contains_key(&new_entry_id.uuid()));
}
#[test]
fn test_add_new_non_root_entry() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
let new_entry_id = source_db
.group_mut(GROUP1_ID)
.unwrap()
.add_entry()
.edit_tracking(|e| {
e.set_unprotected("Title", "new_entry");
})
.id();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 1);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before + 1);
assert_eq!(group_count_after, group_count_before);
assert!(destination_db.entry(new_entry_id).is_some());
}
#[test]
fn test_add_new_entry_new_group() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
let new_group_id = source_db
.root_mut()
.add_group()
.edit(|g| g.name = "new_group".to_string())
.id();
let new_subgroup_id = source_db
.group_mut(new_group_id)
.unwrap()
.add_group()
.edit(|g| g.name = "new_subgroup".to_string())
.id();
let new_entry_id = source_db
.group_mut(new_subgroup_id)
.unwrap()
.add_entry()
.edit_tracking(|e| {
e.set_unprotected("Title", "new_entry");
})
.id();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 3);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before + 1);
assert_eq!(group_count_after, group_count_before + 2);
assert!(destination_db.group(new_group_id).is_some());
assert!(destination_db.group(new_subgroup_id).is_some());
assert!(destination_db.entry(new_entry_id).is_some());
}
#[test]
fn test_entry_relocation_existing_group() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
sleep();
source_db
.entry_mut(ENTRY2_ID)
.unwrap()
.track_changes()
.move_to(GROUP2_ID)
.expect("move successful");
let location_changed_timestamp = source_db
.entry(ENTRY2_ID)
.unwrap()
.times
.location_changed
.unwrap();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 1);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(group_count_after, group_count_before);
assert_eq!(entry_count_after, entry_count_before);
assert!(destination_db.entry(ENTRY2_ID).is_some());
let entry = destination_db.entry(ENTRY2_ID).unwrap();
assert_eq!(entry.parent().id(), GROUP2_ID);
assert_eq!(entry.times.location_changed, Some(location_changed_timestamp));
}
#[test]
fn test_entry_relocation_and_update() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
sleep();
source_db.entry_mut(ENTRY2_ID).unwrap().edit_tracking(|e| {
e.set_unprotected("Title", "entry2_modified_in_source");
});
source_db
.entry_mut(ENTRY2_ID)
.unwrap()
.track_changes()
.move_to(GROUP2_ID)
.expect("move successful");
let location_changed_timestamp = source_db
.entry(ENTRY2_ID)
.unwrap()
.times
.location_changed
.unwrap();
sleep();
destination_db.entry_mut(ENTRY2_ID).unwrap().edit_tracking(|e| {
e.set_unprotected("Title", "entry2_modified_in_destination");
});
let entry_modified_timestamp = destination_db
.entry(ENTRY2_ID)
.unwrap()
.times
.last_modification
.unwrap();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 1);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(group_count_after, group_count_before);
assert_eq!(entry_count_after, entry_count_before);
assert!(destination_db.entry(ENTRY2_ID).is_some());
let entry = destination_db.entry(ENTRY2_ID).unwrap();
assert_eq!(entry.parent().id(), GROUP2_ID);
assert_eq!(entry.times.location_changed, Some(location_changed_timestamp));
assert_eq!(entry.get(fields::TITLE), Some("entry2_modified_in_destination"));
assert_eq!(entry.times.last_modification, Some(entry_modified_timestamp));
}
#[test]
fn test_entry_relocation_in_destination_and_update() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
sleep();
source_db.entry_mut(ENTRY2_ID).unwrap().edit_tracking(|e| {
e.set_unprotected(fields::TITLE, "entry2_modified_in_source");
});
let entry_modified_timestamp = source_db
.entry(ENTRY2_ID)
.unwrap()
.times
.last_modification
.unwrap();
destination_db
.entry_mut(ENTRY2_ID)
.unwrap()
.track_changes()
.move_to(GROUP2_ID)
.expect("move successful");
let location_changed_timestamp = destination_db
.entry(ENTRY2_ID)
.unwrap()
.times
.location_changed
.unwrap();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 1);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(group_count_after, group_count_before);
assert_eq!(entry_count_after, entry_count_before);
assert!(destination_db.entry(ENTRY2_ID).is_some());
let entry = destination_db.entry(ENTRY2_ID).unwrap();
assert_eq!(entry.parent().id(), GROUP2_ID);
assert_eq!(entry.times.location_changed, Some(location_changed_timestamp));
assert_eq!(entry.get(fields::TITLE), Some("entry2_modified_in_source"));
assert_eq!(entry.times.last_modification, Some(entry_modified_timestamp));
}
#[test]
fn test_entry_relocation_new_group() {
let mut destination_db = create_test_database();
let new_entry_id = destination_db
.root_mut()
.add_entry()
.edit(|e| {
e.set_unprotected("Title", "new_entry");
})
.id();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
let mut source_db = destination_db.clone();
let new_group_id = source_db
.root_mut()
.add_group()
.edit(|g| g.name = "new_group".to_string())
.id();
sleep();
source_db.entry_mut(new_entry_id).unwrap().edit_tracking(|e| {
e.set_unprotected("Title", "new_entry_modified_in_source");
});
source_db
.entry_mut(new_entry_id)
.unwrap()
.track_changes()
.move_to(new_group_id)
.expect("move successful");
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 3);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before + 1);
assert!(destination_db.entry(new_entry_id).is_some());
let entry = destination_db.entry(new_entry_id).unwrap();
assert_eq!(entry.parent().id(), new_group_id);
assert_eq!(entry.get(fields::TITLE), Some("new_entry_modified_in_source"));
}
#[test]
fn test_group_relocation() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
sleep();
source_db
.group_mut(SUBGROUP1_ID)
.unwrap()
.track_changes()
.move_to(GROUP2_ID)
.expect("move successful");
let location_changed_timestamp = source_db
.group(SUBGROUP1_ID)
.unwrap()
.times
.location_changed
.unwrap();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 1);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before);
assert!(destination_db.group(SUBGROUP1_ID).is_some());
assert!(destination_db.entry(ENTRY2_ID).is_some());
let group = destination_db.group(SUBGROUP1_ID).unwrap();
assert_eq!(group.parent().unwrap().id(), GROUP2_ID);
assert_eq!(group.times.location_changed, Some(location_changed_timestamp));
}
#[test]
fn test_update_in_destination_no_conflict() {
let mut destination_db = create_test_database();
let source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
sleep();
destination_db.entry_mut(ENTRY1_ID).unwrap().edit_tracking(|e| {
e.set_unprotected("Title", "entry1_updated");
});
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 0);
let merged_history = destination_db.entry(ENTRY1_ID).unwrap().history.clone().unwrap();
assert_history_ordered(&merged_history);
assert_eq!(merged_history.entries.len(), 1);
let merged_entry = &merged_history.entries[0];
assert_eq!(merged_entry.get(fields::TITLE), Some("entry1"));
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before);
assert_eq!(
destination_db.entry(ENTRY1_ID).unwrap().get(fields::TITLE),
Some("entry1_updated")
);
}
#[test]
fn test_update_in_source_no_conflict() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
sleep();
source_db.entry_mut(ENTRY1_ID).unwrap().edit_tracking(|e| {
e.set_unprotected("Title", "entry1_updated");
});
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 1);
let merged_history = destination_db.entry(ENTRY1_ID).unwrap().history.clone().unwrap();
assert_history_ordered(&merged_history);
assert_eq!(merged_history.entries.len(), 1);
let merged_entry = &merged_history.entries[0];
assert_eq!(merged_entry.get(fields::TITLE), Some("entry1"));
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before);
assert_eq!(
destination_db.entry(ENTRY1_ID).unwrap().get(fields::TITLE),
Some("entry1_updated")
);
}
#[test]
fn test_update_with_conflicts() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
sleep();
destination_db.entry_mut(ENTRY1_ID).unwrap().edit_tracking(|e| {
e.set_unprotected("Title", "entry1_updated_from_destination");
});
sleep();
source_db.entry_mut(ENTRY1_ID).unwrap().edit_tracking(|e| {
e.set_unprotected("Title", "entry1_updated_from_source");
});
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 1);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before);
let entry = destination_db.entry(ENTRY1_ID).unwrap();
assert_eq!(entry.get(fields::TITLE), Some("entry1_updated_from_source"));
let merged_history = entry.history.clone().unwrap();
assert_history_ordered(&merged_history);
assert_eq!(merged_history.entries.len(), 2);
assert_eq!(
merged_history.entries[0].get(fields::TITLE),
Some("entry1_updated_from_destination")
);
assert_eq!(merged_history.entries[1].get(fields::TITLE), Some("entry1"));
let merge_result = destination_db.merge(&destination_db.clone()).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 0);
}
#[test]
fn test_group_update_in_source() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
sleep();
source_db.group_mut(SUBGROUP1_ID).unwrap().edit_tracking(|g| {
g.name = "subgroup1_updated_name".to_string();
});
let modification_timestamp = source_db
.group(SUBGROUP1_ID)
.unwrap()
.times
.last_modification
.unwrap();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 1);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before);
assert!(destination_db.group(SUBGROUP1_ID).is_some());
assert_eq!(
destination_db.group(SUBGROUP1_ID).unwrap().name,
"subgroup1_updated_name"
);
assert_eq!(
destination_db
.group(SUBGROUP1_ID)
.unwrap()
.times
.last_modification,
Some(modification_timestamp)
);
}
#[test]
fn test_group_update_in_destination() {
let mut destination_db = create_test_database();
let source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
sleep();
destination_db
.group_mut(SUBGROUP1_ID)
.unwrap()
.edit_tracking(|g| {
g.name = "subgroup1_updated_name".to_string();
});
let last_modification = destination_db
.group(SUBGROUP1_ID)
.unwrap()
.times
.last_modification
.unwrap();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 0);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before);
assert!(destination_db.group(SUBGROUP1_ID).is_some());
assert_eq!(
destination_db.group(SUBGROUP1_ID).unwrap().name,
"subgroup1_updated_name"
);
assert_eq!(
destination_db
.group(SUBGROUP1_ID)
.unwrap()
.times
.last_modification,
Some(last_modification)
);
}
#[test]
fn test_group_update_and_relocation() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
sleep();
source_db
.group_mut(SUBGROUP1_ID)
.unwrap()
.track_changes()
.edit(|g| {
g.name = "subgroup1_updated_name".to_string();
})
.move_to(GROUP2_ID)
.expect("move successful");
let modification_timestamp = source_db
.group(SUBGROUP1_ID)
.unwrap()
.times
.last_modification
.unwrap();
let location_changed_timestamp = source_db
.group(SUBGROUP1_ID)
.unwrap()
.times
.location_changed
.unwrap();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 2);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before);
assert!(destination_db.group(SUBGROUP1_ID).is_some());
let group = destination_db.group(SUBGROUP1_ID).unwrap();
assert_eq!(group.name, "subgroup1_updated_name");
assert_eq!(group.parent().unwrap().id(), GROUP2_ID);
assert_eq!(group.times.last_modification, Some(modification_timestamp));
assert_eq!(group.times.location_changed, Some(location_changed_timestamp));
}
#[test]
fn test_group_update_in_destination_and_relocation_in_source() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
let entry_count_before = destination_db.entries.len();
let group_count_before = destination_db.groups.len();
sleep();
source_db.group_mut(SUBGROUP1_ID).unwrap().edit_tracking(|g| {
g.name = "subgroup1_updated_name".to_string();
});
let modification_timestamp = source_db
.group(SUBGROUP1_ID)
.unwrap()
.times
.last_modification
.unwrap();
destination_db
.group_mut(SUBGROUP1_ID)
.unwrap()
.track_changes()
.move_to(GROUP2_ID)
.expect("move successful");
let location_changed_timestamp = destination_db
.group(SUBGROUP1_ID)
.unwrap()
.times
.location_changed
.unwrap();
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 0);
assert_eq!(merge_result.events.len(), 1);
let entry_count_after = destination_db.entries.len();
let group_count_after = destination_db.groups.len();
assert_eq!(entry_count_after, entry_count_before);
assert_eq!(group_count_after, group_count_before);
assert!(destination_db.group(SUBGROUP1_ID).is_some());
let group = destination_db.group(SUBGROUP1_ID).unwrap();
assert_eq!(group.name, "subgroup1_updated_name");
assert_eq!(group.parent().unwrap().id(), GROUP2_ID);
assert_eq!(group.times.last_modification, Some(modification_timestamp));
assert_eq!(group.times.location_changed, Some(location_changed_timestamp));
}
#[test]
fn test_merge_untracked_group_history() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
source_db
.group_mut(GROUP1_ID)
.unwrap()
.edit(|g| {
g.name = "group1_updated_name".to_string();
})
.move_to(GROUP2_ID)
.expect("move successful");
assert_eq!(
destination_db.group(GROUP1_ID).unwrap().times,
source_db.group(GROUP1_ID).unwrap().times
);
assert!(destination_db.merge(&source_db).is_err());
destination_db
.group_mut(GROUP1_ID)
.unwrap()
.times
.last_modification = None;
destination_db
.group_mut(GROUP1_ID)
.unwrap()
.times
.location_changed = None;
source_db.group_mut(GROUP1_ID).unwrap().times.last_modification = None;
source_db.group_mut(GROUP1_ID).unwrap().times.location_changed = None;
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 3);
assert_eq!(merge_result.events.len(), 0);
}
#[test]
fn test_merge_untracked_entry_history() {
let mut destination_db = create_test_database();
let mut source_db = destination_db.clone();
source_db
.entry_mut(ENTRY1_ID)
.unwrap()
.edit(|e| {
e.set_unprotected("Title", "entry1_updated_title");
})
.move_to(GROUP2_ID)
.expect("move successful");
assert_eq!(
destination_db.entry(ENTRY1_ID).unwrap().times,
source_db.entry(ENTRY1_ID).unwrap().times
);
assert!(destination_db.merge(&source_db).is_err());
destination_db
.entry_mut(ENTRY1_ID)
.unwrap()
.times
.last_modification = None;
destination_db
.entry_mut(ENTRY1_ID)
.unwrap()
.times
.location_changed = None;
source_db.entry_mut(ENTRY1_ID).unwrap().times.last_modification = None;
source_db.entry_mut(ENTRY1_ID).unwrap().times.location_changed = None;
let merge_result = destination_db.merge(&source_db).unwrap();
assert_eq!(merge_result.warnings.len(), 3);
assert_eq!(merge_result.events.len(), 0);
}
}