use std::error::Error;
use super::{ActorKind, 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_naming_none_of_them() -> 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),
)?;
let bytes = row.encode()?;
let value: Value = serde_json::from_slice(&bytes)?;
assert!(value.get("change").is_none(), "{value}");
assert_eq!(value["request_id"], "import");
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_author_and_keeps_no_target_or_body() -> TestResult {
let row = row()?;
let bytes = row.encode()?;
assert!(bytes.starts_with(b"{\"format\":2,"));
let text = String::from_utf8(bytes)?;
assert!(!text.contains("body-marker"), "{text}");
assert!(!text.contains("child-1"), "{text}");
assert_eq!(row.caller().subject(), "doctor-a");
Ok(())
}
#[test]
fn unknown_format_and_unknown_fields_are_refused() -> TestResult {
let mut value = serde_json::to_value(row()?)?;
value["format"] = json!(3);
assert_eq!(
decode_value(&value)
.err()
.ok_or("unknown format accepted")?
.code,
"change_record_format_unsupported"
);
value["format"] = json!(2);
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 every_change_kind_writes_a_row_without_its_change() -> TestResult {
let from = json!({"collection":"charts", "document_id":"a"});
let to = json!({"collection":"charts", "document_id":"b"});
let changes = [
json!({"kind":"collection.put", "value":{"collection":"charts", "schema":{"private":"body-marker"}}}),
json!({"kind":"document.delete", "value":{"collection":"charts", "document_id":"a"}}),
json!({"kind":"edge.put", "value":{"relation":"link", "from":from, "to":to, "properties":{"private":"body-marker"}}}),
json!({"kind":"edge.delete", "value":{"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.delete", "value":{"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.delete", "value":{"relation":"link"}}),
json!({"kind":"branch.fork", "value":{"name":"work", "from":version(1)}}),
json!({"kind":"branch.discard", "value":{"name":"work"}}),
];
for (index, change) in changes.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!(value.get("change").is_none(), "{value}");
assert_eq!(value["request_id"], format!("request-{index}"));
assert!(!String::from_utf8(encoded)?.contains("body-marker"));
}
Ok(())
}
#[test]
fn nested_credentials_and_a_format_two_change_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"] =
json!({"kind":"document.put", "collection":"charts", "document_id":"child-1"});
assert_eq!(
decode_value(&value)
.err()
.ok_or("a format 2 row with a change accepted")?
.code,
"change_record_corrupt"
);
Ok(())
}
#[test]
fn oversized_rows_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"
);
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 an_invalid_request_id_is_refused_before_a_row_is_made() -> TestResult {
let caller = crate::serve::contracts::Caller::local("tom")?;
for request_id in ["", "a\0b"] {
assert_eq!(
Row::by(request_id, &caller, version(1), version(2))
.err()
.ok_or("invalid request id recorded")?
.code,
"invalid_identifier"
);
}
Ok(())
}
#[test]
fn a_vector_import_row_holds_no_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_eq!(decoded.request_id(), "vectors");
let value: Value = serde_json::from_slice(&row.encode()?)?;
assert!(value.get("change").is_none(), "{value}");
Ok(())
}
#[test]
fn a_delete_of_three_hundred_ids_writes_the_row_a_delete_of_one_writes() -> TestResult {
let delete = |count: usize| {
let ids: Vec<_> = (0..count)
.map(|index| format!("record-{index:03}"))
.collect();
json!({"kind":"documents.delete_many", "value":{"collection":"charts", "document_ids":ids}})
};
let one = Row::for_request(
&request("delete", &person("doctor-a"), &delete(1))?,
version(2),
)?;
let many = Row::for_request(
&request("delete", &person("doctor-a"), &delete(300))?,
version(2),
)?;
assert_eq!(one.encode()?, many.encode()?);
let decoded = Row::decode(&many.key()?, &many.encode()?)?;
assert_eq!(decoded.request_id(), "delete");
assert_eq!(decoded.caller().subject(), "doctor-a");
Ok(())
}
#[test]
fn a_format_one_row_is_read_without_its_change() -> TestResult {
let mut value = serde_json::to_value(row()?)?;
value["format"] = json!(1);
value["change"] = json!({"kind":"documents.delete_many", "changes":[
{"kind":"document.delete", "collection":"charts", "document_id":"a"}
]});
let decoded = decode_value(&value)?;
assert_eq!(
decoded.author(),
json!({"request_id":"write", "caller":{"issuer":"lys", "subject":"doctor-a"}})
);
let rewritten: Value = serde_json::from_slice(&decoded.encode()?)?;
assert_eq!(rewritten["format"], 2);
assert!(rewritten.get("change").is_none());
let mut bare = serde_json::to_value(row()?)?;
bare["format"] = json!(1);
assert_eq!(
decode_value(&bare)
.err()
.ok_or("a format 1 row without its change accepted")?
.code,
"change_record_corrupt"
);
Ok(())
}