use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, Default)]
pub struct SupportUploadDiagnosticsParams {
#[serde(default)]
pub summary: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contact: Option<String>,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct SupportUploadDiagnosticsResult {
pub object_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ticket_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ticket_url: Option<String>,
pub size_bytes: u64,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, PartialEq, Eq)]
pub struct UnlockGrant {
pub scope: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
pub expires_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct SupportUnlockParams {
pub code: String,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct SupportUnlockResult {
pub grants: Vec<UnlockGrant>,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, Default)]
pub struct SupportLockParams {}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct SupportLockResult {
pub released: usize,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, Default)]
pub struct SupportStatusParams {}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct SupportStatusResult {
pub grants: Vec<UnlockGrant>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn params_default_is_empty_summary() {
let p = SupportUploadDiagnosticsParams::default();
assert_eq!(p.summary, "");
assert!(p.detail.is_none());
assert!(p.contact.is_none());
}
#[test]
fn params_minimal_wire_decodes() {
let p: SupportUploadDiagnosticsParams = serde_json::from_str("{}").unwrap();
assert_eq!(p.summary, "");
}
#[test]
fn result_with_ticket_round_trips() {
let r = SupportUploadDiagnosticsResult {
object_key: "support/2026-05-24/abc123.zip".into(),
ticket_id: Some("HELP-42".into()),
ticket_url: Some("https://helpdesk.example.com/tickets/HELP-42".into()),
size_bytes: 4_200_000,
};
let json = serde_json::to_string(&r).unwrap();
let back: SupportUploadDiagnosticsResult = serde_json::from_str(&json).unwrap();
assert_eq!(back.object_key, r.object_key);
assert_eq!(back.ticket_id, r.ticket_id);
assert_eq!(back.ticket_url, r.ticket_url);
assert_eq!(back.size_bytes, r.size_bytes);
}
#[test]
fn result_without_ticket_omits_field_on_wire() {
let r = SupportUploadDiagnosticsResult {
object_key: "x".into(),
ticket_id: None,
ticket_url: None,
size_bytes: 0,
};
let v = serde_json::to_value(&r).unwrap();
assert!(v.get("ticket_id").is_none(), "wire: {v:?}");
assert!(v.get("ticket_url").is_none(), "wire: {v:?}");
}
#[test]
fn unlock_grant_omits_absent_label() {
let g = UnlockGrant {
scope: "support".into(),
label: None,
expires_at: chrono::Utc::now(),
};
let v = serde_json::to_value(&g).unwrap();
assert_eq!(v["scope"], "support");
assert!(v.get("label").is_none(), "wire: {v:?}");
}
#[test]
fn unlock_result_round_trips() {
let wire = r#"{"grants":[
{"scope":"support","label":"ヘルプデスク","expires_at":"2026-07-28T09:00:00Z"}
]}"#;
let r: SupportUnlockResult = serde_json::from_str(wire).unwrap();
assert_eq!(r.grants.len(), 1);
assert_eq!(r.grants[0].scope, "support");
assert_eq!(r.grants[0].label.as_deref(), Some("ヘルプデスク"));
}
#[test]
fn status_result_empty_means_locked() {
let r = SupportStatusResult { grants: vec![] };
let v = serde_json::to_value(&r).unwrap();
assert_eq!(v["grants"], serde_json::json!([]));
}
}