use std::sync::{Arc, Mutex};
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use lys_pass::deliberate::CheckAnswer;
use lys_pass::drafts::{DraftRequest, request_digest};
use lys_pass::{Error, Target};
use serde_json::{Value, json};
use super::tests::{ISSUER, KID, NOW, pass, role, signer};
use super::{Gate, LiveCheck};
use crate::serve::protocol::{Operation, Request};
type TestResult = Result<(), Box<dyn std::error::Error>>;
struct HeldLys {
reachable: bool,
drafts: Mutex<Vec<DraftRequest>>,
}
impl HeldLys {
fn new(reachable: bool) -> Arc<Self> {
Arc::new(Self {
reachable,
drafts: Mutex::new(Vec::new()),
})
}
fn recorded(&self) -> Result<Vec<DraftRequest>, Box<dyn std::error::Error>> {
Ok(self
.drafts
.lock()
.map_err(|_| "the double's draft list was poisoned")?
.clone())
}
}
impl LiveCheck for HeldLys {
fn ask(&self, _token: &str, _holder: &str, _target: &Target) -> Result<CheckAnswer, Error> {
Ok(serde_json::from_value(json!({
"allowed": false, "grant": "branch-by-draft", "mode": "by_draft", "path": [],
"via": null, "refusal": null, "reason": null, "degraded": null,
}))?)
}
fn draft(
&self,
_token: &str,
request: &DraftRequest,
_judgment: &CheckAnswer,
) -> Result<String, Error> {
if !self.reachable {
return Err(Error::CannotAsk("the double is unreachable"));
}
self.drafts
.lock()
.map_err(|_| Error::CannotAsk("the double's draft list was poisoned"))?
.push(request.clone());
Ok("draft-1".to_owned())
}
}
fn gate(live: Arc<HeldLys>) -> Result<Gate, Error> {
let x = URL_SAFE_NO_PAD.encode(signer().verifying_key().as_bytes());
let keys = json!({"keys": [{"kty": "OKP", "crv": "Ed25519", "kid": KID, "x": x, "alg": "EdDSA", "use": "sig"}]});
Gate::new(&keys.to_string(), ISSUER, "wards", live)
}
const FORK: &str = r#"{"id":"7","operation":{"method":"mutation.apply","params":{"request_id":"f","branch":"main","expected":{"generation":"1","sequence":"1"},"change":{"kind":"branch.fork","value":{"name":"trial","from":{"generation":"1","sequence":"1"}}}}}}"#;
fn operation(words: &str) -> Result<Operation, Box<dyn std::error::Error>> {
Ok(serde_json::from_str::<Request>(words)?.operation)
}
#[test]
fn a_held_change_is_written_as_a_draft_of_its_exact_words() -> TestResult {
let lys = HeldLys::new(true);
let gate = gate(Arc::clone(&lys))?;
let admin = pass("administrator", &role(&["read"], "read-grant"));
let held = gate
.authorize(Some(&admin), &operation(FORK)?, Some(FORK), NOW)
.err()
.ok_or("a held fork was allowed")?;
assert_eq!(held.code, "held_for_approval");
assert_eq!(
serde_json::from_str::<Value>(&held.message)?,
json!({"draft": "draft-1", "grant": "branch-by-draft", "mode": "by_draft"})
);
let recorded = lys.recorded()?;
let [draft] = recorded.as_slice() else {
return Err(format!("expected one draft, Lys recorded {}", recorded.len()).into());
};
assert_eq!(draft.operation, "haematite:wards:main:f");
assert_eq!(draft.grant, "branch-by-draft");
assert_eq!(
(
draft.target.kind.as_str(),
draft.target.id.as_str(),
draft.target.action.as_str()
),
("haematite.database", "wards", "branch")
);
assert_eq!(draft.words, FORK);
assert_eq!(draft.request_digest, request_digest(FORK));
Ok(())
}
#[test]
fn a_draft_lys_cannot_record_is_refused_lys_could_not_be_asked() -> TestResult {
let gate = gate(HeldLys::new(false))?;
let admin = pass("administrator", &role(&["read"], "read-grant"));
let refused = gate
.authorize(Some(&admin), &operation(FORK)?, Some(FORK), NOW)
.err()
.ok_or("a held fork was allowed")?;
assert_eq!(
(refused.code.as_str(), refused.message.as_str()),
("lys_could_not_be_asked", "Lys could not be asked")
);
Ok(())
}
#[test]
fn a_held_read_is_never_drafted() -> TestResult {
let lys = HeldLys::new(true);
let gate = gate(Arc::clone(&lys))?;
let words = r#"{"id":"8","operation":{"method":"drafts.run","params":{}}}"#;
let admin = pass("administrator", &role(&["read"], "read-grant"));
let held = gate
.authorize(Some(&admin), &operation(words)?, Some(words), NOW)
.err()
.ok_or("a held read was allowed")?;
assert_eq!(held.code, "held_for_approval");
assert!(lys.recorded()?.is_empty(), "a read was drafted");
Ok(())
}