use std::error::Error;
use std::thread::sleep;
use std::time::Duration;
use liminal_protocol::wire::{
AttachAttemptToken, AttachBound, AttachSecret, ClientRequest, ConnectionIncarnation,
CredentialAttachRequest, DetachAttemptToken, DetachRequest, EnrollmentRequest, EnrollmentToken,
Generation, ReceiptExpired, ReceiptExpiryReason, ReceiptReplay, ServerValue,
StaleOrUnknownReceipt,
};
use crate::config::types::ParticipantConfig;
use super::ProductionParticipantHandler;
use super::tests::{dispatch, open_disk_store_for_tests, test_participant_config};
pub(super) const fn short_ttl_config(
attach_receipt_ttl_ms: u64,
receipt_provenance_ttl_ms: u64,
) -> ParticipantConfig {
let mut config = test_participant_config();
config.attach_receipt_ttl_ms = attach_receipt_ttl_ms;
config.receipt_provenance_ttl_ms = receipt_provenance_ttl_ms;
config
}
pub(super) fn enroll(
handler: &ProductionParticipantHandler,
incarnation: ConnectionIncarnation,
conversation_id: u64,
token: [u8; 16],
) -> Result<liminal_protocol::wire::EnrollBound, Box<dyn Error>> {
let enrolled = dispatch(
handler,
incarnation,
ClientRequest::Enrollment(EnrollmentRequest {
conversation_id,
enrollment_token: EnrollmentToken::new(token),
}),
)?;
let ServerValue::EnrollBound(receipt) = enrolled else {
return Err(format!("enrollment did not bind: {enrolled:?}").into());
};
Ok(receipt)
}
pub(super) fn detach(
handler: &ProductionParticipantHandler,
incarnation: ConnectionIncarnation,
conversation_id: u64,
participant_id: u64,
generation: Generation,
token: [u8; 16],
) -> Result<(), Box<dyn Error>> {
let detached = dispatch(
handler,
incarnation,
ClientRequest::Detach(DetachRequest {
conversation_id,
participant_id,
capability_generation: generation,
detach_attempt_token: DetachAttemptToken::new(token),
}),
)?;
if !matches!(detached, ServerValue::DetachCommitted(_)) {
return Err(format!("detach did not commit: {detached:?}").into());
}
Ok(())
}
pub(super) fn attach_request(
conversation_id: u64,
participant_id: u64,
generation: Generation,
secret: AttachSecret,
token: [u8; 16],
) -> ClientRequest {
ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id,
participant_id,
capability_generation: generation,
attach_secret: secret,
attach_attempt_token: AttachAttemptToken::new(token),
accept_marker_delivery_seq: None,
})
}
pub(super) fn attach(
handler: &ProductionParticipantHandler,
incarnation: ConnectionIncarnation,
request: ClientRequest,
) -> Result<AttachBound, Box<dyn Error>> {
let attached = dispatch(handler, incarnation, request)?;
let ServerValue::AttachBound(receipt) = attached else {
return Err(format!("attach did not bind: {attached:?}").into());
};
Ok(receipt)
}
pub(super) const GEN_ONE: Generation = Generation::ONE;
pub(super) fn generation(value: u64) -> Result<Generation, Box<dyn Error>> {
Generation::new(value).ok_or_else(|| "zero generation in test fixture".into())
}
#[test]
fn attach_deadline_provenance_replay_carries_result_generation() -> Result<(), Box<dyn Error>> {
let home = tempfile::tempdir()?;
let data_dir = home.path().join("durability");
let incarnation = ConnectionIncarnation::new(62, 1);
let store = open_disk_store_for_tests(&data_dir)?;
let handler = ProductionParticipantHandler::new(store, short_ttl_config(300, 600_000))?;
let conversation_id = 602;
let receipt = enroll(&handler, incarnation, conversation_id, [64; 16])?;
let participant_id = receipt.participant_id();
detach(
&handler,
incarnation,
conversation_id,
participant_id,
GEN_ONE,
[65; 16],
)?;
let attach_token = [66; 16];
let request = attach_request(
conversation_id,
participant_id,
GEN_ONE,
receipt.attach_secret(),
attach_token,
);
let attached = attach(&handler, incarnation, request.clone())?;
assert_eq!(attached.capability_generation(), generation(2)?);
sleep(Duration::from_millis(500));
let replayed = dispatch(&handler, incarnation, request)?;
let ServerValue::ReceiptExpired(ReceiptExpired::CredentialAttach {
token,
presented_generation,
result_generation,
current_generation,
reason,
..
}) = replayed
else {
return Err(
format!("expected the ReceiptExpired provenance row, got: {replayed:?}").into(),
);
};
assert_eq!(token, AttachAttemptToken::new(attach_token));
assert_eq!(presented_generation, GEN_ONE);
assert_eq!(
result_generation,
generation(2)?,
"the provenance row must carry the minted RESULT generation"
);
assert_eq!(current_generation, generation(2)?);
assert_eq!(reason, ReceiptExpiryReason::Deadline);
Ok(())
}
#[test]
fn superseded_receipt_keeps_provenance_then_degrades_to_stale_or_unknown()
-> Result<(), Box<dyn Error>> {
let home = tempfile::tempdir()?;
let data_dir = home.path().join("durability");
let incarnation = ConnectionIncarnation::new(63, 1);
let store = open_disk_store_for_tests(&data_dir)?;
let handler = ProductionParticipantHandler::new(store, short_ttl_config(2_000, 2_500))?;
let conversation_id = 603;
let receipt = enroll(&handler, incarnation, conversation_id, [67; 16])?;
let participant_id = receipt.participant_id();
detach(
&handler,
incarnation,
conversation_id,
participant_id,
GEN_ONE,
[68; 16],
)?;
let first_token = [69; 16];
let first_request = attach_request(
conversation_id,
participant_id,
GEN_ONE,
receipt.attach_secret(),
first_token,
);
let first = attach(&handler, incarnation, first_request.clone())?;
assert_eq!(first.capability_generation(), generation(2)?);
detach(
&handler,
incarnation,
conversation_id,
participant_id,
generation(2)?,
[70; 16],
)?;
let second = attach(
&handler,
incarnation,
attach_request(
conversation_id,
participant_id,
generation(2)?,
first.attach_secret(),
[71; 16],
),
)?;
assert_eq!(second.capability_generation(), generation(3)?);
let in_window = dispatch(&handler, incarnation, first_request.clone())?;
let ServerValue::ReceiptExpired(ReceiptExpired::CredentialAttach {
token,
presented_generation,
result_generation,
current_generation,
reason,
..
}) = in_window
else {
return Err(format!(
"superseded token inside its window must answer ReceiptExpired, got: {in_window:?}"
)
.into());
};
assert_eq!(token, AttachAttemptToken::new(first_token));
assert_eq!(presented_generation, GEN_ONE);
assert_eq!(result_generation, generation(2)?);
assert_eq!(current_generation, generation(3)?);
assert_eq!(reason, ReceiptExpiryReason::Superseded);
sleep(Duration::from_millis(3_000));
let after_window = dispatch(&handler, incarnation, first_request)?;
let ServerValue::StaleOrUnknownReceipt(StaleOrUnknownReceipt {
token,
presented_generation,
current_generation,
..
}) = after_window
else {
return Err(format!(
"superseded token after its window must answer StaleOrUnknownReceipt, got: \
{after_window:?}"
)
.into());
};
assert_eq!(token, AttachAttemptToken::new(first_token));
assert_eq!(presented_generation, GEN_ONE);
assert_eq!(current_generation, generation(3)?);
Ok(())
}
#[test]
fn live_receipt_replays_with_invalidated_old_secret() -> Result<(), Box<dyn Error>> {
let home = tempfile::tempdir()?;
let data_dir = home.path().join("durability");
let incarnation = ConnectionIncarnation::new(64, 1);
let store = open_disk_store_for_tests(&data_dir)?;
let handler = ProductionParticipantHandler::new(store, test_participant_config())?;
let conversation_id = 604;
let receipt = enroll(&handler, incarnation, conversation_id, [72; 16])?;
let participant_id = receipt.participant_id();
detach(
&handler,
incarnation,
conversation_id,
participant_id,
GEN_ONE,
[73; 16],
)?;
let old_secret = receipt.attach_secret();
let request = attach_request(
conversation_id,
participant_id,
GEN_ONE,
old_secret,
[74; 16],
);
let attached = attach(&handler, incarnation, request.clone())?;
assert_ne!(
attached.attach_secret(),
old_secret,
"rotation must invalidate the presented secret"
);
let replayed = dispatch(&handler, incarnation, request)?;
let ServerValue::Bound(ReceiptReplay::CredentialAttach(replay)) = replayed else {
return Err(format!("live receipt replay must answer Bound, got: {replayed:?}").into());
};
assert_eq!(
replay, attached,
"replay must be the exact committed result"
);
let forged = dispatch(
&handler,
incarnation,
attach_request(
conversation_id,
participant_id,
GEN_ONE,
AttachSecret::new([0xEE; 32]),
[74; 16],
),
)?;
assert!(
matches!(forged, ServerValue::StaleAuthority(_)),
"wrong-secret replay of a live token must be StaleAuthority: {forged:?}"
);
Ok(())
}