use openehr::base::HierObjectId;
use openehr_sqlite::SqliteStore;
use openehr_store::{
Store as _, conformance,
integrity::{Breach, Integrity, verify_versions},
};
use rusqlite::Connection;
use std::path::{Path, PathBuf};
fn temp_db(tag: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("after the epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!("openehr-tamper-{tag}-{nanos}.sqlite3"));
let _ = std::fs::remove_file(&path);
path
}
fn container() -> HierObjectId {
HierObjectId::from_uid_str(conformance::RECORD).expect("literal")
}
fn seed(path: &Path) {
let mut store = SqliteStore::open(path).expect("open");
store.install().expect("install");
let ehr = conformance::sample_ehr();
let ehr_id = ehr.ehr_id().clone();
store.create_ehr(&ehr).expect("ehr");
store
.create_contribution(
&ehr_id,
&conformance::sample_contribution("22222222-3333-4444-5555-666666666666", &[1, 2, 3]),
)
.expect("contribution");
for (n, preceding) in [(1, None), (2, Some(1)), (3, Some(2))] {
store
.commit_composition(
&ehr_id,
&conformance::sample_version(n, preceding, n * 5),
"22222222-3333-4444-5555-666666666666",
)
.expect("commit");
}
}
fn tamper(path: &Path, sql: &str) {
let connection = Connection::open(path).expect("second connection");
connection
.execute_batch(
"DROP TRIGGER IF EXISTS trg_openehr_version_no_update;
DROP TRIGGER IF EXISTS trg_openehr_version_no_delete;",
)
.expect("drop triggers");
connection.execute_batch(sql).expect("tamper");
}
fn excise(path: &Path, suffix: &str) {
tamper(
path,
&format!(
"DELETE FROM openehr_composition_index WHERE version_uid LIKE '%::{suffix}';
DELETE FROM openehr_version WHERE uid LIKE '%::{suffix}';"
),
);
}
fn verdict(path: &Path) -> Integrity {
let store = SqliteStore::open(path).expect("reopen");
let rows = store.all_versions(&container()).expect("read back");
verify_versions(&rows, &[])
}
#[test]
fn an_untampered_history_verifies() {
let path = temp_db("clean");
seed(&path);
assert_eq!(verdict(&path), Integrity::Unkeyed);
let _ = std::fs::remove_file(&path);
}
#[test]
fn editing_a_stored_document_is_detected() {
let path = temp_db("content");
seed(&path);
tamper(
&path,
"UPDATE openehr_version
SET data_json = replace(data_json, 'Encounter 2', 'Encounter X')
WHERE uid LIKE '%::2'",
);
match verdict(&path) {
Integrity::Broken { at, uid, reason } => {
assert_eq!(reason, Breach::ContentAltered);
assert_eq!(at, 1, "the second version was the one edited");
assert!(uid.ends_with("::2"), "{uid}");
}
other => panic!("an edited clinical document was not detected: {other:?}"),
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn removing_a_version_from_the_middle_is_detected() {
let path = temp_db("middle");
seed(&path);
excise(&path, "2");
match verdict(&path) {
Integrity::Broken { at, reason, .. } => {
assert_eq!(reason, Breach::PreviousMismatch);
assert_eq!(at, 1, "the break is at the row that no longer follows");
}
other => panic!("a version removed from the middle was not detected: {other:?}"),
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn rewriting_one_chain_column_is_detected() {
let path = temp_db("digest");
seed(&path);
tamper(
&path,
"UPDATE openehr_version SET chain_digest = zeroblob(32) WHERE uid LIKE '%::1'",
);
match verdict(&path) {
Integrity::Broken { at, reason, .. } => {
assert_eq!(reason, Breach::DigestMismatch);
assert_eq!(at, 0);
}
other => panic!("a rewritten chain digest was not detected: {other:?}"),
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn truncating_the_newest_version_is_not_detected_and_that_is_why_checkpoints_exist() {
let path = temp_db("truncate");
seed(&path);
excise(&path, "3");
assert!(
verdict(&path).is_intact(),
"if this ever fails, the chain gained a property it is not documented \
to have, and M3.16c should be revisited rather than this test deleted"
);
let store = SqliteStore::open(&path).expect("reopen");
let checkpoint = store.chain_checkpoint(&container()).expect("checkpoint");
assert!(
checkpoint.starts_with("entries=2 "),
"the checkpoint must report the count it can actually see: {checkpoint}"
);
let _ = std::fs::remove_file(&path);
}