use alloc::vec;
use alloc::vec::Vec;
use super::{
CapsuleDecodeError, CapsuleRestoreError, ConversationDecision, ConversationGenesis,
LifecycleCapsule, LifecycleView, ParticipantConversation,
};
const CONVERSATION_ID: u64 = 91;
fn validated() -> Result<ParticipantConversation, &'static str> {
let fresh = ParticipantConversation::from_genesis(ConversationGenesis::new(CONVERSATION_ID));
let ConversationDecision::Commit(commit) = fresh.decide_genesis_validation() else {
return Err("a fresh genesis must select its one-shot event");
};
Ok(commit.commit())
}
fn capsule_of(
conversation: &ParticipantConversation,
covered: u64,
) -> Result<LifecycleCapsule, &'static str> {
LifecycleCapsule::capture(&LifecycleView {
sealed: None,
conversation,
lifecycle: None,
covered_log_sequence: covered,
})
.map_err(|_| "a shell without a lifecycle always captures")
}
fn round_trip(
conversation: &ParticipantConversation,
covered: u64,
) -> Result<ParticipantConversation, &'static str> {
let bytes = capsule_of(conversation, covered)?.encode_canonical();
let decoded = LifecycleCapsule::decode_canonical(&bytes).map_err(|_| "decode refused")?;
assert_eq!(decoded, capsule_of(conversation, covered)?);
let restored = decoded.restore().map_err(|_| "restore refused")?;
assert_eq!(restored.covered_log_sequence(), covered);
Ok(restored.into_parts().conversation)
}
#[test]
fn a_fresh_shell_round_trips() -> Result<(), &'static str> {
let fresh = ParticipantConversation::from_genesis(ConversationGenesis::new(CONVERSATION_ID));
assert_eq!(round_trip(&fresh, 0)?, fresh);
Ok(())
}
#[test]
fn a_validated_shell_round_trips() -> Result<(), &'static str> {
let shell = validated()?;
assert_eq!(round_trip(&shell, 4)?, shell);
Ok(())
}
#[test]
fn a_shell_deep_in_its_log_round_trips() -> Result<(), &'static str> {
let shell = ParticipantConversation::from_test_state(
ConversationGenesis::new(CONVERSATION_ID),
u64::MAX - 1,
true,
);
assert_eq!(round_trip(&shell, u64::MAX)?, shell);
Ok(())
}
#[test]
fn the_canonical_bytes_are_stable() -> Result<(), &'static str> {
let bytes = capsule_of(&validated()?, 4)?.encode_canonical();
assert_eq!(
bytes,
vec![
b'L', b'P', b'C', b'K', 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 91, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 15, 0, 1, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 1, 1, ]
);
Ok(())
}
fn canonical() -> Result<Vec<u8>, &'static str> {
Ok(capsule_of(&validated()?, 4)?.encode_canonical())
}
fn decode(bytes: &[u8]) -> Option<CapsuleDecodeError> {
LifecycleCapsule::decode_canonical(bytes).err()
}
#[test]
fn every_malformed_capsule_is_refused_by_name() -> Result<(), &'static str> {
let good = canonical()?;
assert_eq!(
decode(good.get(..27).ok_or("short")?),
Some(CapsuleDecodeError::Truncated {
required: 28,
available: 27
})
);
let mut magic = good.clone();
*magic.first_mut().ok_or("empty")? = b'X';
assert_eq!(decode(&magic), Some(CapsuleDecodeError::InvalidMagic));
let mut version = good.clone();
*version.get_mut(5).ok_or("short")? = 2;
assert_eq!(
decode(&version),
Some(CapsuleDecodeError::UnsupportedCodec { major: 2, minor: 0 })
);
let mut long = good.clone();
long.push(0);
assert_eq!(
decode(&long),
Some(CapsuleDecodeError::NonCanonicalLength {
declared_body_len: 15,
actual_body_len: 16
})
);
let mut trailing = long;
*trailing.get_mut(27).ok_or("short")? = 16;
assert_eq!(
decode(&trailing),
Some(CapsuleDecodeError::TrailingBytes { trailing: 1 })
);
let mut unknown = good.clone();
*unknown.get_mut(29).ok_or("short")? = 9;
assert_eq!(
decode(&unknown),
Some(CapsuleDecodeError::UnknownSection { tag: 9 })
);
let mut empty_body = good.get(..28).ok_or("short")?.to_vec();
*empty_body.get_mut(27).ok_or("short")? = 0;
assert_eq!(
decode(&empty_body),
Some(CapsuleDecodeError::MissingSection { tag: 1 })
);
let mut flags = good;
*flags.last_mut().ok_or("empty")? = 0b11;
assert_eq!(
decode(&flags),
Some(CapsuleDecodeError::NonCanonicalSection { tag: 1 })
);
Ok(())
}
#[test]
fn a_section_of_the_wrong_length_is_refused() -> Result<(), &'static str> {
let mut short = canonical()?;
short.pop();
*short.get_mut(27).ok_or("short")? = 14;
*short.get_mut(33).ok_or("short")? = 8;
assert_eq!(
decode(&short),
Some(CapsuleDecodeError::NonCanonicalSectionLength {
tag: 1,
declared: 8
})
);
Ok(())
}
#[test]
fn a_shell_whose_ordinal_and_validation_disagree_is_refused() -> Result<(), &'static str> {
let mut unvalidated_past_genesis = canonical()?;
*unvalidated_past_genesis.last_mut().ok_or("empty")? = 0;
assert_eq!(
decode(&unvalidated_past_genesis),
Some(CapsuleDecodeError::NonCanonicalSection { tag: 1 })
);
let mut validated_at_zero = canonical()?;
*validated_at_zero.get_mut(41).ok_or("short")? = 0;
assert_eq!(
decode(&validated_at_zero),
Some(CapsuleDecodeError::NonCanonicalSection { tag: 1 })
);
let inconsistent = ParticipantConversation::from_test_state(
ConversationGenesis::new(CONVERSATION_ID),
3,
false,
);
assert_eq!(
capsule_of(&inconsistent, 3)?.restore().err(),
Some(CapsuleRestoreError::InconsistentConversation)
);
Ok(())
}