use std::error::Error;
use liminal_protocol::wire::{
AttachAttemptToken, ClientRequest, CredentialAttachRequest, DetachAttemptToken, DetachRequest,
EnrollmentRequest, EnrollmentToken, Generation, LeaveAttemptToken, LeaveRequest,
RecordAdmission, RecordAdmissionAttemptToken, ServerValue,
};
use super::e2e_cold_all_shapes_fixture::{ColdMember, expect_enrolled};
use super::e2e_tests::SocketFixture;
const CONVERSATION: u64 = 777;
#[test]
fn a_seat_retired_by_its_own_leave_re_enrols_and_its_attach_on_a_new_connection_is_answered()
-> Result<(), Box<dyn Error>> {
let data_dir = tempfile::tempdir()?;
let mut first = SocketFixture::start(data_dir.path())?;
let retired = ColdMember::enrolled(&expect_enrolled(
first.request(ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: CONVERSATION,
enrollment_token: EnrollmentToken::new([0x71; 16]),
}))?,
"first identity",
)?);
let left = first.request(ClientRequest::Leave(LeaveRequest {
conversation_id: CONVERSATION,
participant_id: retired.participant_id,
capability_generation: retired.generation,
attach_secret: retired.secret,
leave_attempt_token: LeaveAttemptToken::new([0x72; 16]),
}))?;
assert!(matches!(left, ServerValue::LeaveCommitted(_)), "{left:?}");
let mut reopened = first.spawn_peer()?;
let successor = ColdMember::enrolled(&expect_enrolled(
reopened.request(ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: CONVERSATION,
enrollment_token: EnrollmentToken::new([0x73; 16]),
}))?,
"successor",
)?);
assert_ne!(successor.participant_id, retired.participant_id);
let mut attaching = first.spawn_peer()?;
let attached = attaching.request(ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: CONVERSATION,
participant_id: successor.participant_id,
capability_generation: successor.generation,
attach_secret: successor.secret,
attach_attempt_token: AttachAttemptToken::new([0x74; 16]),
accept_marker_delivery_seq: None,
}))?;
let ServerValue::AttachBound(_) = attached else {
return Err(format!("the successor's attach was not bound: {attached:?}").into());
};
Ok(())
}
#[test]
fn a_seat_retired_by_its_own_leave_re_enrols_and_its_attach_on_the_same_connection_is_answered()
-> Result<(), Box<dyn Error>> {
let data_dir = tempfile::tempdir()?;
let mut first = SocketFixture::start(data_dir.path())?;
let retired = ColdMember::enrolled(&expect_enrolled(
first.request(ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: CONVERSATION,
enrollment_token: EnrollmentToken::new([0x75; 16]),
}))?,
"first identity",
)?);
let left = first.request(ClientRequest::Leave(LeaveRequest {
conversation_id: CONVERSATION,
participant_id: retired.participant_id,
capability_generation: retired.generation,
attach_secret: retired.secret,
leave_attempt_token: LeaveAttemptToken::new([0x76; 16]),
}))?;
assert!(matches!(left, ServerValue::LeaveCommitted(_)), "{left:?}");
let mut reopened = first.spawn_peer()?;
let successor = ColdMember::enrolled(&expect_enrolled(
reopened.request(ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: CONVERSATION,
enrollment_token: EnrollmentToken::new([0x77; 16]),
}))?,
"successor",
)?);
let attached = reopened.request(ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: CONVERSATION,
participant_id: successor.participant_id,
capability_generation: successor.generation,
attach_secret: successor.secret,
attach_attempt_token: AttachAttemptToken::new([0x78; 16]),
accept_marker_delivery_seq: None,
}))?;
assert!(
matches!(
attached,
ServerValue::AttachBound(_) | ServerValue::ConnectionConversationBindingOccupied(_)
),
"the attach on the enrolling connection was neither bound nor refused by name: {attached:?}"
);
Ok(())
}
#[test]
fn the_doors_next_open_after_a_detached_leave_presents_re_enrols_and_attaches_on_one_connection()
-> Result<(), Box<dyn Error>> {
let data_dir = tempfile::tempdir()?;
let mut first = SocketFixture::start(data_dir.path())?;
let retired = ColdMember::enrolled(&expect_enrolled(
first.request(ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: CONVERSATION,
enrollment_token: EnrollmentToken::new([0x79; 16]),
}))?,
"first identity",
)?);
let detached = first.request(ClientRequest::Detach(DetachRequest {
conversation_id: CONVERSATION,
participant_id: retired.participant_id,
capability_generation: retired.generation,
detach_attempt_token: DetachAttemptToken::new([0x7a; 16]),
}))?;
assert!(
matches!(detached, ServerValue::DetachCommitted(_)),
"{detached:?}"
);
let mut leaving = first.spawn_peer()?;
let left = leaving.request(ClientRequest::Leave(LeaveRequest {
conversation_id: CONVERSATION,
participant_id: retired.participant_id,
capability_generation: retired.generation,
attach_secret: retired.secret,
leave_attempt_token: LeaveAttemptToken::new([0x7b; 16]),
}))?;
assert!(matches!(left, ServerValue::LeaveCommitted(_)), "{left:?}");
let mut next = first.spawn_peer()?;
let dead = next.request(ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: CONVERSATION,
participant_id: retired.participant_id,
capability_generation: retired.generation,
attach_secret: retired.secret,
attach_attempt_token: AttachAttemptToken::new([0x7c; 16]),
accept_marker_delivery_seq: None,
}))?;
assert!(
!matches!(dead, ServerValue::AttachBound(_)),
"the retired credential attached: {dead:?}"
);
let successor = ColdMember::enrolled(&expect_enrolled(
next.request(ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: CONVERSATION,
enrollment_token: EnrollmentToken::new([0x7d; 16]),
}))?,
"successor",
)?);
assert_ne!(successor.participant_id, retired.participant_id);
let attached = next
.request(ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: CONVERSATION,
participant_id: successor.participant_id,
capability_generation: successor.generation,
attach_secret: successor.secret,
attach_attempt_token: AttachAttemptToken::new([0x7e; 16]),
accept_marker_delivery_seq: None,
}))
.map_err(|error| format!("the successor's attach was not answered (dead credential answer was {dead:?}): {error}"))?;
assert!(
matches!(
attached,
ServerValue::AttachBound(_) | ServerValue::ConnectionConversationBindingOccupied(_)
),
"the successor's attach was neither bound nor refused by name: {attached:?}"
);
Ok(())
}
#[test]
fn an_attach_with_a_generation_the_participant_never_reached_is_refused_by_name()
-> Result<(), Box<dyn Error>> {
let data_dir = tempfile::tempdir()?;
let mut first = SocketFixture::start(data_dir.path())?;
let retired = ColdMember::enrolled(&expect_enrolled(
first.request(ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: CONVERSATION,
enrollment_token: EnrollmentToken::new([0x81; 16]),
}))?,
"first identity",
)?);
let left = first.request(ClientRequest::Leave(LeaveRequest {
conversation_id: CONVERSATION,
participant_id: retired.participant_id,
capability_generation: retired.generation,
attach_secret: retired.secret,
leave_attempt_token: LeaveAttemptToken::new([0x82; 16]),
}))?;
assert!(matches!(left, ServerValue::LeaveCommitted(_)), "{left:?}");
let mut next = first.spawn_peer()?;
let successor = ColdMember::enrolled(&expect_enrolled(
next.request(ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: CONVERSATION,
enrollment_token: EnrollmentToken::new([0x83; 16]),
}))?,
"successor",
)?);
let beyond = Generation::new(successor.generation.get() + 1).ok_or("no next generation")?;
let answer = next
.request(ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: CONVERSATION,
participant_id: successor.participant_id,
capability_generation: beyond,
attach_secret: successor.secret,
attach_attempt_token: AttachAttemptToken::new([0x84; 16]),
accept_marker_delivery_seq: None,
}))
.map_err(|error| {
format!(
"the attach with generation {} was not answered: {error}",
beyond.get()
)
})?;
assert!(
!matches!(answer, ServerValue::AttachBound(_)),
"a generation the participant never reached was bound: {answer:?}"
);
let again = next.request(ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: CONVERSATION,
enrollment_token: EnrollmentToken::new([0x83; 16]),
}))?;
assert!(!matches!(again, ServerValue::AttachBound(_)), "{again:?}");
Ok(())
}
#[test]
fn a_marker_bearing_attach_by_a_successor_never_delivered_a_marker_is_refused_by_name()
-> Result<(), Box<dyn Error>> {
let data_dir = tempfile::tempdir()?;
let mut first = SocketFixture::start(data_dir.path())?;
let retired = ColdMember::enrolled(&expect_enrolled(
first.request(ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: CONVERSATION,
enrollment_token: EnrollmentToken::new([0x85; 16]),
}))?,
"first identity",
)?);
let left = first.request(ClientRequest::Leave(LeaveRequest {
conversation_id: CONVERSATION,
participant_id: retired.participant_id,
capability_generation: retired.generation,
attach_secret: retired.secret,
leave_attempt_token: LeaveAttemptToken::new([0x86; 16]),
}))?;
assert!(matches!(left, ServerValue::LeaveCommitted(_)), "{left:?}");
let mut next = first.spawn_peer()?;
let successor = ColdMember::enrolled(&expect_enrolled(
next.request(ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: CONVERSATION,
enrollment_token: EnrollmentToken::new([0x87; 16]),
}))?,
"successor",
)?);
let answer = next
.request(ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: CONVERSATION,
participant_id: successor.participant_id,
capability_generation: successor.generation,
attach_secret: successor.secret,
attach_attempt_token: AttachAttemptToken::new([0x88; 16]),
accept_marker_delivery_seq: Some(1),
}))
.map_err(|error| format!("the marker-bearing attach was not answered: {error}"))?;
assert!(
!matches!(answer, ServerValue::AttachBound(_)),
"a marker the successor was never delivered was accepted: {answer:?}"
);
Ok(())
}
#[test]
fn a_re_enrolled_seats_reattach_with_its_rotated_credential_after_a_record_is_answered()
-> Result<(), Box<dyn Error>> {
let data_dir = tempfile::tempdir()?;
let mut first = SocketFixture::start(data_dir.path())?;
let retired = ColdMember::enrolled(&expect_enrolled(
first.request(ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: CONVERSATION,
enrollment_token: EnrollmentToken::new([0x89; 16]),
}))?,
"first identity",
)?);
let detached = first.request(ClientRequest::Detach(DetachRequest {
conversation_id: CONVERSATION,
participant_id: retired.participant_id,
capability_generation: retired.generation,
detach_attempt_token: DetachAttemptToken::new([0x8a; 16]),
}))?;
assert!(
matches!(detached, ServerValue::DetachCommitted(_)),
"{detached:?}"
);
let mut leaving = first.spawn_peer()?;
let left = leaving.request(ClientRequest::Leave(LeaveRequest {
conversation_id: CONVERSATION,
participant_id: retired.participant_id,
capability_generation: retired.generation,
attach_secret: retired.secret,
leave_attempt_token: LeaveAttemptToken::new([0x8b; 16]),
}))?;
assert!(matches!(left, ServerValue::LeaveCommitted(_)), "{left:?}");
drop(leaving);
let mut reopened = first.spawn_peer()?;
let successor = ColdMember::enrolled(&expect_enrolled(
reopened.request(ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: CONVERSATION,
enrollment_token: EnrollmentToken::new([0x8c; 16]),
}))?,
"successor",
)?);
let mut attaching = first.spawn_peer()?;
let attached = attaching.request(ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: CONVERSATION,
participant_id: successor.participant_id,
capability_generation: successor.generation,
attach_secret: successor.secret,
attach_attempt_token: AttachAttemptToken::new([0x8d; 16]),
accept_marker_delivery_seq: None,
}))?;
let ServerValue::AttachBound(bound) = attached else {
return Err(format!("the successor's first attach was not bound: {attached:?}").into());
};
let rotated = (bound.capability_generation(), bound.attach_secret());
let committed = attaching.request(ClientRequest::RecordAdmission(RecordAdmission {
conversation_id: CONVERSATION,
participant_id: successor.participant_id,
capability_generation: rotated.0,
record_admission_attempt_token: RecordAdmissionAttemptToken::new([0x8e; 16]),
payload: vec![0xD4],
}))?;
assert!(
matches!(committed, ServerValue::RecordCommitted(_)),
"{committed:?}"
);
drop(attaching);
drop(reopened);
let mut again = first.spawn_peer()?;
let answer = again
.request(ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: CONVERSATION,
participant_id: successor.participant_id,
capability_generation: rotated.0,
attach_secret: rotated.1,
attach_attempt_token: AttachAttemptToken::new([0x8f; 16]),
accept_marker_delivery_seq: None,
}))
.map_err(|error| {
format!(
"the reattach as participant {} generation {} was not answered: {error}",
successor.participant_id,
rotated.0.get()
)
})?;
assert!(
matches!(answer, ServerValue::AttachBound(_)),
"the reattach with the rotated credential was not bound: {answer:?}"
);
Ok(())
}