use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::profiles::TrustProfile;
use crate::roster::EnrollmentState;
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct JoinRequest {
pub hostname: String,
pub auth: koi_crypto::auth::AuthResponse,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sans: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct JoinResponse {
pub hostname: String,
pub ca_cert: String,
pub service_cert: String,
pub service_key: String,
pub ca_fingerprint: String,
pub cert_path: String,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct CertmeshStatus {
pub ca_initialized: bool,
pub ca_locked: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub ca_fingerprint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auth_method: Option<String>,
pub profile: TrustProfile,
pub enrollment_state: EnrollmentState,
#[serde(skip_serializing_if = "Option::is_none")]
pub enrollment_deadline: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_subnet: Option<String>,
pub member_count: usize,
pub members: Vec<MemberSummary>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct MemberSummary {
pub hostname: String,
pub role: String,
pub status: String,
pub cert_fingerprint: String,
pub cert_expires: String,
}
#[derive(Debug, Clone)]
pub struct CaAnnouncement {
pub name: String,
pub port: u16,
pub txt: HashMap<String, String>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct SetHookRequest {
pub hostname: String,
pub reload: String,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct SetHookResponse {
pub hostname: String,
pub reload: String,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct CreateCaRequest {
pub passphrase: String,
pub entropy_hex: String,
pub profile: TrustProfile,
#[serde(skip_serializing_if = "Option::is_none")]
pub operator: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enrollment_open: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires_approval: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub totp_secret_hex: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct CreateCaResponse {
pub auth_setup: koi_crypto::auth::AuthSetup,
pub ca_fingerprint: String,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UnlockRequest {
pub passphrase: String,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UnlockResponse {
pub success: bool,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct RotateAuthRequest {
pub passphrase: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct RotateAuthResponse {
pub auth_setup: koi_crypto::auth::AuthSetup,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct AuditLogResponse {
pub entries: String,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct DestroyResponse {
pub destroyed: bool,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct BackupRequest {
pub ca_passphrase: String,
pub backup_passphrase: String,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct BackupResponse {
pub backup_hex: String,
pub format: String,
pub version: u16,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct RestoreRequest {
pub backup_hex: String,
pub backup_passphrase: String,
pub new_passphrase: String,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct RestoreResponse {
pub restored: bool,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct RevokeRequest {
pub hostname: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub operator: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct RevokeResponse {
pub revoked: bool,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct PolicyRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_subnet: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct OpenEnrollmentRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub deadline: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct PolicySummary {
pub enrollment_state: EnrollmentState,
#[serde(skip_serializing_if = "Option::is_none")]
pub enrollment_deadline: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_subnet: Option<String>,
pub profile: TrustProfile,
pub requires_approval: bool,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct ComplianceResponse {
pub policy: PolicySummary,
pub audit_entries: usize,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct PromoteRequest {
pub auth: koi_crypto::auth::AuthResponse,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "optional_byte_array"
)]
pub ephemeral_public: Option<[u8; 32]>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct PromoteResponse {
pub encrypted_ca_key: koi_crypto::keys::EncryptedKey,
pub auth_data: serde_json::Value,
pub roster_json: String,
pub ca_cert_pem: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "optional_byte_array"
)]
pub ephemeral_public: Option<[u8; 32]>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct RenewRequest {
pub hostname: String,
pub cert_pem: String,
pub key_pem: String,
pub ca_pem: String,
pub fullchain_pem: String,
pub fingerprint: String,
pub expires: String,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct RenewResponse {
pub hostname: String,
pub renewed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub hook_result: Option<HookResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct HookResult {
pub success: bool,
pub command: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct RosterManifest {
pub roster_json: String,
pub signature: Vec<u8>,
pub ca_public_key: String,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct HealthRequest {
pub hostname: String,
pub pinned_ca_fingerprint: String,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct HealthResponse {
pub valid: bool,
pub ca_fingerprint: String,
}
mod optional_byte_array {
use serde::{self, Deserialize, Deserializer, Serializer};
pub fn serialize<S>(value: &Option<[u8; 32]>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
Some(bytes) => {
let hex = koi_common::encoding::hex_encode(bytes);
serializer.serialize_str(&hex)
}
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<[u8; 32]>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<String> = Option::deserialize(deserializer)?;
match opt {
Some(hex) => {
let bytes =
koi_common::encoding::hex_decode(&hex).map_err(serde::de::Error::custom)?;
if bytes.len() != 32 {
return Err(serde::de::Error::custom(format!(
"expected 32 bytes, got {}",
bytes.len()
)));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(Some(arr))
}
None => Ok(None),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn join_request_serde_round_trip() {
let req = JoinRequest {
hostname: "stone-05".to_string(),
auth: koi_crypto::auth::AuthResponse::Totp {
code: "123456".to_string(),
},
sans: vec!["10.0.0.5".to_string()],
};
let json = serde_json::to_string(&req).unwrap();
let parsed: JoinRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.hostname, "stone-05");
assert!(
matches!(parsed.auth, koi_crypto::auth::AuthResponse::Totp { ref code } if code == "123456")
);
assert_eq!(parsed.sans, vec!["10.0.0.5"]);
}
#[test]
fn join_request_without_sans_deserializes() {
let json = r#"{"hostname":"stone-05","auth":{"method":"totp","code":"123456"}}"#;
let parsed: JoinRequest = serde_json::from_str(json).unwrap();
assert_eq!(parsed.hostname, "stone-05");
assert!(parsed.sans.is_empty());
}
#[test]
fn join_response_serializes() {
let resp = JoinResponse {
hostname: "stone-05".to_string(),
ca_cert: "-----BEGIN CERTIFICATE-----\nca\n-----END CERTIFICATE-----\n".to_string(),
service_cert: "-----BEGIN CERTIFICATE-----\nsvc\n-----END CERTIFICATE-----\n"
.to_string(),
service_key: "-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----\n"
.to_string(),
ca_fingerprint: "abc123".to_string(),
cert_path: "/home/koi/.koi/certs/stone-05".to_string(),
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("stone-05"));
assert!(json.contains("ca_fingerprint"));
}
#[test]
fn set_hook_request_serde_round_trip() {
let req = SetHookRequest {
hostname: "stone-01".to_string(),
reload: "systemctl restart nginx".to_string(),
};
let json = serde_json::to_string(&req).unwrap();
let parsed: SetHookRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.hostname, "stone-01");
assert_eq!(parsed.reload, "systemctl restart nginx");
}
#[test]
fn set_hook_response_serializes() {
let resp = SetHookResponse {
hostname: "stone-01".to_string(),
reload: "systemctl restart nginx".to_string(),
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("stone-01"));
assert!(json.contains("systemctl restart nginx"));
}
#[test]
fn ca_announcement_has_correct_fields() {
use std::collections::HashMap;
let mut txt = HashMap::new();
txt.insert("role".to_string(), "primary".to_string());
txt.insert("profile".to_string(), "just_me".to_string());
let ann = CaAnnouncement {
name: "koi-ca-stone-01".to_string(),
port: 5641,
txt,
};
assert_eq!(ann.name, "koi-ca-stone-01");
assert_eq!(ann.port, 5641);
assert_eq!(ann.txt.get("role").unwrap(), "primary");
}
#[test]
fn promote_request_serde_round_trip() {
let req = PromoteRequest {
auth: koi_crypto::auth::AuthResponse::Totp {
code: "654321".to_string(),
},
ephemeral_public: None,
};
let json = serde_json::to_string(&req).unwrap();
let parsed: PromoteRequest = serde_json::from_str(&json).unwrap();
assert!(
matches!(parsed.auth, koi_crypto::auth::AuthResponse::Totp { ref code } if code == "654321")
);
assert!(parsed.ephemeral_public.is_none());
}
#[test]
fn promote_request_with_ephemeral_public_round_trip() {
let pub_key = [42u8; 32];
let req = PromoteRequest {
auth: koi_crypto::auth::AuthResponse::Totp {
code: "654321".to_string(),
},
ephemeral_public: Some(pub_key),
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("ephemeral_public"));
let parsed: PromoteRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.ephemeral_public, Some(pub_key));
}
#[test]
fn promote_response_serde_round_trip() {
let resp = PromoteResponse {
encrypted_ca_key: koi_crypto::keys::EncryptedKey {
ciphertext: vec![1, 2, 3],
salt: vec![4, 5, 6],
nonce: vec![7, 8, 9],
kdf_params: Default::default(),
},
auth_data: serde_json::json!({"method": "totp", "encrypted_secret": {"ciphertext": [10], "salt": [11], "nonce": [12]}}),
roster_json: r#"{"metadata":{}}"#.to_string(),
ca_cert_pem: "-----BEGIN CERTIFICATE-----\nca\n-----END CERTIFICATE-----\n".to_string(),
ephemeral_public: None,
};
let json = serde_json::to_string(&resp).unwrap();
let parsed: PromoteResponse = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.encrypted_ca_key.ciphertext, vec![1, 2, 3]);
assert_eq!(parsed.ca_cert_pem.len(), resp.ca_cert_pem.len());
assert!(parsed.ephemeral_public.is_none());
}
#[test]
fn promote_response_with_ephemeral_public_round_trip() {
let server_pub = [99u8; 32];
let resp = PromoteResponse {
encrypted_ca_key: koi_crypto::keys::EncryptedKey {
ciphertext: vec![1, 2, 3],
salt: vec![4, 5, 6],
nonce: vec![7, 8, 9],
kdf_params: Default::default(),
},
auth_data: serde_json::json!({"method": "totp"}),
roster_json: "{}".to_string(),
ca_cert_pem: "cert".to_string(),
ephemeral_public: Some(server_pub),
};
let json = serde_json::to_string(&resp).unwrap();
let parsed: PromoteResponse = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.ephemeral_public, Some(server_pub));
}
#[test]
fn renew_request_serde_round_trip() {
let req = RenewRequest {
hostname: "stone-05".to_string(),
cert_pem: "cert".to_string(),
key_pem: "key".to_string(),
ca_pem: "ca".to_string(),
fullchain_pem: "chain".to_string(),
fingerprint: "abc123".to_string(),
expires: "2026-03-15T00:00:00Z".to_string(),
};
let json = serde_json::to_string(&req).unwrap();
let parsed: RenewRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.hostname, "stone-05");
assert_eq!(parsed.fingerprint, "abc123");
}
#[test]
fn renew_response_serde_round_trip() {
let resp = RenewResponse {
hostname: "stone-05".to_string(),
renewed: true,
hook_result: Some(HookResult {
success: true,
command: "systemctl reload nginx".to_string(),
output: Some("OK".to_string()),
}),
};
let json = serde_json::to_string(&resp).unwrap();
let parsed: RenewResponse = serde_json::from_str(&json).unwrap();
assert!(parsed.renewed);
assert!(parsed.hook_result.unwrap().success);
}
#[test]
fn renew_response_omits_none_hook_result() {
let resp = RenewResponse {
hostname: "stone-05".to_string(),
renewed: true,
hook_result: None,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(!json.contains("hook_result"));
}
#[test]
fn hook_result_omits_none_output() {
let hr = HookResult {
success: false,
command: "bad-cmd".to_string(),
output: None,
};
let json = serde_json::to_string(&hr).unwrap();
assert!(!json.contains("output"));
}
#[test]
fn roster_manifest_serde_round_trip() {
let manifest = RosterManifest {
roster_json: r#"{"members":[]}"#.to_string(),
signature: vec![1, 2, 3, 4, 5],
ca_public_key: "-----BEGIN PUBLIC KEY-----\ntest\n-----END PUBLIC KEY-----\n"
.to_string(),
};
let json = serde_json::to_string(&manifest).unwrap();
let parsed: RosterManifest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.signature, vec![1, 2, 3, 4, 5]);
}
#[test]
fn health_request_serde_round_trip() {
let req = HealthRequest {
hostname: "stone-05".to_string(),
pinned_ca_fingerprint: "abcdef".to_string(),
};
let json = serde_json::to_string(&req).unwrap();
let parsed: HealthRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.hostname, "stone-05");
assert_eq!(parsed.pinned_ca_fingerprint, "abcdef");
}
#[test]
fn health_response_serde_round_trip() {
let resp = HealthResponse {
valid: true,
ca_fingerprint: "cafp123".to_string(),
};
let json = serde_json::to_string(&resp).unwrap();
let parsed: HealthResponse = serde_json::from_str(&json).unwrap();
assert!(parsed.valid);
assert_eq!(parsed.ca_fingerprint, "cafp123");
}
#[test]
fn certmesh_status_serializes() {
let status = CertmeshStatus {
ca_initialized: true,
ca_locked: false,
ca_fingerprint: Some("abc123".to_string()),
auth_method: None,
profile: TrustProfile::JustMe,
enrollment_state: EnrollmentState::Open,
enrollment_deadline: None,
allowed_domain: None,
allowed_subnet: None,
member_count: 1,
members: vec![MemberSummary {
hostname: "stone-01".to_string(),
role: "primary".to_string(),
status: "active".to_string(),
cert_fingerprint: "abc".to_string(),
cert_expires: "2026-03-13T00:00:00Z".to_string(),
}],
};
let json = serde_json::to_string(&status).unwrap();
assert!(json.contains("\"ca_initialized\":true"));
assert!(json.contains("\"member_count\":1"));
}
#[test]
fn certmesh_status_omits_none_policy_fields() {
let status = CertmeshStatus {
ca_initialized: true,
ca_locked: false,
ca_fingerprint: None,
auth_method: None,
profile: TrustProfile::JustMe,
enrollment_state: EnrollmentState::Open,
enrollment_deadline: None,
allowed_domain: None,
allowed_subnet: None,
member_count: 0,
members: vec![],
};
let json = serde_json::to_string(&status).unwrap();
assert!(!json.contains("enrollment_deadline"));
assert!(!json.contains("allowed_domain"));
assert!(!json.contains("allowed_subnet"));
}
#[test]
fn certmesh_status_includes_policy_when_set() {
let status = CertmeshStatus {
ca_initialized: true,
ca_locked: false,
ca_fingerprint: Some("fp-org".to_string()),
auth_method: None,
profile: TrustProfile::MyOrganization,
enrollment_state: EnrollmentState::Closed,
enrollment_deadline: Some("2026-03-01T00:00:00Z".to_string()),
allowed_domain: Some("school.local".to_string()),
allowed_subnet: Some("10.0.0.0/8".to_string()),
member_count: 0,
members: vec![],
};
let json = serde_json::to_string(&status).unwrap();
assert!(json.contains("enrollment_deadline"));
assert!(json.contains("school.local"));
assert!(json.contains("10.0.0.0/8"));
}
#[test]
fn policy_request_serde_round_trip() {
let req = PolicyRequest {
allowed_domain: Some("lab.local".to_string()),
allowed_subnet: Some("192.168.1.0/24".to_string()),
};
let json = serde_json::to_string(&req).unwrap();
let parsed: PolicyRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.allowed_domain.as_deref(), Some("lab.local"));
assert_eq!(parsed.allowed_subnet.as_deref(), Some("192.168.1.0/24"));
}
#[test]
fn policy_request_omits_none_fields() {
let req = PolicyRequest {
allowed_domain: None,
allowed_subnet: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(!json.contains("allowed_domain"));
assert!(!json.contains("allowed_subnet"));
}
#[test]
fn open_enrollment_request_serde_round_trip() {
let req = OpenEnrollmentRequest {
deadline: Some("2026-03-01T00:00:00Z".to_string()),
};
let json = serde_json::to_string(&req).unwrap();
let parsed: OpenEnrollmentRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.deadline.as_deref(), Some("2026-03-01T00:00:00Z"));
}
#[test]
fn create_ca_request_serde_round_trip() {
let req = CreateCaRequest {
passphrase: "hunter2".to_string(),
entropy_hex: "0a1b2c3d".to_string(),
profile: TrustProfile::JustMe,
operator: None,
enrollment_open: None,
requires_approval: None,
totp_secret_hex: None,
};
let json = serde_json::to_string(&req).unwrap();
let parsed: CreateCaRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.passphrase, "hunter2");
assert_eq!(parsed.entropy_hex, "0a1b2c3d");
assert_eq!(parsed.profile, TrustProfile::JustMe);
assert!(parsed.operator.is_none());
assert!(parsed.enrollment_open.is_none());
assert!(parsed.requires_approval.is_none());
}
#[test]
fn create_ca_request_with_operator() {
let req = CreateCaRequest {
passphrase: "pass".to_string(),
entropy_hex: "ff".to_string(),
profile: TrustProfile::MyOrganization,
operator: Some("ops@acme.com".to_string()),
enrollment_open: Some(false),
requires_approval: Some(true),
totp_secret_hex: None,
};
let json = serde_json::to_string(&req).unwrap();
let parsed: CreateCaRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.operator.as_deref(), Some("ops@acme.com"));
assert_eq!(parsed.profile, TrustProfile::MyOrganization);
assert_eq!(parsed.enrollment_open, Some(false));
assert_eq!(parsed.requires_approval, Some(true));
}
#[test]
fn create_ca_request_omits_none_operator() {
let req = CreateCaRequest {
passphrase: "p".to_string(),
entropy_hex: "aa".to_string(),
profile: TrustProfile::JustMe,
operator: None,
enrollment_open: None,
requires_approval: None,
totp_secret_hex: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(!json.contains("operator"));
assert!(!json.contains("enrollment_open"));
assert!(!json.contains("requires_approval"));
}
#[test]
fn create_ca_response_serde_round_trip() {
let resp = CreateCaResponse {
auth_setup: koi_crypto::auth::AuthSetup::Totp {
totp_uri: "otpauth://totp/Koi:admin?secret=ABC123".to_string(),
},
ca_fingerprint: "sha256:abcdef".to_string(),
};
let json = serde_json::to_string(&resp).unwrap();
let parsed: CreateCaResponse = serde_json::from_str(&json).unwrap();
assert!(json.contains("ABC123"));
assert_eq!(parsed.ca_fingerprint, "sha256:abcdef");
}
#[test]
fn unlock_request_serde_round_trip() {
let req = UnlockRequest {
passphrase: "my-secret".to_string(),
};
let json = serde_json::to_string(&req).unwrap();
let parsed: UnlockRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.passphrase, "my-secret");
}
#[test]
fn unlock_response_serde_round_trip() {
let resp = UnlockResponse { success: true };
let json = serde_json::to_string(&resp).unwrap();
let parsed: UnlockResponse = serde_json::from_str(&json).unwrap();
assert!(parsed.success);
}
#[test]
fn rotate_auth_request_serde_round_trip() {
let req = RotateAuthRequest {
passphrase: "rotate-pass".to_string(),
method: None,
};
let json = serde_json::to_string(&req).unwrap();
let parsed: RotateAuthRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.passphrase, "rotate-pass");
}
#[test]
fn rotate_auth_response_serde_round_trip() {
let resp = RotateAuthResponse {
auth_setup: koi_crypto::auth::AuthSetup::Totp {
totp_uri: "otpauth://totp/Koi:admin?secret=NEWBASE32".to_string(),
},
};
let json = serde_json::to_string(&resp).unwrap();
let _: RotateAuthResponse = serde_json::from_str(&json).unwrap();
assert!(json.contains("NEWBASE32"));
}
#[test]
fn audit_log_response_serde_round_trip() {
let resp = AuditLogResponse {
entries: "2026-02-11T00:00:00Z pond_initialized\n".to_string(),
};
let json = serde_json::to_string(&resp).unwrap();
let parsed: AuditLogResponse = serde_json::from_str(&json).unwrap();
assert!(parsed.entries.contains("pond_initialized"));
}
#[test]
fn destroy_response_serde_round_trip() {
let resp = DestroyResponse { destroyed: true };
let json = serde_json::to_string(&resp).unwrap();
let parsed: DestroyResponse = serde_json::from_str(&json).unwrap();
assert!(parsed.destroyed);
}
#[test]
fn certmesh_status_serde_round_trip() {
let status = CertmeshStatus {
ca_initialized: true,
ca_locked: false,
ca_fingerprint: Some("fp-round-trip".to_string()),
auth_method: None,
profile: TrustProfile::MyTeam,
enrollment_state: EnrollmentState::Open,
enrollment_deadline: Some("2026-03-01T00:00:00Z".to_string()),
allowed_domain: None,
allowed_subnet: None,
member_count: 2,
members: vec![
MemberSummary {
hostname: "stone-01".to_string(),
role: "primary".to_string(),
status: "active".to_string(),
cert_fingerprint: "fp1".to_string(),
cert_expires: "2026-06-01".to_string(),
},
MemberSummary {
hostname: "stone-02".to_string(),
role: "member".to_string(),
status: "active".to_string(),
cert_fingerprint: "fp2".to_string(),
cert_expires: "2026-06-01".to_string(),
},
],
};
let json = serde_json::to_string(&status).unwrap();
let parsed: CertmeshStatus = serde_json::from_str(&json).unwrap();
assert!(parsed.ca_initialized);
assert!(!parsed.ca_locked);
assert_eq!(parsed.profile, TrustProfile::MyTeam);
assert_eq!(parsed.member_count, 2);
assert_eq!(parsed.members.len(), 2);
assert_eq!(parsed.members[0].hostname, "stone-01");
assert_eq!(parsed.members[1].hostname, "stone-02");
}
#[test]
fn certmesh_status_uninitialized_round_trip() {
let status = CertmeshStatus {
ca_initialized: false,
ca_locked: false,
ca_fingerprint: None,
auth_method: None,
profile: TrustProfile::JustMe,
enrollment_state: EnrollmentState::Closed,
enrollment_deadline: None,
allowed_domain: None,
allowed_subnet: None,
member_count: 0,
members: vec![],
};
let json = serde_json::to_string(&status).unwrap();
let parsed: CertmeshStatus = serde_json::from_str(&json).unwrap();
assert!(!parsed.ca_initialized);
assert_eq!(parsed.member_count, 0);
assert!(parsed.members.is_empty());
}
#[test]
fn policy_summary_serializes() {
let summary = PolicySummary {
enrollment_state: EnrollmentState::Open,
enrollment_deadline: Some("2026-03-01T00:00:00Z".to_string()),
allowed_domain: Some("school.local".to_string()),
allowed_subnet: None,
profile: TrustProfile::MyOrganization,
requires_approval: true,
};
let json = serde_json::to_string(&summary).unwrap();
assert!(json.contains("requires_approval"));
assert!(json.contains("school.local"));
assert!(!json.contains("allowed_subnet"));
}
}