use super::*;
#[derive(Debug)]
struct RecordingReissuer {
outcome: crate::health::reissue::OperatorCredentialReissueOutcome,
seen: Mutex<Vec<OperatorCredentialReissueRequest>>,
}
impl OperatorCredentialReissuer for RecordingReissuer {
fn reissue(
&self,
request: OperatorCredentialReissueRequest,
) -> Result<
crate::health::reissue::OperatorCredentialReissueOutcome,
crate::health::reissue::OperatorCredentialReissueError,
> {
self.seen
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(request);
Ok(self.outcome.clone())
}
}
fn served_with_reissuer(
outcome: crate::health::reissue::OperatorCredentialReissueOutcome,
) -> (ServedState, Arc<RecordingReissuer>) {
let reissuer = Arc::new(RecordingReissuer {
outcome,
seen: Mutex::new(Vec::new()),
});
let state = served(SharedReadinessState::default());
state
.reissue
.install(Arc::clone(&reissuer) as Arc<dyn OperatorCredentialReissuer>);
(state, reissuer)
}
const REISSUE_TARGET: &str = "/operator/credential-reissue?conversation_id=7&participant_id=3&\
expected_current_generation=14";
#[test]
fn the_reissue_route_reports_an_uninstalled_participant() -> Result<(), Box<dyn std::error::Error>>
{
let request = format!("POST {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
let response =
response_for_request(request.as_bytes(), &served(SharedReadinessState::default()))?;
let response = String::from_utf8(response)?;
assert_status(&response, 503);
Ok(())
}
#[test]
fn the_reissue_route_carries_the_three_inputs_and_returns_the_secret_once()
-> Result<(), Box<dyn std::error::Error>> {
let issued = crate::health::reissue::OperatorCredentialReissued {
conversation_id: 7,
participant_id: 3,
presented_generation: 14,
issued_generation: 15,
attach_secret: crate::health::reissue::encode_hex(&[0x5A; 32]),
};
let (state, reissuer) = served_with_reissuer(
crate::health::reissue::OperatorCredentialReissueOutcome::Issued(issued),
);
let request = format!("POST {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
assert_status(&response, 200);
let body = json_body(&response)?;
assert_eq!(body["issued_generation"], 15);
assert_eq!(body["presented_generation"], 14);
assert_eq!(body["attach_secret"], "5a".repeat(32));
let seen = reissuer
.seen
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
assert_eq!(
seen.as_slice(),
[OperatorCredentialReissueRequest {
conversation_id: 7,
participant_id: 3,
expected_current_generation: 14,
}]
);
Ok(())
}
#[test]
fn the_reissue_route_serves_the_normative_generation_pair() -> Result<(), Box<dyn std::error::Error>>
{
let (state, _) = served_with_reissuer(
crate::health::reissue::OperatorCredentialReissueOutcome::Refused(
OperatorCredentialReissueRefusal::GenerationMismatch {
conversation_id: 7,
participant_id: 3,
presented_generation: 14,
current_generation: 15,
},
),
);
let request = format!("POST {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
assert_status(&response, 409);
let body = json_body(&response)?;
assert_eq!(body["refusal"], "generation_mismatch");
assert_eq!(body["presented_generation"], 14);
assert_eq!(body["current_generation"], 15);
Ok(())
}
#[test]
fn the_reissue_route_answers_a_lookup_miss_with_not_found() -> Result<(), Box<dyn std::error::Error>>
{
let (state, _) = served_with_reissuer(
crate::health::reissue::OperatorCredentialReissueOutcome::Refused(
OperatorCredentialReissueRefusal::ConversationUnknown { conversation_id: 7 },
),
);
let request = format!("POST {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
assert_status(&response, 404);
let body = json_body(&response)?;
assert_eq!(body["refusal"], "conversation_unknown");
assert_eq!(body["conversation_id"], 7);
assert!(
body.get("participant_id").is_none(),
"an unknown-conversation refusal must disclose nothing beyond the presented \
conversation id: {body}"
);
Ok(())
}
#[test]
fn a_malformed_reissue_call_is_refused_and_never_reaches_the_authority()
-> Result<(), Box<dyn std::error::Error>> {
let (state, reissuer) = served_with_reissuer(
crate::health::reissue::OperatorCredentialReissueOutcome::Refused(
OperatorCredentialReissueRefusal::ConversationUnknown { conversation_id: 7 },
),
);
let malformed = [
"/operator/credential-reissue?conversation_id=7&participant_id=3",
"/operator/credential-reissue",
"/operator/credential-reissue?conversation_id=7&participant_id=3&\
expected_current_generation=fourteen",
"/operator/credential-reissue?conversation_id=7&participant_id=3&\
expected_current_generation=14&force=1",
"/operator/credential-reissue?conversation_id=7&conversation_id=8&\
participant_id=3&expected_current_generation=14",
];
for target in malformed {
let request = format!("POST {target} HTTP/1.1\r\n\r\n");
let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
assert_status(&response, 400);
}
assert!(
reissuer
.seen
.lock()
.unwrap_or_else(PoisonError::into_inner)
.is_empty(),
"a malformed call must never reach the serialized participant-state point"
);
Ok(())
}
#[test]
fn the_reissue_route_refuses_every_other_method() -> Result<(), Box<dyn std::error::Error>> {
let (state, reissuer) = served_with_reissuer(
crate::health::reissue::OperatorCredentialReissueOutcome::Refused(
OperatorCredentialReissueRefusal::ConversationUnknown { conversation_id: 7 },
),
);
for method in ["GET", "PUT", "DELETE"] {
let request = format!("{method} {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
assert_status(&response, 405);
}
assert!(
reissuer
.seen
.lock()
.unwrap_or_else(PoisonError::into_inner)
.is_empty()
);
Ok(())
}
#[test]
fn the_reissue_route_did_not_move_any_existing_routes_answer()
-> Result<(), Box<dyn std::error::Error>> {
let state = served(SharedReadinessState::default());
for target in [
"/health?x=1",
"/ready?x=1",
"/metrics?x=1",
"/unloadable-conversations?x=1",
] {
let request = format!("GET {target} HTTP/1.1\r\n\r\n");
let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
assert_status(&response, 404);
}
let response = String::from_utf8(response_for_request(
b"GET /health HTTP/1.1\r\n\r\n",
&state,
)?)?;
assert_status(&response, 200);
Ok(())
}