Skip to main content

htb_cli/models/
mod.rs

1use serde::{Deserialize, Deserializer, Serialize};
2
3pub mod challenge;
4pub mod ctf;
5pub mod machine;
6pub mod season;
7pub mod sherlock;
8pub mod user;
9pub mod vpn;
10
11/// Deserialize a value that may be a string or an integer into `Option<String>`.
12pub(crate) fn deserialize_string_or_int<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
13where
14    D: Deserializer<'de>,
15{
16    let v = Option::<serde_json::Value>::deserialize(deserializer)?;
17    Ok(v.map(|v| match v {
18        serde_json::Value::String(s) => s,
19        serde_json::Value::Number(n) => n.to_string(),
20        other => other.to_string(),
21    }))
22}
23
24/// Deserialize a bool that may arrive as `null` from the API.
25pub(crate) fn deserialize_bool_or_null<'de, D>(deserializer: D) -> Result<bool, D::Error>
26where
27    D: Deserializer<'de>,
28{
29    Ok(Option::<bool>::deserialize(deserializer)?.unwrap_or(false))
30}
31
32#[derive(Debug, Deserialize)]
33pub struct Paginated<T> {
34    pub data: Vec<T>,
35    pub links: PaginationLinks,
36    pub meta: PaginationMeta,
37}
38
39#[derive(Debug, Deserialize)]
40pub struct PaginationLinks {
41    pub first: Option<String>,
42    pub last: Option<String>,
43    pub prev: Option<String>,
44    pub next: Option<String>,
45}
46
47#[derive(Debug, Deserialize)]
48pub struct PaginationMeta {
49    pub current_page: u32,
50    pub from: Option<u32>,
51    pub last_page: u32,
52    pub per_page: u32,
53    pub to: Option<u32>,
54    pub total: u32,
55}
56
57#[derive(Debug, Deserialize, Serialize)]
58pub struct ActionResponse {
59    pub message: String,
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn paginated_machines() {
68        let json = include_str!("../../tests/fixtures/v5-machines-list.json");
69        let result: Paginated<serde_json::Value> = serde_json::from_str(json).unwrap();
70        assert_eq!(result.data.len(), 15);
71        assert_eq!(result.meta.total, 545);
72        assert_eq!(result.meta.last_page, 37);
73        assert_eq!(result.meta.current_page, 1);
74    }
75
76    #[test]
77    fn paginated_challenges() {
78        let json = include_str!("../../tests/fixtures/challenges-list.json");
79        let result: Paginated<serde_json::Value> = serde_json::from_str(json).unwrap();
80        assert_eq!(result.data.len(), 15);
81        assert_eq!(result.meta.total, 839);
82    }
83
84    #[test]
85    fn paginated_sherlocks() {
86        let json = include_str!("../../tests/fixtures/sherlocks-list.json");
87        let result: Paginated<serde_json::Value> = serde_json::from_str(json).unwrap();
88        assert!(!result.data.is_empty());
89    }
90
91    #[test]
92    fn action_response_container_start() {
93        let json = include_str!("../../tests/fixtures/container-start.json");
94        let result: ActionResponse = serde_json::from_str(json).unwrap();
95        assert_eq!(result.message, "Instance Created!");
96    }
97
98    #[test]
99    fn action_response_flag_incorrect() {
100        let json = include_str!("../../tests/fixtures/challenge-own-incorrect.json");
101        let result: ActionResponse = serde_json::from_str(json).unwrap();
102        assert_eq!(result.message, "Incorrect flag");
103    }
104
105    #[test]
106    fn bool_or_null_handles_null() {
107        #[derive(Deserialize)]
108        struct T {
109            #[serde(default, deserialize_with = "deserialize_bool_or_null")]
110            flag: bool,
111        }
112        let null: T = serde_json::from_str(r#"{"flag": null}"#).unwrap();
113        assert!(!null.flag);
114        let t: T = serde_json::from_str(r#"{"flag": true}"#).unwrap();
115        assert!(t.flag);
116        let missing: T = serde_json::from_str(r#"{}"#).unwrap();
117        assert!(!missing.flag);
118
119        // Rejects types the API shouldn't send but might
120        assert!(serde_json::from_str::<T>(r#"{"flag": 0}"#).is_err());
121        assert!(serde_json::from_str::<T>(r#"{"flag": 1}"#).is_err());
122        assert!(serde_json::from_str::<T>(r#"{"flag": "true"}"#).is_err());
123    }
124
125    #[test]
126    fn string_or_int_handles_both() {
127        #[derive(Deserialize)]
128        struct T {
129            #[serde(default, deserialize_with = "deserialize_string_or_int")]
130            val: Option<String>,
131        }
132        let s: T = serde_json::from_str(r#"{"val": "50"}"#).unwrap();
133        assert_eq!(s.val.as_deref(), Some("50"));
134        let i: T = serde_json::from_str(r#"{"val": 0}"#).unwrap();
135        assert_eq!(i.val.as_deref(), Some("0"));
136        let n: T = serde_json::from_str(r#"{"val": null}"#).unwrap();
137        assert!(n.val.is_none());
138    }
139
140    #[test]
141    fn string_or_int_handles_bool() {
142        #[derive(Deserialize)]
143        struct T {
144            #[serde(default, deserialize_with = "deserialize_string_or_int")]
145            val: Option<String>,
146        }
147        let t: T = serde_json::from_str(r#"{"val": true}"#).unwrap();
148        assert_eq!(t.val.as_deref(), Some("true"));
149        let f: T = serde_json::from_str(r#"{"val": false}"#).unwrap();
150        assert_eq!(f.val.as_deref(), Some("false"));
151    }
152
153    #[test]
154    fn string_or_int_handles_float() {
155        #[derive(Deserialize)]
156        struct T {
157            #[serde(default, deserialize_with = "deserialize_string_or_int")]
158            val: Option<String>,
159        }
160        let f: T = serde_json::from_str(r#"{"val": 4.5}"#).unwrap();
161        assert_eq!(f.val.as_deref(), Some("4.5"));
162    }
163
164    #[test]
165    fn string_or_int_handles_empty_string() {
166        #[derive(Deserialize)]
167        struct T {
168            #[serde(default, deserialize_with = "deserialize_string_or_int")]
169            val: Option<String>,
170        }
171        let e: T = serde_json::from_str(r#"{"val": ""}"#).unwrap();
172        assert_eq!(e.val.as_deref(), Some(""));
173    }
174}