use super::*;
use crate::wire::{
AttachAttemptToken, AttachSecret, ClientRequest, CredentialAttachRequest, Generation,
ReceiptReplay, ServerValue,
};
use super::gen_skip_supersession_tests::{TestResult, epoch, generation};
const CONVERSATION: u64 = 141;
const PARTICIPANT: u64 = 142;
const STALE_SECRET: u8 = 143;
const ROTATED_SECRET: u8 = 144;
const CHURN_TOKEN: u8 = 0xC3;
fn detached_at(generation_value: u64, secret: u8) -> TestResult<ClientParticipantAggregate> {
let mut aggregate = ClientParticipantAggregate::new();
aggregate.binding = ClientBindingState::Detached {
conversation_id: CONVERSATION,
participant_id: PARTICIPANT,
generation: generation(generation_value)?,
attach_secret: AttachSecret::new([secret; 32]),
};
Ok(aggregate)
}
fn attach_request(
participant_id: u64,
generation_value: u64,
secret: u8,
token: u8,
) -> TestResult<ClientRequest> {
Ok(ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: CONVERSATION,
participant_id,
capability_generation: generation(generation_value)?,
attach_secret: AttachSecret::new([secret; 32]),
attach_attempt_token: AttachAttemptToken::new([token; 16]),
accept_marker_delivery_seq: None,
}))
}
fn expected_attach(
participant_id: u64,
generation_value: u64,
secret: u8,
) -> TestResult<ExpectedOperationState> {
Ok(ExpectedOperationState {
request: attach_request(participant_id, generation_value, secret, CHURN_TOKEN)?,
issued: true,
authorization: 1,
lost: None,
})
}
fn receipt(
participant_id: u64,
request_generation: u64,
granted_generation: u64,
secret: u8,
) -> TestResult<crate::wire::AttachBound> {
crate::wire::AttachBound::ordinary(
CONVERSATION,
AttachAttemptToken::new([CHURN_TOKEN; 16]),
participant_id,
generation(request_generation)?,
AttachSecret::new([secret; 32]),
epoch(granted_generation)?,
0,
0,
0,
)
.ok_or("the receipt's granted generation must succeed its request generation")
}
fn consume_replay(
aggregate: ClientParticipantAggregate,
replay: crate::wire::AttachBound,
) -> TestResult<ClientParticipantAggregate> {
let ClientCorrelatedInboundDecision::Applied(applied) = decide_correlated_inbound(
aggregate,
ServerValue::UnboundReceipt(ReceiptReplay::CredentialAttach(replay)),
ClientResponseCorrelation { authorization: 1 },
) else {
return Err("the attach receipt replay must correlate to the issued attach");
};
let (aggregate, _) = applied.into_parts();
Ok(aggregate)
}
fn credential(aggregate: &ClientParticipantAggregate) -> Option<(Generation, AttachSecret)> {
match &aggregate.binding {
ClientBindingState::Bound {
generation,
attach_secret,
..
}
| ClientBindingState::Detached {
generation,
attach_secret,
..
} => Some((*generation, *attach_secret)),
ClientBindingState::Unbound | ClientBindingState::Left { .. } => None,
}
}
#[test]
fn a_torn_but_bound_attach_recovers_from_its_receipt_replay() -> TestResult {
let mut aggregate = detached_at(1, STALE_SECRET)?;
aggregate.expected = Some(expected_attach(PARTICIPANT, 1, STALE_SECRET)?);
aggregate.next_operation_authorization = 1;
let aggregate = consume_replay(aggregate, receipt(PARTICIPANT, 1, 2, ROTATED_SECRET)?)?;
let Some((held_generation, held_secret)) = credential(&aggregate) else {
return Err("#62 REPRODUCED: the replay left no usable credential at all");
};
if held_generation != generation(2)? {
return Err(
"#62 REPRODUCED: the rotated generation was dropped, the client is stranded one \
generation behind forever",
);
}
if held_secret != AttachSecret::new([ROTATED_SECRET; 32]) {
return Err("#62 REPRODUCED: the rotated attach secret was dropped from the replay");
}
assert!(
matches!(aggregate.binding, ClientBindingState::Detached { .. }),
"an UnboundReceipt names a binding the server has already released, so adopting it as \
Bound would claim a live binding that does not exist"
);
let mut aggregate = aggregate;
aggregate.next_operation_authorization = 1;
let decision = record_operation(
aggregate,
attach_request(PARTICIPANT, 2, ROTATED_SECRET, 0xC5)?,
);
let ClientOperationRecordDecision::Pending(_) = decision else {
return Err(
"#62 REPRODUCED: the strand -- the aggregate refuses to form an attach at the rotated \
generation (BindingMismatch) while the server refuses one at the old generation",
);
};
Ok(())
}
#[test]
fn the_pre_tear_credential_is_the_one_the_server_calls_stale() -> TestResult {
let mut aggregate = detached_at(1, STALE_SECRET)?;
aggregate.next_operation_authorization = 1;
let decision = record_operation(
aggregate,
attach_request(PARTICIPANT, 1, STALE_SECRET, 0xC5)?,
);
assert!(
matches!(decision, ClientOperationRecordDecision::Pending(_)),
"the pre-tear client can form only its own generation-1 attach -- the request the server \
answers with StaleAuthority {{ current_generation: 2 }}"
);
Ok(())
}
fn refuse_replay(
aggregate: ClientParticipantAggregate,
replay: crate::wire::AttachBound,
) -> TestResult<ClientInboundRefusalReason> {
let ClientCorrelatedInboundDecision::Refused(refusal) = decide_correlated_inbound(
aggregate,
ServerValue::UnboundReceipt(ReceiptReplay::CredentialAttach(replay)),
ClientResponseCorrelation { authorization: 1 },
) else {
return Err("this receipt replay must NOT be applied to the aggregate");
};
Ok(refusal.reason())
}
#[test]
fn a_foreign_participants_receipt_never_reaches_the_adopting_arm() -> TestResult {
const FOREIGN: u64 = 999;
let mut aggregate = detached_at(1, STALE_SECRET)?;
aggregate.expected = Some(expected_attach(FOREIGN, 1, STALE_SECRET)?);
aggregate.next_operation_authorization = 1;
assert_eq!(
refuse_replay(aggregate, receipt(FOREIGN, 1, 2, ROTATED_SECRET)?)?,
ClientInboundRefusalReason::ForeignResponse,
"a receipt for participant 999 must be refused before it can rewrite participant 142"
);
Ok(())
}
#[test]
fn a_receipt_replay_never_reaches_the_arm_after_a_durable_leave() -> TestResult {
let mut aggregate = ClientParticipantAggregate::new();
aggregate.binding = ClientBindingState::Left {
conversation_id: CONVERSATION,
participant_id: PARTICIPANT,
generation: generation(1)?,
};
aggregate.expected = Some(expected_attach(PARTICIPANT, 1, STALE_SECRET)?);
aggregate.next_operation_authorization = 1;
let reason = refuse_replay(aggregate, receipt(PARTICIPANT, 1, 2, ROTATED_SECRET)?)?;
assert!(
matches!(
reason,
ClientInboundRefusalReason::AlreadyDead | ClientInboundRefusalReason::ForeignResponse
),
"a durable Leave is permanent; a receipt replay must be refused before the adopting arm, \
got {reason:?}"
);
Ok(())
}
#[test]
fn an_applied_attach_receipt_can_only_ever_grant_the_next_generation() -> TestResult {
assert!(
crate::wire::AttachBound::ordinary(
CONVERSATION,
AttachAttemptToken::new([CHURN_TOKEN; 16]),
PARTICIPANT,
generation(5)?,
AttachSecret::new([ROTATED_SECRET; 32]),
epoch(2)?,
0,
0,
0,
)
.is_none(),
"a receipt granting a generation BELOW its presented one must not be constructible -- if \
it ever becomes so, the adopting arm needs the monotonicity guard this pin retires"
);
let mut aggregate = detached_at(4, STALE_SECRET)?;
aggregate.expected = Some(expected_attach(PARTICIPANT, 4, STALE_SECRET)?);
aggregate.next_operation_authorization = 1;
let aggregate = consume_replay(aggregate, receipt(PARTICIPANT, 4, 5, ROTATED_SECRET)?)?;
assert_eq!(
credential(&aggregate).map(|held| held.0),
Some(generation(5)?),
"the applied grant must be exactly the successor of the retained generation"
);
Ok(())
}