Skip to content

Commit 0d7eb18

Browse files
iglocskaclaude
andcommitted
fix: [models] Handle integer timestamps in MispUser, relax statistics assertions
- Add string_or_int_as_string_opt serde helper for fields that MISP returns as either strings or integers (timestamps) - Apply to last_login, date_created, date_modified in MispUser - Relax statistics test to accept both object and array responses (fresh MISP instances return empty arrays) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 26ec244 commit 0d7eb18

3 files changed

Lines changed: 64 additions & 7 deletions

File tree

src/models/serde_helpers.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,40 @@ pub mod string_or_i64_opt {
4444
}
4545
}
4646

47+
/// Deserialize an `Option<String>` that may arrive as a string or integer.
48+
/// Handles: `null`, `"text"`, `123` (→ `"123"`), `""` (→ `None`).
49+
/// Useful for timestamp fields MISP returns as either strings or integers.
50+
pub mod string_or_int_as_string_opt {
51+
use super::*;
52+
53+
pub fn serialize<S>(value: &Option<String>, serializer: S) -> Result<S::Ok, S::Error>
54+
where
55+
S: Serializer,
56+
{
57+
match value {
58+
Some(v) => serializer.serialize_str(v),
59+
None => serializer.serialize_none(),
60+
}
61+
}
62+
63+
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
64+
where
65+
D: Deserializer<'de>,
66+
{
67+
let opt: Option<serde_json::Value> = Option::deserialize(deserializer)?;
68+
match opt {
69+
None | Some(serde_json::Value::Null) => Ok(None),
70+
Some(serde_json::Value::String(s)) if s.is_empty() => Ok(None),
71+
Some(serde_json::Value::String(s)) => Ok(Some(s)),
72+
Some(serde_json::Value::Number(n)) => Ok(Some(n.to_string())),
73+
Some(serde_json::Value::Bool(b)) => Ok(Some(b.to_string())),
74+
Some(other) => Err(serde::de::Error::custom(format!(
75+
"expected string or number, got {other}"
76+
))),
77+
}
78+
}
79+
}
80+
4781
/// Deserialize a value that may be a string or number into an `i64`.
4882
/// Handles: `"123"`, `123`.
4983
pub mod string_or_i64 {

src/models/user.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use serde::{Deserialize, Deserializer, Serialize};
22

33
use super::organisation::MispOrganisation;
4-
use super::serde_helpers::{flexible_bool, flexible_bool_opt, string_or_i64_opt};
4+
use super::serde_helpers::{
5+
flexible_bool, flexible_bool_opt, string_or_i64_opt, string_or_int_as_string_opt,
6+
};
57

68
/// Deserialize password, treating masked values (all '*') as None like PyMISP.
79
fn deserialize_password<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
@@ -135,15 +137,27 @@ pub struct MispUser {
135137
pub notification_monthly: Option<bool>,
136138

137139
/// Timestamp of last login.
138-
#[serde(default, skip_serializing_if = "Option::is_none")]
140+
#[serde(
141+
default,
142+
with = "string_or_int_as_string_opt",
143+
skip_serializing_if = "Option::is_none"
144+
)]
139145
pub last_login: Option<String>,
140146

141147
/// Date the user was created.
142-
#[serde(default, skip_serializing_if = "Option::is_none")]
148+
#[serde(
149+
default,
150+
with = "string_or_int_as_string_opt",
151+
skip_serializing_if = "Option::is_none"
152+
)]
143153
pub date_created: Option<String>,
144154

145155
/// Date the user was last modified.
146-
#[serde(default, skip_serializing_if = "Option::is_none")]
156+
#[serde(
157+
default,
158+
with = "string_or_int_as_string_opt",
159+
skip_serializing_if = "Option::is_none"
160+
)]
147161
pub date_modified: Option<String>,
148162

149163
/// Nested Organisation object (read-only, returned by server).

tests/integration_tests.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1868,13 +1868,22 @@ async fn test_statistics() {
18681868
.attributes_statistics(None, None)
18691869
.await
18701870
.expect("attr stats");
1871-
assert!(attr_stats.is_object(), "Should return JSON object");
1871+
assert!(
1872+
attr_stats.is_object() || attr_stats.is_array(),
1873+
"Should return JSON object or array"
1874+
);
18721875

18731876
let tag_stats = client.tags_statistics(None, None).await.expect("tag stats");
1874-
assert!(tag_stats.is_object(), "Should return JSON object");
1877+
assert!(
1878+
tag_stats.is_object() || tag_stats.is_array(),
1879+
"Should return JSON object or array"
1880+
);
18751881

18761882
let user_stats = client.users_statistics(None).await.expect("user stats");
1877-
assert!(user_stats.is_object(), "Should return JSON object");
1883+
assert!(
1884+
user_stats.is_object() || user_stats.is_array(),
1885+
"Should return JSON object or array"
1886+
);
18781887
}
18791888

18801889
// ============================================================================

0 commit comments

Comments
 (0)