use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct CredentialsListFilter {
#[serde(skip_serializing_if = "Option::is_none")]
pub channel_filter: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CredentialSummary {
pub channel: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub instance: Option<String>,
pub agent_ids: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct CredentialsListResponse {
pub credentials: Vec<CredentialSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CredentialRegisterInput {
pub channel: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub instance: Option<String>,
pub agent_ids: Vec<String>,
pub payload: serde_json::Value,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CredentialValidationOutcome {
pub probed: bool,
pub healthy: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason_code: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CredentialRegisterResponse {
pub summary: CredentialSummary,
#[serde(skip_serializing_if = "Option::is_none")]
pub validation: Option<CredentialValidationOutcome>,
}
pub mod reason_code {
pub const OK: &str = "ok";
pub const UNSUPPORTED_CHANNEL: &str = "unsupported_channel";
pub const INVALID_PAYLOAD: &str = "invalid_payload";
pub const INVALID_METADATA: &str = "invalid_metadata";
pub const CONNECTIVITY_FAILED: &str = "connectivity_failed";
pub const AUTH_FAILED: &str = "auth_failed";
pub const TLS_FAILED: &str = "tls_failed";
pub const NOT_PROBED: &str = "not_probed";
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CredentialsRevokeParams {
pub channel: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub instance: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct CredentialsRevokeResponse {
pub removed: bool,
pub unbound_agents: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn credential_summary_round_trip() {
let s = CredentialSummary {
channel: "whatsapp".into(),
instance: Some("personal".into()),
agent_ids: vec!["ana".into(), "carlos".into()],
};
let v = serde_json::to_value(&s).unwrap();
let back: CredentialSummary = serde_json::from_value(v).unwrap();
assert_eq!(s, back);
}
#[test]
fn register_input_default_empty_agent_ids_round_trip() {
let i = CredentialRegisterInput {
channel: "whatsapp".into(),
instance: None,
agent_ids: vec![],
payload: serde_json::json!({ "token": "wa.X" }),
metadata: HashMap::new(),
};
let v = serde_json::to_value(&i).unwrap();
let back: CredentialRegisterInput = serde_json::from_value(v).unwrap();
assert_eq!(i, back);
assert!(back.instance.is_none());
}
#[test]
fn revoke_response_default_empty() {
let r = CredentialsRevokeResponse::default();
assert!(!r.removed);
assert!(r.unbound_agents.is_empty());
}
#[test]
fn register_input_metadata_default_empty_skips_in_serialization() {
let i = CredentialRegisterInput {
channel: "telegram".into(),
instance: Some("kate".into()),
agent_ids: vec!["kate".into()],
payload: serde_json::json!({ "token": "tg.X" }),
metadata: HashMap::new(),
};
let v = serde_json::to_value(&i).unwrap();
assert!(v.get("metadata").is_none());
let back: CredentialRegisterInput = serde_json::from_value(v).unwrap();
assert_eq!(i, back);
assert!(back.metadata.is_empty());
}
#[test]
fn register_input_with_metadata_round_trip() {
let mut metadata = HashMap::new();
metadata.insert(
"imap".into(),
serde_json::json!({ "host": "imap.gmail.com", "port": 993, "tls": "implicit_tls" }),
);
metadata.insert("address".into(), serde_json::json!("ops@example.com"));
let i = CredentialRegisterInput {
channel: "email".into(),
instance: Some("ops".into()),
agent_ids: vec!["ana".into()],
payload: serde_json::json!({ "password": "p" }),
metadata,
};
let v = serde_json::to_value(&i).unwrap();
assert!(v.get("metadata").is_some());
let back: CredentialRegisterInput = serde_json::from_value(v).unwrap();
assert_eq!(i, back);
}
#[test]
fn register_input_pre_82_10_n_payload_parses_with_default_metadata() {
let v = serde_json::json!({
"channel": "whatsapp",
"instance": "personal",
"agent_ids": ["ana"],
"payload": { "token": "wa.X" }
});
let parsed: CredentialRegisterInput = serde_json::from_value(v).unwrap();
assert!(parsed.metadata.is_empty());
assert_eq!(parsed.channel, "whatsapp");
}
#[test]
fn validation_outcome_round_trip_full() {
let o = CredentialValidationOutcome {
probed: true,
healthy: false,
detail: Some("IMAP LOGIN rejected".into()),
reason_code: Some(reason_code::AUTH_FAILED.into()),
};
let v = serde_json::to_value(&o).unwrap();
let back: CredentialValidationOutcome = serde_json::from_value(v).unwrap();
assert_eq!(o, back);
}
#[test]
fn validation_outcome_skips_optional_fields_when_absent() {
let o = CredentialValidationOutcome {
probed: false,
healthy: false,
detail: None,
reason_code: None,
};
let v = serde_json::to_value(&o).unwrap();
assert!(v.get("detail").is_none());
assert!(v.get("reason_code").is_none());
}
#[test]
fn register_response_round_trip_with_validation() {
let r = CredentialRegisterResponse {
summary: CredentialSummary {
channel: "telegram".into(),
instance: Some("kate".into()),
agent_ids: vec!["kate".into()],
},
validation: Some(CredentialValidationOutcome {
probed: true,
healthy: true,
detail: None,
reason_code: Some(reason_code::OK.into()),
}),
};
let v = serde_json::to_value(&r).unwrap();
let back: CredentialRegisterResponse = serde_json::from_value(v).unwrap();
assert_eq!(r, back);
}
#[test]
fn register_response_round_trip_no_validation_skips_field() {
let r = CredentialRegisterResponse {
summary: CredentialSummary {
channel: "whatsapp".into(),
instance: None,
agent_ids: vec![],
},
validation: None,
};
let v = serde_json::to_value(&r).unwrap();
assert!(v.get("validation").is_none());
let back: CredentialRegisterResponse = serde_json::from_value(v).unwrap();
assert_eq!(r, back);
}
#[test]
fn reason_code_constants_are_stable() {
assert_eq!(reason_code::OK, "ok");
assert_eq!(reason_code::UNSUPPORTED_CHANNEL, "unsupported_channel");
assert_eq!(reason_code::INVALID_PAYLOAD, "invalid_payload");
assert_eq!(reason_code::INVALID_METADATA, "invalid_metadata");
assert_eq!(reason_code::CONNECTIVITY_FAILED, "connectivity_failed");
assert_eq!(reason_code::AUTH_FAILED, "auth_failed");
assert_eq!(reason_code::TLS_FAILED, "tls_failed");
assert_eq!(reason_code::NOT_PROBED, "not_probed");
}
}