use std::error::Error;
use std::thread::sleep;
use std::time::Duration;
use liminal_protocol::wire::{
AttachSecret, ConnectionIncarnation, ReceiptCapacityExceeded, ReceiptCapacityScope,
ServerValue, StaleAuthority, StaleOrUnknownReceipt,
};
use super::ProductionParticipantHandler;
use super::tests::{dispatch, open_disk_store_for_tests, test_participant_config};
use super::tests_capacity::capacity_config;
use super::tests_receipts::{GEN_ONE, attach, attach_request, detach, enroll, generation};
fn assert_attach_receipt_refusal(
value: &ServerValue,
conversation_id: u64,
scope: ReceiptCapacityScope,
limit: u64,
occupied: u64,
) -> Result<(), Box<dyn Error>> {
let ServerValue::ReceiptCapacityExceeded(ReceiptCapacityExceeded::CredentialAttach {
request,
scope: got_scope,
limit: got_limit,
occupied: got_occupied,
}) = value
else {
return Err(format!(
"expected the credential-attach ReceiptCapacityExceeded row ({scope:?}), got: \
{value:?}"
)
.into());
};
assert_eq!(request.conversation_id, conversation_id);
assert_eq!(*got_scope, scope);
assert_eq!(*got_limit, limit);
assert_eq!(*got_occupied, occupied);
Ok(())
}
#[test]
fn attach_live_receipt_server_scope_refusal() -> Result<(), Box<dyn Error>> {
let home = tempfile::tempdir()?;
let data_dir = home.path().join("durability");
let incarnation = ConnectionIncarnation::new(75, 1);
let store = open_disk_store_for_tests(&data_dir)?;
let config = capacity_config(|c| c.max_live_attach_receipts_server = 1);
let handler = ProductionParticipantHandler::new(store, config)?;
let conversation_id = 741;
let receipt = enroll(&handler, incarnation, conversation_id, [41; 16])?;
let participant_id = receipt.participant_id();
detach(
&handler,
incarnation,
conversation_id,
participant_id,
GEN_ONE,
[42; 16],
)?;
let refused = dispatch(
&handler,
incarnation,
attach_request(
conversation_id,
participant_id,
GEN_ONE,
receipt.attach_secret(),
[43; 16],
),
)?;
assert_attach_receipt_refusal(
&refused,
conversation_id,
ReceiptCapacityScope::LiveReceiptServer,
1,
1,
)
}
#[test]
fn attach_live_receipt_participant_scope_refusal() -> Result<(), Box<dyn Error>> {
let home = tempfile::tempdir()?;
let data_dir = home.path().join("durability");
let incarnation = ConnectionIncarnation::new(76, 1);
let store = open_disk_store_for_tests(&data_dir)?;
let config = capacity_config(|c| c.max_live_attach_receipts_per_participant = 1);
let handler = ProductionParticipantHandler::new(store, config)?;
let conversation_id = 742;
let receipt = enroll(&handler, incarnation, conversation_id, [44; 16])?;
let participant_id = receipt.participant_id();
detach(
&handler,
incarnation,
conversation_id,
participant_id,
GEN_ONE,
[45; 16],
)?;
let refused = dispatch(
&handler,
incarnation,
attach_request(
conversation_id,
participant_id,
GEN_ONE,
receipt.attach_secret(),
[46; 16],
),
)?;
assert_attach_receipt_refusal(
&refused,
conversation_id,
ReceiptCapacityScope::LiveReceiptParticipant,
1,
1,
)
}
#[test]
fn attach_provenance_scope_refusals_follow_the_fixed_order() -> Result<(), Box<dyn Error>> {
type Mutator = fn(&mut crate::config::types::ParticipantConfig);
let cases: [(u64, Mutator, ReceiptCapacityScope); 3] = [
(
743,
|c| c.max_receipt_provenance_server = 1,
ReceiptCapacityScope::ProvenanceServer,
),
(
744,
|c| c.max_receipt_provenance_per_conversation = 1,
ReceiptCapacityScope::ProvenanceConversation,
),
(
745,
|c| c.max_receipt_provenance_per_participant = 1,
ReceiptCapacityScope::ProvenanceParticipant,
),
];
for (conversation_id, mutate, scope) in cases {
let home = tempfile::tempdir()?;
let data_dir = home.path().join("durability");
let incarnation = ConnectionIncarnation::new(77, 1);
let store = open_disk_store_for_tests(&data_dir)?;
let handler = ProductionParticipantHandler::new(store, capacity_config(mutate))?;
let receipt = enroll(&handler, incarnation, conversation_id, [47; 16])?;
let participant_id = receipt.participant_id();
detach(
&handler,
incarnation,
conversation_id,
participant_id,
GEN_ONE,
[48; 16],
)?;
let refused = dispatch(
&handler,
incarnation,
attach_request(
conversation_id,
participant_id,
GEN_ONE,
receipt.attach_secret(),
[49; 16],
),
)?;
assert_attach_receipt_refusal(&refused, conversation_id, scope, 1, 1)?;
}
Ok(())
}
#[test]
fn full_provenance_participant_scope_refuses_and_in_window_unknown_is_stale_authority()
-> Result<(), Box<dyn Error>> {
let home = tempfile::tempdir()?;
let data_dir = home.path().join("durability");
let incarnation = ConnectionIncarnation::new(78, 1);
let store = open_disk_store_for_tests(&data_dir)?;
let config = capacity_config(|c| c.max_receipt_provenance_per_participant = 2);
let handler = ProductionParticipantHandler::new(store, config)?;
let conversation_id = 746;
let receipt = enroll(&handler, incarnation, conversation_id, [50; 16])?;
let participant_id = receipt.participant_id();
detach(
&handler,
incarnation,
conversation_id,
participant_id,
GEN_ONE,
[51; 16],
)?;
let first = attach(
&handler,
incarnation,
attach_request(
conversation_id,
participant_id,
GEN_ONE,
receipt.attach_secret(),
[52; 16],
),
)?;
assert_eq!(first.capability_generation(), generation(2)?);
detach(
&handler,
incarnation,
conversation_id,
participant_id,
generation(2)?,
[53; 16],
)?;
let refused = dispatch(
&handler,
incarnation,
attach_request(
conversation_id,
participant_id,
generation(2)?,
first.attach_secret(),
[54; 16],
),
)?;
assert_attach_receipt_refusal(
&refused,
conversation_id,
ReceiptCapacityScope::ProvenanceParticipant,
2,
2,
)?;
let unknown_in_window = dispatch(
&handler,
incarnation,
attach_request(
conversation_id,
participant_id,
GEN_ONE,
AttachSecret::new([0xAB; 32]),
[55; 16],
),
)?;
let ServerValue::StaleAuthority(StaleAuthority::Live {
current_generation, ..
}) = unknown_in_window
else {
return Err(format!(
"an unknown old token inside the fingerprint window must keep the StaleAuthority \
no-commit proof, got: {unknown_in_window:?}"
)
.into());
};
assert_eq!(current_generation, generation(2)?);
Ok(())
}
#[test]
fn attach_over_limit_scope_refuses_with_true_numbers() -> Result<(), Box<dyn Error>> {
let home = tempfile::tempdir()?;
let data_dir = home.path().join("durability");
let conversation_id = 748;
let participant_id;
let secret;
{
let store = open_disk_store_for_tests(&data_dir)?;
let handler = ProductionParticipantHandler::new(store, test_participant_config())?;
let incarnation = ConnectionIncarnation::new(82, 1);
let receipt = enroll(&handler, incarnation, conversation_id, [61; 16])?;
participant_id = receipt.participant_id();
secret = receipt.attach_secret();
enroll(&handler, incarnation, 749, [62; 16])?;
}
let store = open_disk_store_for_tests(&data_dir)?;
let config = capacity_config(|c| c.max_receipt_provenance_server = 1);
let handler = ProductionParticipantHandler::new(store, config)?;
let refused = dispatch(
&handler,
ConnectionIncarnation::new(82, 2),
attach_request(conversation_id, participant_id, GEN_ONE, secret, [63; 16]),
)?;
assert_attach_receipt_refusal(
&refused,
conversation_id,
ReceiptCapacityScope::ProvenanceServer,
1,
2,
)
}
#[test]
fn attach_mixed_full_and_over_limit_refuses_the_earlier_full_scope() -> Result<(), Box<dyn Error>> {
let home = tempfile::tempdir()?;
let data_dir = home.path().join("durability");
let conversation_id = 750;
let participant_id;
let secret;
{
let store = open_disk_store_for_tests(&data_dir)?;
let handler = ProductionParticipantHandler::new(store, test_participant_config())?;
let incarnation = ConnectionIncarnation::new(83, 1);
let receipt = enroll(&handler, incarnation, conversation_id, [64; 16])?;
participant_id = receipt.participant_id();
secret = receipt.attach_secret();
enroll(&handler, incarnation, 751, [65; 16])?;
}
let store = open_disk_store_for_tests(&data_dir)?;
let config = capacity_config(|c| {
c.max_live_attach_receipts_server = 2;
c.max_receipt_provenance_server = 1;
});
let handler = ProductionParticipantHandler::new(store, config)?;
let refused = dispatch(
&handler,
ConnectionIncarnation::new(83, 2),
attach_request(conversation_id, participant_id, GEN_ONE, secret, [66; 16]),
)?;
assert_attach_receipt_refusal(
&refused,
conversation_id,
ReceiptCapacityScope::LiveReceiptServer,
2,
2,
)
}
#[test]
fn expired_provenance_prunes_freeing_capacity_and_degrading_exact_old_tokens()
-> Result<(), Box<dyn Error>> {
let home = tempfile::tempdir()?;
let data_dir = home.path().join("durability");
let incarnation = ConnectionIncarnation::new(79, 1);
let store = open_disk_store_for_tests(&data_dir)?;
let config = capacity_config(|c| {
c.attach_receipt_ttl_ms = 1_000;
c.receipt_provenance_ttl_ms = 1_200;
c.max_receipt_provenance_per_participant = 2;
});
let handler = ProductionParticipantHandler::new(store, config)?;
let conversation_id = 747;
let receipt = enroll(&handler, incarnation, conversation_id, [56; 16])?;
let participant_id = receipt.participant_id();
detach(
&handler,
incarnation,
conversation_id,
participant_id,
GEN_ONE,
[57; 16],
)?;
let first_request = attach_request(
conversation_id,
participant_id,
GEN_ONE,
receipt.attach_secret(),
[58; 16],
);
let first = attach(&handler, incarnation, first_request.clone())?;
assert_eq!(first.capability_generation(), generation(2)?);
detach(
&handler,
incarnation,
conversation_id,
participant_id,
generation(2)?,
[59; 16],
)?;
sleep(Duration::from_millis(2_000));
let second = attach(
&handler,
incarnation,
attach_request(
conversation_id,
participant_id,
generation(2)?,
first.attach_secret(),
[60; 16],
),
)?;
assert_eq!(second.capability_generation(), generation(3)?);
let exact_old = dispatch(&handler, incarnation, first_request)?;
let ServerValue::StaleOrUnknownReceipt(StaleOrUnknownReceipt {
presented_generation,
current_generation,
..
}) = exact_old
else {
return Err(format!(
"a pruned exact old token must answer StaleOrUnknownReceipt, got: {exact_old:?}"
)
.into());
};
assert_eq!(presented_generation, GEN_ONE);
assert_eq!(current_generation, generation(3)?);
Ok(())
}