use std::error::Error;
use super::{ActorKind, Changed, Row, key, version_prefix};
use crate::serve::contracts::{MutationRequest, ResolvedMutation};
use crate::serve::protocol::Version;
use serde_json::{Value, json};
type TestResult = Result<(), Box<dyn Error>>;
const fn version(sequence: u64) -> Version {
Version {
generation: 7,
sequence,
}
}
fn request(id: &str, caller: &Value, change: &Value) -> Result<ResolvedMutation, Box<dyn Error>> {
Ok(MutationRequest::decode(&serde_json::to_vec(&json!({
"request_id":id, "branch":"main", "caller":caller,
"contract":{"kind":"dependencies", "origin":version(1), "reads":[]},
"change":change,
}))?)?
.resolve("operator-not-the-author")?)
}
fn person(subject: &str) -> Value {
json!({"issuer":"lys", "subject":subject})
}
fn put() -> Value {
json!({"kind":"document.put", "value":{"collection":"charts", "document_id":"child-1", "document":{"private":"body-marker"}}})
}
fn row() -> Result<Row, Box<dyn Error>> {
Ok(Row::for_request(
&request("write", &person("doctor-a"), &put())?,
version(2),
)?)
}
fn decode_value(value: &Value) -> Result<Row, crate::serve::storage::Failure> {
Row::decode(
&key(version(2), "write")?,
&serde_json::to_vec(value).map_err(crate::serve::storage::Failure::storage)?,
)
}
#[test]
fn two_callers_in_one_version_keep_two_rows_and_their_own_callers() -> TestResult {
let first = Row::for_request(&request("first", &person("doctor-a"), &put())?, version(2))?;
let second = Row::for_request(&request("second", &person("doctor-b"), &put())?, version(2))?;
assert_ne!(first.key()?, second.key()?);
let first = Row::decode(&first.key()?, &first.encode()?)?;
let second = Row::decode(&second.key()?, &second.encode()?)?;
assert_eq!(first.caller().subject(), "doctor-a");
assert_eq!(second.caller().subject(), "doctor-b");
assert_eq!(first.version(), second.version());
Ok(())
}
#[test]
fn three_imported_documents_have_one_row_with_count_three() -> TestResult {
let import = json!({"kind":"documents.put_many", "value":{"collection":"charts", "documents":[
{"document_id":"a", "document":{"private":"body-a"}},
{"document_id":"b", "document":{"private":"body-b"}},
{"document_id":"c", "document":{"private":"body-c"}}
]}});
let row = Row::for_request(
&request("import", &person("doctor-a"), &import)?,
version(2),
)?;
assert!(
matches!(row.changed(), Changed::Import { collection, count: 3 } if collection == "charts")
);
let bytes = row.encode()?;
let value: Value = serde_json::from_slice(&bytes)?;
assert_eq!(value["change"]["count"], 3);
assert!(value["change"].get("documents").is_none());
assert!(!String::from_utf8(bytes)?.contains("body-"));
Ok(())
}
#[test]
fn moving_a_row_to_another_request_key_is_refused() -> TestResult {
let row = row()?;
let error = Row::decode(&key(version(2), "other")?, &row.encode()?)
.err()
.ok_or("moved row accepted")?;
assert_eq!(error.code, "change_record_key_mismatch");
Ok(())
}
#[test]
fn moving_a_row_to_another_version_key_is_refused() -> TestResult {
let row = row()?;
let error = Row::decode(&key(version(3), "write")?, &row.encode()?)
.err()
.ok_or("moved row accepted")?;
assert_eq!(error.code, "change_record_key_mismatch");
Ok(())
}
#[test]
fn person_and_agent_keep_the_subject_and_responsible_person() -> TestResult {
let person = row()?;
assert_eq!(person.actor_kind(), ActorKind::Person);
assert_eq!(person.caller().responsible(), None);
let caller = json!({"issuer":"lys", "subject":"agent-a", "responsible":"doctor-a"});
let agent = Row::for_request(&request("agent", &caller, &put())?, version(2))?;
let decoded = Row::decode(&agent.key()?, &agent.encode()?)?;
assert_eq!(decoded.actor_kind(), ActorKind::Agent);
assert_eq!(decoded.caller().issuer(), "lys");
assert_eq!(decoded.caller().subject(), "agent-a");
assert_eq!(decoded.caller().responsible(), Some("doctor-a"));
Ok(())
}
#[test]
fn one_request_id_at_two_versions_has_two_distinct_keys() -> TestResult {
let request = request("write", &person("doctor-a"), &put())?;
let first = Row::for_request(&request, version(2))?;
let second = Row::for_request(&request, version(3))?;
assert_ne!(first.key()?, second.key()?);
assert!(first.key()? < second.key()?);
assert_eq!(first.request_id(), second.request_id());
assert_eq!(first.origin(), version(1));
assert_eq!(second.origin(), version(1));
Ok(())
}
#[test]
fn keys_sort_numerically_across_generation_and_sequence_boundaries() -> TestResult {
let versions = [
Version {
generation: 1,
sequence: 255,
},
Version {
generation: 1,
sequence: 256,
},
Version {
generation: 1,
sequence: u64::MAX,
},
Version {
generation: 2,
sequence: 0,
},
];
let keys = versions
.into_iter()
.map(|at| key(at, "write"))
.collect::<Result<Vec<_>, _>>()?;
assert!(keys.windows(2).all(|pair| pair[0] < pair[1]));
assert_eq!(version_prefix(version(2)).len(), 19);
assert!(key(version(2), "write")?.starts_with(&version_prefix(version(2))));
Ok(())
}
#[test]
fn a_document_row_names_its_target_and_keeps_no_body() -> TestResult {
let row = row()?;
assert!(
matches!(row.changed(), Changed::DocumentPut { collection, document_id } if collection == "charts" && document_id == "child-1")
);
let bytes = row.encode()?;
assert!(bytes.starts_with(b"{\"format\":1,"));
assert!(!String::from_utf8(bytes)?.contains("body-marker"));
Ok(())
}
#[test]
fn unknown_format_and_unknown_fields_are_refused() -> TestResult {
let mut value = serde_json::to_value(row()?)?;
value["format"] = json!(2);
assert_eq!(
decode_value(&value)
.err()
.ok_or("unknown format accepted")?
.code,
"change_record_format_unsupported"
);
value["format"] = json!(1);
value["credential"] = json!("must-not-be-kept");
assert_eq!(
decode_value(&value)
.err()
.ok_or("unknown field accepted")?
.code,
"change_record_corrupt"
);
Ok(())
}
#[test]
fn an_anonymous_stored_row_is_refused_without_an_operator_fallback() -> TestResult {
let mut value = serde_json::to_value(row()?)?;
value
.as_object_mut()
.ok_or("not an object")?
.remove("caller");
assert_eq!(
decode_value(&value)
.err()
.ok_or("anonymous row accepted")?
.code,
"change_record_corrupt"
);
Ok(())
}
#[test]
fn malformed_keys_and_invalid_request_ids_are_refused() -> TestResult {
let row = row()?;
for malformed in [b"a\0".as_slice(), b"z\0missing", b"a\0short"] {
assert_eq!(
Row::decode(malformed, &row.encode()?)
.err()
.ok_or("invalid key accepted")?
.code,
"change_record_key_invalid"
);
}
assert!(key(version(2), "").is_err());
assert!(key(version(2), "a\0b").is_err());
Ok(())
}
#[test]
fn all_target_kinds_round_trip_without_payloads() -> TestResult {
let from = json!({"collection":"charts", "document_id":"a"});
let to = json!({"collection":"charts", "document_id":"b"});
let cases = [
(
json!({"kind":"collection.put", "value":{"collection":"charts", "schema":{"private":"body-marker"}}}),
json!({"kind":"collection.put", "collection":"charts"}),
),
(
json!({"kind":"document.delete", "value":{"collection":"charts", "document_id":"a"}}),
json!({"kind":"document.delete", "collection":"charts", "document_id":"a"}),
),
(
json!({"kind":"edge.put", "value":{"relation":"link", "from":from, "to":to, "properties":{"private":"body-marker"}}}),
json!({"kind":"edge.put", "relation":"link", "from":from, "to":to}),
),
(
json!({"kind":"edge.delete", "value":{"relation":"link", "from":from, "to":to}}),
json!({"kind":"edge.delete", "relation":"link", "from":from, "to":to}),
),
(
json!({"kind":"vector.put", "value":{"collection":"charts", "document_id":"a", "field":"embedding", "vector":[1.0,2.0]}}),
json!({"kind":"vector.put", "collection":"charts", "document_id":"a", "field":"embedding"}),
),
(
json!({"kind":"vector.delete", "value":{"collection":"charts", "document_id":"a", "field":"embedding"}}),
json!({"kind":"vector.delete", "collection":"charts", "document_id":"a", "field":"embedding"}),
),
(
json!({"kind":"relation.put", "value":{"relation":"link", "from_collection":"charts", "to_collection":"charts", "cardinality":"many_to_many"}}),
json!({"kind":"relation.put", "relation":"link"}),
),
(
json!({"kind":"relation.delete", "value":{"relation":"link"}}),
json!({"kind":"relation.delete", "relation":"link"}),
),
(
json!({"kind":"branch.fork", "value":{"name":"work", "from":version(1)}}),
json!({"kind":"branch.fork", "name":"work", "from_branch":"main", "from":version(1)}),
),
(
json!({"kind":"branch.discard", "value":{"name":"work"}}),
json!({"kind":"branch.discard", "name":"work"}),
),
];
for (index, (change, expected)) in cases.into_iter().enumerate() {
let row = Row::for_request(
&request(&format!("request-{index}"), &person("doctor-a"), &change)?,
version(2),
)?;
let encoded = row.encode()?;
let decoded = Row::decode(&row.key()?, &encoded)?;
let value = serde_json::to_value(decoded)?;
assert_eq!(value["change"], expected);
assert!(!String::from_utf8(encoded)?.contains("body-marker"));
}
Ok(())
}
#[test]
fn nested_credentials_and_unknown_change_kinds_are_refused() -> TestResult {
let mut value = serde_json::to_value(row()?)?;
value["caller"]["token"] = json!("must-not-be-kept");
assert_eq!(
decode_value(&value)
.err()
.ok_or("credential accepted")?
.code,
"change_record_corrupt"
);
value["caller"] = person("doctor-a");
value["change"]["kind"] = json!("unserved.mutation");
assert_eq!(
decode_value(&value)
.err()
.ok_or("unknown kind accepted")?
.code,
"change_record_corrupt"
);
Ok(())
}
#[test]
fn oversized_rows_and_empty_import_metadata_are_refused() -> TestResult {
let oversized = vec![b' '; super::MAX_ROW_BYTES + 1];
assert_eq!(
Row::decode(&key(version(2), "write")?, &oversized)
.err()
.ok_or("oversize accepted")?
.code,
"change_record_too_large"
);
let mut value = serde_json::to_value(row()?)?;
value["change"] = json!({"kind":"documents.put_many", "collection":"charts", "count":0});
assert_eq!(
decode_value(&value)
.err()
.ok_or("empty import accepted")?
.code,
"change_record_corrupt"
);
Ok(())
}
#[test]
fn maximum_versions_and_utf8_request_ids_round_trip() -> TestResult {
let at = Version {
generation: u64::MAX,
sequence: u64::MAX,
};
let request = request("é-clinician", &person("doctor-a"), &put())?;
let row = Row::for_request(&request, at)?;
let decoded = Row::decode(&row.key()?, &row.encode()?)?;
assert_eq!(decoded.version(), at);
assert_eq!(decoded.request_id(), "é-clinician");
Ok(())
}
#[test]
fn invalid_target_names_are_refused_before_an_attribution_is_created() -> TestResult {
let malformed = json!({"kind":"document.put", "value":{"collection":"charts", "document_id":"", "document":{}}});
let request = request("write", &person("doctor-a"), &malformed)?;
assert_eq!(
Row::for_request(&request, version(2))
.err()
.ok_or("invalid target recorded")?
.code,
"invalid_identifier"
);
Ok(())
}
#[test]
fn vector_import_attribution_counts_rows_without_storing_vectors() -> TestResult {
let import = json!({"kind":"vectors.put_many", "value":{"vectors":[
{"collection":"charts", "document_id":"a", "field":"supplied", "vector":[1.0]},
{"collection":"charts", "document_id":"b", "field":"supplied", "vector":[2.0]}
]}});
let row = Row::for_request(
&request("vectors", &person("doctor-a"), &import)?,
version(2),
)?;
let decoded = Row::decode(&row.key()?, &row.encode()?)?;
assert!(matches!(
decoded.changed(),
Changed::VectorImport { count: 2 }
));
let value: Value = serde_json::from_slice(&row.encode()?)?;
assert_eq!(value["change"]["kind"], "vectors.put_many");
assert!(value["change"].get("vectors").is_none());
Ok(())
}
#[test]
fn three_hundred_deletion_entries_keep_single_delete_shape_and_input_order() -> TestResult {
let ids: Vec<_> = (0..300)
.rev()
.map(|index| format!("record-{index:03}"))
.collect();
let delete = json!({"kind":"documents.delete_many", "value":{
"collection":"charts", "document_ids":ids,
}});
let row = Row::for_request(
&request("delete", &person("doctor-a"), &delete)?,
version(2),
)?;
let decoded = Row::decode(&row.key()?, &row.encode()?)?;
let value: Value = serde_json::from_slice(&decoded.encode()?)?;
let entries = value["change"]["changes"]
.as_array()
.ok_or("no deletion entries")?;
assert_eq!(entries.len(), 300);
for (entry, id) in entries.iter().zip(&ids) {
assert_eq!(
entry,
&serde_json::to_value(Changed::DocumentDelete {
collection: "charts".to_owned(),
document_id: id.clone(),
})?,
);
}
assert_eq!(decoded.request_id(), "delete");
assert_eq!(decoded.caller().subject(), "doctor-a");
Ok(())
}
#[test]
fn deletion_batch_metadata_refuses_empty_duplicate_and_mixed_collections() -> TestResult {
for changes in [
json!([]),
json!([
{"kind":"document.delete", "collection":"charts", "document_id":"a"},
{"kind":"document.delete", "collection":"charts", "document_id":"a"}
]),
json!([
{"kind":"document.delete", "collection":"charts", "document_id":"a"},
{"kind":"document.delete", "collection":"other", "document_id":"b"}
]),
] {
let mut value: Value = serde_json::from_slice(&row()?.encode()?)?;
value["change"] = json!({"kind":"documents.delete_many", "changes":changes});
assert_eq!(
decode_value(&value)
.err()
.ok_or("invalid attribution accepted")?
.code,
"change_record_corrupt",
);
}
Ok(())
}