use crate::types::{AuthorizationStatus, Identifier, OrderStatus};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Challenge {
#[serde(rename = "type")]
pub challenge_type: String,
pub url: String,
pub status: String,
pub token: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_authorization: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validation: Option<String>,
#[serde(default)]
pub updated: Option<String>,
#[serde(default)]
pub error: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Authorization {
pub identifier: Identifier,
pub status: String,
pub expires: String,
pub challenges: Vec<Challenge>,
#[serde(default)]
pub wildcard: Option<bool>,
#[serde(default)]
pub combined_challenges: Option<Vec<Challenge>>,
}
impl Authorization {
pub fn get_challenge(&self, challenge_type: &str) -> Option<&Challenge> {
self.challenges
.iter()
.find(|c| c.challenge_type == challenge_type)
}
pub fn status_enum(&self) -> Option<AuthorizationStatus> {
self.status.parse().ok()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Order {
pub status: String,
pub expires: String,
pub identifiers: Vec<Identifier>,
pub authorizations: Vec<String>,
pub finalize: String,
#[serde(default)]
pub certificate: Option<String>,
#[serde(skip)]
pub combined_authorizations: Option<Vec<Authorization>>,
}
impl Order {
pub fn status_enum(&self) -> Option<OrderStatus> {
self.status.parse().ok()
}
pub fn is_ready(&self) -> bool {
matches!(self.status_enum(), Some(OrderStatus::Ready))
}
pub fn is_valid(&self) -> bool {
matches!(self.status_enum(), Some(OrderStatus::Valid))
}
pub fn is_pending(&self) -> bool {
matches!(self.status_enum(), Some(OrderStatus::Pending))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewOrderRequest {
pub identifiers: Vec<Identifier>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "notBefore")]
pub not_before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "notAfter")]
pub not_after: Option<String>,
}
impl NewOrderRequest {
pub fn new(domains: Vec<String>) -> Self {
tracing::debug!("Creating NewOrderRequest for domains: {:?}", domains);
let identifiers = domains.into_iter().map(Identifier::dns).collect();
Self {
identifiers,
not_before: None,
not_after: None,
}
}
pub fn with_not_before(mut self, not_before: String) -> Self {
self.not_before = Some(not_before);
self
}
pub fn with_not_after(mut self, not_after: String) -> Self {
self.not_after = Some(not_after);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FinalizationRequest {
pub csr: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_challenge_parsing() {
let json = r#"{
"type": "http-01",
"url": "https://example.com/acme/challenge/123",
"status": "pending",
"token": "test-token"
}"#;
let challenge: Challenge = serde_json::from_str(json).expect("Failed to parse challenge");
assert_eq!(challenge.challenge_type, "http-01");
assert_eq!(challenge.token, "test-token");
}
#[test]
fn test_authorization_get_challenge() {
let json = r#"{
"identifier": {"type": "dns", "value": "example.com"},
"status": "pending",
"expires": "2024-01-01T00:00:00Z",
"challenges": [
{
"type": "http-01",
"url": "https://example.com/acme/challenge/1",
"status": "pending",
"token": "token1"
},
{
"type": "dns-01",
"url": "https://example.com/acme/challenge/2",
"status": "pending",
"token": "token2"
}
]
}"#;
let auth: Authorization =
serde_json::from_str(json).expect("Failed to parse authorization");
assert!(auth.get_challenge("http-01").is_some());
assert!(auth.get_challenge("dns-01").is_some());
assert!(auth.get_challenge("tls-alpn-01").is_none());
}
#[test]
fn test_order_status_checks() {
let mut order: Order = serde_json::from_str(
r#"{
"status": "pending",
"expires": "2024-01-01T00:00:00Z",
"identifiers": [{"type": "dns", "value": "example.com"}],
"authorizations": ["https://example.com/acme/authz/1"],
"finalize": "https://example.com/acme/finalize/1"
}"#,
)
.expect("Failed to parse order");
assert!(order.is_pending());
assert!(!order.is_ready());
assert!(!order.is_valid());
order.status = "ready".to_string();
assert!(!order.is_pending());
assert!(order.is_ready());
assert!(!order.is_valid());
order.status = "valid".to_string();
assert!(!order.is_pending());
assert!(!order.is_ready());
assert!(order.is_valid());
}
#[test]
fn test_new_order_request() {
let req = NewOrderRequest::new(vec![
"example.com".to_string(),
"www.example.com".to_string(),
]);
assert_eq!(req.identifiers.len(), 2);
assert_eq!(req.identifiers[0].value, "example.com");
assert_eq!(req.identifiers[0].id_type, "dns");
}
}