mod fixtures;
mod revision;
use super::pass::run_mirror_sync;
use crate::{
bridge::library::mirror::options::{MirrorOptions, MirrorOutcome, mirror_source_id},
settings::WorkerSettings,
};
use fixtures::{FakeTransport, design_record, design_summary, temp_db};
use indicatrix_net::library::{AttachedFileMeta, DesignRecord, DesignSummary};
use indicatrix_vault::model::{
detail::FacetingDiagramDetail, entry::FacetingDiagramEntry, file::AttachedFile,
};
use std::sync::atomic::{AtomicBool, Ordering};
#[test]
fn a_local_only_design_survives_a_mirror_sync() {
let (db, path) = temp_db();
let local_entry_id = {
let guard = db.lock().unwrap();
guard
.save_diagram_entry(
&FacetingDiagramEntry {
title: "My Own Trichecker".to_string(),
url: "local://my_trichecker.asc".to_string(),
design_id: String::new(),
},
indicatrix_vault::local::LOCAL_SOURCE_ID,
)
.unwrap()
};
{
let guard = db.lock().unwrap();
guard
.save_diagram_detail(
&FacetingDiagramDetail {
shape: Some("Trichecker".to_string()),
attached_files: vec![AttachedFile {
name: "my_trichecker.asc".to_string(),
url: String::new(),
content: b"a real user file".to_vec(),
}],
..FacetingDiagramDetail::default()
},
local_entry_id,
)
.unwrap();
}
let remote_summary = design_summary(1, "Round Brilliant", "https://example.test/1", "v1");
let remote_design = design_record(1, "Round Brilliant", "https://example.test/1", "v1");
let transport = FakeTransport::new(vec![remote_summary], vec![remote_design]);
let outcome = run_mirror_sync(
&db,
&transport,
"remote-library:example.test:9443",
MirrorOptions::default(),
&AtomicBool::new(false),
|_| {},
);
assert!(matches!(outcome, MirrorOutcome::Completed(c) if c.new_count == 1));
let guard = db.lock().unwrap();
let local_full = guard.get_diagram_full(local_entry_id).unwrap().unwrap();
assert_eq!(local_full.title, "My Own Trichecker");
assert_eq!(local_full.url, "local://my_trichecker.asc");
assert_eq!(local_full.attached_files.len(), 1);
assert_eq!(local_full.attached_files[0].content, b"a real user file");
assert_eq!(guard.get_total_count().unwrap(), 2);
drop(guard);
std::fs::remove_file(&path).ok();
}
#[test]
fn a_remote_design_colliding_with_an_unmirrored_local_urls_identity_is_left_untouched() {
let (db, path) = temp_db();
let collision_url = "local://collide.asc";
let local_entry_id = {
let guard = db.lock().unwrap();
guard
.save_diagram_entry(
&FacetingDiagramEntry {
title: "Local Original".to_string(),
url: collision_url.to_string(),
design_id: String::new(),
},
indicatrix_vault::local::LOCAL_SOURCE_ID,
)
.unwrap()
};
{
let guard = db.lock().unwrap();
guard
.save_diagram_detail(
&FacetingDiagramDetail {
attached_files: vec![AttachedFile {
name: "collide.asc".to_string(),
url: String::new(),
content: b"original local bytes".to_vec(),
}],
..FacetingDiagramDetail::default()
},
local_entry_id,
)
.unwrap();
}
let remote_summary = design_summary(1, "Remote Version", collision_url, "v1");
let remote_design = design_record(1, "Remote Version", collision_url, "v1");
let transport = FakeTransport::new(vec![remote_summary], vec![remote_design]);
let outcome = run_mirror_sync(
&db,
&transport,
"remote-library:w",
MirrorOptions::default(),
&AtomicBool::new(false),
|_| {},
);
let MirrorOutcome::Completed(counts) = outcome else {
panic!("expected Completed, got {outcome:?}");
};
assert_eq!(counts.local_conflicts_skipped, 1);
assert_eq!(counts.new_count, 0);
assert_eq!(counts.updated_count, 0);
assert_eq!(counts.failed, 0);
let guard = db.lock().unwrap();
assert_eq!(
guard.get_total_count().unwrap(),
1,
"the collision must never be inserted as a second row"
);
let local_full = guard.get_diagram_full(local_entry_id).unwrap().unwrap();
assert_eq!(local_full.title, "Local Original");
assert_eq!(local_full.attached_files.len(), 1);
assert_eq!(
local_full.attached_files[0].content,
b"original local bytes"
);
assert_eq!(
guard.get_mirror_state(collision_url).unwrap(),
None,
"a skipped conflict must never be marked as synced"
);
drop(guard);
std::fs::remove_file(&path).ok();
}
#[test]
fn a_new_design_is_saved_with_its_attachment() {
let (db, path) = temp_db();
let mut record = design_record(1, "Round Brilliant", "https://example.test/1", "v1");
record.attachments = vec![AttachedFileMeta {
id: 100,
name: "schedule.pdf".to_string(),
url: "https://example.test/schedule.pdf".to_string(),
size: 5,
}];
let summary = design_summary(1, "Round Brilliant", "https://example.test/1", "v1");
let transport = FakeTransport::new(vec![summary], vec![record]).with_attachment(
100,
"schedule.pdf",
vec![1, 2, 3, 4, 5],
);
let outcome = run_mirror_sync(
&db,
&transport,
"remote-library:w",
MirrorOptions::default(),
&AtomicBool::new(false),
|_| {},
);
let MirrorOutcome::Completed(counts) = outcome else {
panic!("expected Completed, got {outcome:?}");
};
assert_eq!(counts.new_count, 1);
assert_eq!(counts.attachments_fetched, 1);
assert_eq!(counts.attachment_bytes_fetched, 5);
let guard = db.lock().unwrap();
let full = guard
.search_diagrams(
"",
"All",
"All",
&indicatrix_vault::model::filter::RangeFilter::default(),
)
.unwrap();
assert_eq!(full.len(), 1);
let entry_id = full[0].id;
let record = guard.get_diagram_full(entry_id).unwrap().unwrap();
assert_eq!(record.attached_files.len(), 1);
assert_eq!(record.attached_files[0].content, vec![1, 2, 3, 4, 5]);
drop(guard);
std::fs::remove_file(&path).ok();
}
#[test]
fn a_synced_designs_citation_and_shape_category_fields_are_saved_locally() {
let (db, path) = temp_db();
let mut record = design_record(1, "Competition Entry", "https://example.test/1", "v1");
record.source_citation = Some("Lapidary Journal, May 1994, p95".to_string());
record.pdf_file = Some("2002SSCMasters.pdf".to_string());
record.gem_file = Some("USFG-SSC-2020-Novice-1.gem".to_string());
record.shape_category = Some("5".to_string());
let summary = design_summary(1, "Competition Entry", "https://example.test/1", "v1");
let transport = FakeTransport::new(vec![summary], vec![record]);
let outcome = run_mirror_sync(
&db,
&transport,
"remote-library:w",
MirrorOptions::default(),
&AtomicBool::new(false),
|_| {},
);
assert!(matches!(outcome, MirrorOutcome::Completed(c) if c.new_count == 1));
let guard = db.lock().unwrap();
let items = guard
.search_diagrams(
"",
"All",
"All",
&indicatrix_vault::model::filter::RangeFilter::default(),
)
.unwrap();
let full = guard.get_diagram_full(items[0].id).unwrap().unwrap();
assert_eq!(
full.source_citation.as_deref(),
Some("Lapidary Journal, May 1994, p95")
);
assert_eq!(full.pdf_file.as_deref(), Some("2002SSCMasters.pdf"));
assert_eq!(full.gem_file.as_deref(), Some("USFG-SSC-2020-Novice-1.gem"));
assert_eq!(full.shape_category.as_deref(), Some("5"));
drop(guard);
std::fs::remove_file(&path).ok();
}
#[test]
fn a_second_sync_of_an_unchanged_catalogue_skips_every_design_and_makes_no_fetch_design_calls() {
let (db, path) = temp_db();
let summary = design_summary(1, "Round Brilliant", "https://example.test/1", "v1");
let record = design_record(1, "Round Brilliant", "https://example.test/1", "v1");
let transport = FakeTransport::new(vec![summary], vec![record]);
let first = run_mirror_sync(
&db,
&transport,
"remote-library:w",
MirrorOptions::default(),
&AtomicBool::new(false),
|_| {},
);
assert!(matches!(first, MirrorOutcome::Completed(c) if c.new_count == 1));
assert_eq!(transport.fetch_design_calls.load(Ordering::Relaxed), 1);
let second = run_mirror_sync(
&db,
&transport,
"remote-library:w",
MirrorOptions::default(),
&AtomicBool::new(false),
|_| {},
);
let MirrorOutcome::Completed(counts) = second else {
panic!("expected Completed, got {second:?}");
};
assert_eq!(counts.skipped_unchanged, 1);
assert_eq!(counts.new_count, 0);
assert_eq!(counts.updated_count, 0);
assert_eq!(transport.search_calls.load(Ordering::Relaxed), 2);
assert_eq!(transport.fetch_design_calls.load(Ordering::Relaxed), 1);
assert_eq!(transport.fetch_attachment_calls.load(Ordering::Relaxed), 0);
std::fs::remove_file(&path).ok();
}
#[test]
fn a_changed_design_is_refetched_and_updated_in_place() {
let (db, path) = temp_db();
let summary_v1 = design_summary(1, "Round Brilliant", "https://example.test/1", "v1");
let record_v1 = design_record(1, "Round Brilliant", "https://example.test/1", "v1");
let transport_v1 = FakeTransport::new(vec![summary_v1], vec![record_v1]);
let _ = run_mirror_sync(
&db,
&transport_v1,
"remote-library:w",
MirrorOptions::default(),
&AtomicBool::new(false),
|_| {},
);
let summary_v2 = design_summary(1, "Round Brilliant", "https://example.test/1", "v2-updated");
let record_v2 = design_record(1, "Round Brilliant", "https://example.test/1", "v2-updated");
let transport_v2 = FakeTransport::new(vec![summary_v2], vec![record_v2]);
let outcome = run_mirror_sync(
&db,
&transport_v2,
"remote-library:w",
MirrorOptions::default(),
&AtomicBool::new(false),
|_| {},
);
let MirrorOutcome::Completed(counts) = outcome else {
panic!("expected Completed, got {outcome:?}");
};
assert_eq!(counts.updated_count, 1);
assert_eq!(counts.new_count, 0);
let guard = db.lock().unwrap();
let items = guard
.search_diagrams(
"",
"All",
"All",
&indicatrix_vault::model::filter::RangeFilter::default(),
)
.unwrap();
assert_eq!(
items.len(),
1,
"must update the existing row, not add a second one"
);
assert_eq!(items[0].lw_ratio.as_deref(), Some("v2-updated"));
drop(guard);
std::fs::remove_file(&path).ok();
}
#[test]
fn cancelling_mid_sync_leaves_earlier_designs_committed_and_the_rest_untouched() {
let (db, path) = temp_db();
let summaries = vec![
design_summary(1, "First", "https://example.test/1", "v1"),
design_summary(2, "Second", "https://example.test/2", "v1"),
design_summary(3, "Third", "https://example.test/3", "v1"),
];
let designs = vec![
design_record(1, "First", "https://example.test/1", "v1"),
design_record(2, "Second", "https://example.test/2", "v1"),
design_record(3, "Third", "https://example.test/3", "v1"),
];
let transport = FakeTransport::new(summaries, designs);
let cancel = AtomicBool::new(false);
let mut processed_count = 0;
let outcome = run_mirror_sync(
&db,
&transport,
"remote-library:w",
MirrorOptions::default(),
&cancel,
|progress| {
processed_count = progress.processed;
if progress.processed == 1 {
cancel.store(true, Ordering::Relaxed);
}
},
);
assert_eq!(processed_count, 1);
let MirrorOutcome::Cancelled(counts) = outcome else {
panic!("expected Cancelled, got {outcome:?}");
};
assert_eq!(counts.new_count, 1);
let guard = db.lock().unwrap();
assert_eq!(guard.get_total_count().unwrap(), 1);
assert_eq!(transport.fetch_design_calls.load(Ordering::Relaxed), 1);
assert_eq!(*transport.fetched_entry_ids.borrow(), vec![1]);
drop(guard);
std::fs::remove_file(&path).ok();
}
#[test]
fn an_oversized_attachment_is_skipped_but_the_design_is_still_saved() {
let (db, path) = temp_db();
let mut record = design_record(1, "Round Brilliant", "https://example.test/1", "v1");
record.attachments = vec![
AttachedFileMeta {
id: 100,
name: "huge.pdf".to_string(),
url: String::new(),
size: 1000,
},
AttachedFileMeta {
id: 101,
name: "small.pdf".to_string(),
url: String::new(),
size: 5,
},
];
let summary = design_summary(1, "Round Brilliant", "https://example.test/1", "v1");
let transport = FakeTransport::new(vec![summary], vec![record])
.with_attachment(100, "huge.pdf", vec![0u8; 1000])
.with_attachment(101, "small.pdf", vec![9, 9, 9, 9, 9]);
let options = MirrorOptions {
max_attachment_bytes: 500,
};
let outcome = run_mirror_sync(
&db,
&transport,
"remote-library:w",
options,
&AtomicBool::new(false),
|_| {},
);
let MirrorOutcome::Completed(counts) = outcome else {
panic!("expected Completed, got {outcome:?}");
};
assert_eq!(counts.new_count, 1, "the design itself must still be saved");
assert_eq!(counts.attachments_fetched, 1);
assert_eq!(counts.attachments_skipped_too_large, 1);
assert_eq!(
transport.fetch_attachment_calls.load(Ordering::Relaxed),
1,
"the oversized attachment's bytes must never even be requested"
);
let guard = db.lock().unwrap();
let items = guard
.search_diagrams(
"",
"All",
"All",
&indicatrix_vault::model::filter::RangeFilter::default(),
)
.unwrap();
let full = guard.get_diagram_full(items[0].id).unwrap().unwrap();
assert_eq!(full.attached_files.len(), 1);
assert_eq!(full.attached_files[0].name, "small.pdf");
drop(guard);
std::fs::remove_file(&path).ok();
}
#[test]
fn a_failed_fetch_is_counted_and_never_marked_synced() {
let (db, path) = temp_db();
let summary = design_summary(1, "Ghost", "https://example.test/1", "v1");
let transport = FakeTransport::new(vec![summary], Vec::new());
let outcome = run_mirror_sync(
&db,
&transport,
"remote-library:w",
MirrorOptions::default(),
&AtomicBool::new(false),
|_| {},
);
let MirrorOutcome::Completed(counts) = outcome else {
panic!("expected Completed, got {outcome:?}");
};
assert_eq!(counts.failed, 1);
assert_eq!(counts.new_count, 0);
let guard = db.lock().unwrap();
assert_eq!(guard.get_total_count().unwrap(), 0);
assert_eq!(
guard.get_mirror_state("https://example.test/1").unwrap(),
None
);
drop(guard);
std::fs::remove_file(&path).ok();
}
#[test]
fn a_design_whose_connection_could_not_be_recovered_is_skipped_but_the_rest_of_the_sync_completes()
{
let (db, path) = temp_db();
let summaries = vec![
design_summary(1, "First", "https://example.test/1", "v1"),
design_summary(2, "Second", "https://example.test/2", "v1"),
design_summary(3, "Third", "https://example.test/3", "v1"),
];
let designs = vec![
design_record(1, "First", "https://example.test/1", "v1"),
design_record(2, "Second", "https://example.test/2", "v1"),
design_record(3, "Third", "https://example.test/3", "v1"),
];
let transport = FakeTransport::new(summaries, designs).with_fetch_design_failure(2);
let mut processed_count = 0;
let outcome = run_mirror_sync(
&db,
&transport,
"remote-library:w",
MirrorOptions::default(),
&AtomicBool::new(false),
|progress| processed_count = progress.processed,
);
let MirrorOutcome::Completed(counts) = outcome else {
panic!(
"an unrecoverable connection drop for ONE design must not abort the whole \
sync, got {outcome:?}"
);
};
assert_eq!(
processed_count, 3,
"every design was still examined, including after the drop"
);
assert_eq!(counts.total_found, 3);
assert_eq!(
counts.new_count, 2,
"designs 1 and 3 still land despite design 2's failure"
);
assert_eq!(counts.failed, 1);
let guard = db.lock().unwrap();
assert_eq!(guard.get_total_count().unwrap(), 2);
let items = guard
.search_diagrams(
"",
"All",
"All",
&indicatrix_vault::model::filter::RangeFilter::default(),
)
.unwrap();
let titles: std::collections::HashSet<&str> = items.iter().map(|i| i.title.as_str()).collect();
assert!(titles.contains("First"));
assert!(titles.contains("Third"));
assert!(
!titles.contains("Second"),
"the design whose connection could not be recovered must never be half-saved, \
titles were: {titles:?}"
);
assert_eq!(
guard.get_mirror_state("https://example.test/2").unwrap(),
None,
"never marked synced, so the next sync retries it -- same as any other failed fetch"
);
drop(guard);
std::fs::remove_file(&path).ok();
}
#[test]
fn mirror_source_id_is_distinct_per_worker_address() {
let a = WorkerSettings {
address: "10.0.0.5:9443".to_string(),
..WorkerSettings::default()
};
let b = WorkerSettings {
address: "10.0.0.6:9443".to_string(),
..WorkerSettings::default()
};
assert_ne!(mirror_source_id(&a), mirror_source_id(&b));
}
#[test]
fn a_multi_page_mirror_reaches_designs_beyond_the_first_page() {
let (db, path) = temp_db();
let entries: Vec<(i64, &str)> = vec![
(1, "First"),
(2, "Second"),
(3, "Third"),
(4, "Fourth"),
(5, "Fifth"),
];
let summaries: Vec<DesignSummary> = entries
.iter()
.map(|(id, title)| design_summary(*id, title, &format!("https://example.test/{id}"), "v1"))
.collect();
let designs: Vec<DesignRecord> = entries
.iter()
.map(|(id, title)| design_record(*id, title, &format!("https://example.test/{id}"), "v1"))
.collect();
let transport = FakeTransport::new(summaries, designs).with_page_size(2);
let outcome = run_mirror_sync(
&db,
&transport,
"remote-library:w",
MirrorOptions::default(),
&AtomicBool::new(false),
|_| {},
);
let MirrorOutcome::Completed(counts) = outcome else {
panic!("expected Completed, got {outcome:?}");
};
assert_eq!(
counts.total_found, 5,
"every design across every page must be counted, not just the first page's"
);
assert_eq!(counts.new_count, 5);
assert_eq!(transport.search_calls.load(Ordering::Relaxed), 3);
assert_eq!(transport.fetch_design_calls.load(Ordering::Relaxed), 5);
let guard = db.lock().unwrap();
assert_eq!(guard.get_total_count().unwrap(), 5);
let items = guard
.search_diagrams(
"",
"All",
"All",
&indicatrix_vault::model::filter::RangeFilter::default(),
)
.unwrap();
let titles: std::collections::HashSet<&str> = items.iter().map(|i| i.title.as_str()).collect();
assert!(titles.contains("Fourth"), "titles were: {titles:?}");
assert!(titles.contains("Fifth"), "titles were: {titles:?}");
drop(guard);
std::fs::remove_file(&path).ok();
}
#[test]
fn a_locally_deleted_design_is_not_resurrected_when_the_remote_changes() {
let (db, path) = temp_db();
let url = "https://example.test/1";
let first = run_mirror_sync(
&db,
&FakeTransport::new(
vec![design_summary(1, "Round Brilliant", url, "v1")],
vec![design_record(1, "Round Brilliant", url, "v1")],
),
"remote-library:w",
MirrorOptions::default(),
&AtomicBool::new(false),
|_| {},
);
assert!(matches!(first, MirrorOutcome::Completed(c) if c.new_count == 1));
{
let guard = db.lock().unwrap();
let id = guard.diagram_entry_id_for_url(url).unwrap().unwrap();
guard.delete_diagram_entry(id).unwrap();
}
let transport = FakeTransport::new(
vec![design_summary(1, "Round Brilliant", url, "v2-updated")],
vec![design_record(1, "Round Brilliant", url, "v2-updated")],
);
let outcome = run_mirror_sync(
&db,
&transport,
"remote-library:w",
MirrorOptions::default(),
&AtomicBool::new(false),
|_| {},
);
let MirrorOutcome::Completed(counts) = outcome else {
panic!("expected Completed, got {outcome:?}");
};
assert_eq!(counts.new_count, 0);
assert_eq!(counts.updated_count, 0);
assert_eq!(counts.failed, 0);
assert_eq!(counts.orphaned_mirror_states, 1);
assert_eq!(counts.skipped_deleted, 1);
assert_eq!(
transport.fetch_design_calls.load(Ordering::Relaxed),
0,
"a tombstoned design must not even be fetched"
);
let guard = db.lock().unwrap();
assert_eq!(guard.get_total_count().unwrap(), 0);
assert_eq!(guard.diagram_entry_id_for_url(url).unwrap(), None);
let state = guard.get_mirror_state(url).unwrap().unwrap();
assert!(state.deleted_locally, "the tombstone must survive the pass");
drop(guard);
std::fs::remove_file(&path).ok();
}