liminal-protocol 0.2.0

Shared participant-lifecycle protocol types for liminal
Documentation
use super::*;
use crate::wire::{
    AttachAttemptToken, AttachSecret, BindingEpoch, ClientRequest, ConnectionIncarnation,
    CredentialAttachRequest, DetachAttemptToken, DetachEnvelope, DetachRequest, Generation,
    LeaveAttemptToken, LeaveRequest,
};

type TestResult<T = ()> = Result<T, &'static str>;

fn generation(value: u64) -> TestResult<Generation> {
    Generation::new(value).ok_or("generation must be nonzero")
}

fn epoch(value: u64) -> TestResult<BindingEpoch> {
    Ok(BindingEpoch::new(
        ConnectionIncarnation::new(81, 82),
        generation(value)?,
    ))
}

fn bound() -> TestResult<ClientParticipantAggregate> {
    let mut aggregate = ClientParticipantAggregate::new();
    aggregate.binding = ClientBindingState::Bound {
        conversation_id: 91,
        participant_id: 92,
        generation: generation(7)?,
        attach_secret: AttachSecret::new([93; 32]),
        binding_epoch: epoch(7)?,
    };
    Ok(aggregate)
}

fn detach(token: u8) -> TestResult<ClientRequest> {
    Ok(ClientRequest::Detach(DetachRequest {
        conversation_id: 91,
        participant_id: 92,
        capability_generation: generation(7)?,
        detach_attempt_token: DetachAttemptToken::new([token; 16]),
    }))
}

#[test]
fn active_replay_without_matching_expected_detach_refuses_restore() -> TestResult {
    let mut aggregate = bound()?;
    aggregate.detach_replay.state = replay::DetachReplayState::Recorded {
        request: DetachEnvelope {
            conversation_id: 91,
            participant_id: 92,
            capability_generation: generation(7)?,
            detach_attempt_token: DetachAttemptToken::new([94; 16]),
        },
        status: DetachReplayStatus::Parked,
    };
    let record = aggregate
        .resume_record()
        .map_err(|_| "uncoupled fixture must encode inertly")?;
    assert_eq!(
        record.restore(),
        Err(ClientResumeRestoreError::ActiveReplayExpectedDetachMismatch)
    );
    Ok(())
}

#[test]
fn refused_detach_admission_preserves_authorization_counter() -> TestResult {
    let mut aggregate = bound()?;
    aggregate.next_operation_authorization = 17;
    aggregate.detach_replay.state = replay::DetachReplayState::Recorded {
        request: DetachEnvelope {
            conversation_id: 91,
            participant_id: 92,
            capability_generation: generation(7)?,
            detach_attempt_token: DetachAttemptToken::new([95; 16]),
        },
        status: DetachReplayStatus::Parked,
    };
    let ClientOperationRecordDecision::Refused(refusal) = record_operation(aggregate, detach(96)?)
    else {
        return Err("different active detach must refuse");
    };
    assert_eq!(
        refusal.reason(),
        ClientOperationRecordRefusalReason::DetachReplayOutstanding
    );
    let (aggregate, _) = refusal.into_parts();
    assert_eq!(aggregate.next_operation_authorization, 17);
    Ok(())
}

#[test]
fn same_identity_wrong_secret_attach_and_leave_refuse() -> TestResult {
    let attach = ClientRequest::CredentialAttach(CredentialAttachRequest {
        conversation_id: 91,
        participant_id: 92,
        capability_generation: generation(7)?,
        attach_secret: AttachSecret::new([97; 32]),
        attach_attempt_token: AttachAttemptToken::new([98; 16]),
        accept_marker_delivery_seq: None,
    });
    let ClientOperationRecordDecision::Refused(refusal) = record_operation(bound()?, attach) else {
        return Err("wrong-secret attach must refuse");
    };
    assert_eq!(
        refusal.reason(),
        ClientOperationRecordRefusalReason::BindingMismatch
    );

    let leave = ClientRequest::Leave(LeaveRequest {
        conversation_id: 91,
        participant_id: 92,
        capability_generation: generation(7)?,
        attach_secret: AttachSecret::new([99; 32]),
        leave_attempt_token: LeaveAttemptToken::new([100; 16]),
    });
    let ClientOperationRecordDecision::Refused(refusal) = record_operation(bound()?, leave) else {
        return Err("wrong-secret leave must refuse");
    };
    assert_eq!(
        refusal.reason(),
        ClientOperationRecordRefusalReason::BindingMismatch
    );
    Ok(())
}