use std::collections::BTreeMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct ApiResponse<T: Default> {
pub data: T,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub includes: Option<Includes>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub meta: Option<ResponseMeta>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub errors: Option<Vec<ApiError>>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct Includes {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub users: Option<Vec<User>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tweets: Option<Vec<Tweet>>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct ResponseMeta {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result_count: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub previous_token: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct ApiError {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r#type: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct Tweet {
pub id: String,
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub author_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub in_reply_to_user_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub public_metrics: Option<TweetPublicMetrics>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub referenced_tweets: Option<Vec<ReferencedTweet>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub entities: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attachments: Option<Value>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct TweetPublicMetrics {
#[serde(default)]
pub retweet_count: u64,
#[serde(default)]
pub reply_count: u64,
#[serde(default)]
pub like_count: u64,
#[serde(default)]
pub quote_count: u64,
#[serde(default)]
pub bookmark_count: u64,
#[serde(default)]
pub impression_count: u64,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct ReferencedTweet {
pub id: String,
pub r#type: String,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct User {
pub id: String,
pub name: String,
pub username: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verified: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_image_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub public_metrics: Option<UserPublicMetrics>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct UserPublicMetrics {
#[serde(default)]
pub followers_count: u64,
#[serde(default)]
pub following_count: u64,
#[serde(default)]
pub tweet_count: u64,
#[serde(default)]
pub listed_count: u64,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct DmEvent {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub event_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dm_conversation_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sender_id: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct LikedResult {
pub liked: bool,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct FollowingResult {
pub following: bool,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct DeletedResult {
pub deleted: bool,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct RetweetedResult {
pub retweeted: bool,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct BookmarkedResult {
pub bookmarked: bool,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct BlockingResult {
pub blocking: bool,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct MutingResult {
pub muting: bool,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct MediaUploadResponse {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub media_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_after_secs: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub processing_info: Option<MediaProcessingInfo>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct MediaProcessingInfo {
pub state: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub check_after_secs: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_percent: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<Value>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct UsageData {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_cap: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_usage: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cap_reset_day: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub daily_project_usage: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub daily_client_app_usage: Option<Value>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
pub fn deserialize_response<T: Default + serde::de::DeserializeOwned>(
value: Value,
) -> crate::error::Result<ApiResponse<T>> {
if value.as_object().is_some_and(|m| m.is_empty()) {
return Err(crate::error::XurlError::Json(
"empty response body — expected JSON with a \"data\" field".to_string(),
));
}
if let Some(obj) = value.as_object()
&& !obj.contains_key("data")
&& obj.contains_key("errors")
{
return Err(crate::error::XurlError::validation(value.to_string()));
}
Ok(serde_json::from_value(value)?)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn deserialize_single_tweet() {
let json = json!({
"data": {
"id": "123",
"text": "Hello world",
"created_at": "2026-01-01T00:00:00.000Z",
"public_metrics": {
"retweet_count": 5,
"reply_count": 2,
"like_count": 10,
"quote_count": 1,
"bookmark_count": 0,
"impression_count": 100
}
}
});
let resp: ApiResponse<Tweet> =
serde_json::from_value(json).expect("Tweet response must deserialize");
assert_eq!(resp.data.id, "123");
assert_eq!(resp.data.text, "Hello world");
assert_eq!(
resp.data.created_at.as_deref(),
Some("2026-01-01T00:00:00.000Z")
);
let metrics = resp
.data
.public_metrics
.expect("public_metrics must be present");
assert_eq!(metrics.like_count, 10);
assert_eq!(metrics.impression_count, 100);
}
#[test]
fn deserialize_tweet_list() {
let json = json!({
"data": [
{"id": "1", "text": "first"},
{"id": "2", "text": "second"}
],
"meta": {"result_count": 2}
});
let resp: ApiResponse<Vec<Tweet>> =
serde_json::from_value(json).expect("Tweet list response must deserialize");
assert_eq!(resp.data.len(), 2);
assert_eq!(resp.data[0].id, "1");
assert_eq!(resp.data[1].text, "second");
assert_eq!(
resp.meta.expect("meta must be present").result_count,
Some(2)
);
}
#[test]
fn deserialize_action_liked() {
let json = json!({"data": {"liked": true}});
let resp: ApiResponse<LikedResult> =
serde_json::from_value(json).expect("LikedResult response must deserialize");
assert!(resp.data.liked);
}
#[test]
fn deserialize_with_includes_and_meta() {
let json = json!({
"data": [{"id": "1", "text": "tweet"}],
"includes": {
"users": [{"id": "42", "name": "Bot", "username": "bot"}]
},
"meta": {"result_count": 1, "next_token": "abc123"}
});
let resp: ApiResponse<Vec<Tweet>> =
serde_json::from_value(json).expect("Tweet list with includes/meta must deserialize");
let includes = resp.includes.expect("includes must be present");
let users = includes.users.expect("includes.users must be present");
assert_eq!(users[0].id, "42");
assert_eq!(
resp.meta
.expect("meta must be present")
.next_token
.as_deref(),
Some("abc123")
);
}
#[test]
fn deserialize_user() {
let json = json!({
"data": {
"id": "42",
"name": "Test User",
"username": "testuser",
"verified": true,
"public_metrics": {
"followers_count": 100,
"following_count": 50,
"tweet_count": 1000,
"listed_count": 5
}
}
});
let resp: ApiResponse<User> =
serde_json::from_value(json).expect("User response must deserialize");
assert_eq!(resp.data.id, "42");
assert_eq!(resp.data.username, "testuser");
assert_eq!(resp.data.verified, Some(true));
let metrics = resp
.data
.public_metrics
.expect("user.public_metrics must be present");
assert_eq!(metrics.followers_count, 100);
}
#[test]
fn deserialize_dm_event() {
let json = json!({
"data": {
"id": "dm1",
"text": "hello",
"event_type": "MessageCreate",
"dm_conversation_id": "conv1",
"sender_id": "42"
}
});
let resp: ApiResponse<DmEvent> =
serde_json::from_value(json).expect("DmEvent response must deserialize");
assert_eq!(resp.data.id, "dm1");
assert_eq!(resp.data.text.as_deref(), Some("hello"));
assert_eq!(resp.data.sender_id.as_deref(), Some("42"));
}
#[test]
fn deserialize_usage_data() {
let json = json!({
"data": {
"project_cap": "2000000",
"project_id": "123",
"project_usage": "399",
"cap_reset_day": 19
}
});
let resp: ApiResponse<UsageData> =
serde_json::from_value(json).expect("UsageData response must deserialize");
assert_eq!(resp.data.project_cap.as_deref(), Some("2000000"));
assert_eq!(resp.data.cap_reset_day, Some(19));
}
#[test]
fn deserialize_media_upload_response() {
let json = json!({
"data": {
"id": "media_123",
"media_key": "key_456",
"expires_after_secs": 3600
}
});
let resp: ApiResponse<MediaUploadResponse> =
serde_json::from_value(json).expect("MediaUploadResponse must deserialize");
assert_eq!(resp.data.id, "media_123");
assert_eq!(resp.data.media_key.as_deref(), Some("key_456"));
assert_eq!(resp.data.expires_after_secs, Some(3600));
}
#[test]
fn deserialize_media_with_processing_info() {
let json = json!({
"data": {
"id": "media_123",
"processing_info": {
"state": "in_progress",
"check_after_secs": 5,
"progress_percent": 45
}
}
});
let resp: ApiResponse<MediaUploadResponse> = serde_json::from_value(json)
.expect("MediaUploadResponse with processing_info must deserialize");
let info = resp
.data
.processing_info
.expect("processing_info must be present");
assert_eq!(info.state, "in_progress");
assert_eq!(info.check_after_secs, Some(5));
assert_eq!(info.progress_percent, Some(45));
}
#[test]
fn unknown_fields_captured_in_extra() {
let json = json!({
"data": {
"id": "123",
"text": "hello",
"brand_new_field": "surprise"
},
"top_level_extra": 42
});
let resp: ApiResponse<Tweet> = serde_json::from_value(json)
.expect("Tweet response with unknown fields must deserialize");
assert_eq!(resp.data.extra["brand_new_field"], "surprise");
assert_eq!(resp.extra["top_level_extra"], 42);
}
#[test]
fn unknown_fields_round_trip() {
let json = json!({
"data": {
"id": "123",
"text": "hello",
"new_field": 42
},
"top_extra": "value"
});
let resp: ApiResponse<Tweet> =
serde_json::from_value(json).expect("Tweet round-trip fixture must deserialize");
let serialized = serde_json::to_value(&resp).expect("Tweet response must serialize");
assert_eq!(serialized["data"]["new_field"], 42);
assert_eq!(serialized["top_extra"], "value");
}
#[test]
fn nested_unknown_fields_both_captured() {
let json = json!({
"data": {
"id": "123",
"text": "hello",
"tweet_extra": "a",
"public_metrics": {
"retweet_count": 0,
"reply_count": 0,
"like_count": 0,
"quote_count": 0,
"bookmark_count": 0,
"impression_count": 0,
"metrics_extra": "b"
}
}
});
let resp: ApiResponse<Tweet> = serde_json::from_value(json)
.expect("Tweet response with nested unknown fields must deserialize");
assert_eq!(resp.data.extra["tweet_extra"], "a");
let metrics = resp
.data
.public_metrics
.expect("public_metrics must be present");
assert_eq!(metrics.extra["metrics_extra"], "b");
}
#[test]
fn extra_is_empty_when_no_unknown_fields() {
let json = json!({"data": {"id": "1", "text": "hi"}});
let resp: ApiResponse<Tweet> =
serde_json::from_value(json).expect("minimal Tweet response must deserialize");
assert!(resp.extra.is_empty());
assert!(resp.data.extra.is_empty());
let out = serde_json::to_value(&resp).expect("Tweet response must serialize");
let data = out["data"]
.as_object()
.expect("serialized data must be a JSON object");
assert!(!data.contains_key("extra"));
}
#[test]
fn missing_optional_fields_are_none() {
let json = json!({"data": {"id": "1", "text": "minimal"}});
let resp: ApiResponse<Tweet> =
serde_json::from_value(json).expect("minimal Tweet must deserialize");
assert!(resp.data.created_at.is_none());
assert!(resp.data.public_metrics.is_none());
assert!(resp.data.author_id.is_none());
assert!(resp.includes.is_none());
assert!(resp.meta.is_none());
}
#[test]
fn default_produces_valid_structs() {
let tweet = Tweet {
id: "test".into(),
text: "hello".into(),
..Default::default()
};
assert_eq!(tweet.id, "test");
assert!(tweet.created_at.is_none());
let user = User {
id: "42".into(),
name: "Bot".into(),
username: "bot".into(),
..Default::default()
};
assert_eq!(user.username, "bot");
let _resp: ApiResponse<Tweet> = ApiResponse {
data: tweet,
..Default::default()
};
}
#[test]
fn all_action_types_default_and_deserialize() {
let _: LikedResult = Default::default();
let _: FollowingResult = Default::default();
let _: DeletedResult = Default::default();
let _: RetweetedResult = Default::default();
let _: BookmarkedResult = Default::default();
let _: BlockingResult = Default::default();
let _: MutingResult = Default::default();
for (field, ty) in [
("liked", "LikedResult"),
("following", "FollowingResult"),
("deleted", "DeletedResult"),
("retweeted", "RetweetedResult"),
("bookmarked", "BookmarkedResult"),
("blocking", "BlockingResult"),
("muting", "MutingResult"),
] {
let json = json!({"data": {field: true}});
match ty {
"LikedResult" => {
let r: ApiResponse<LikedResult> = serde_json::from_value(json)
.expect("LikedResult action response must deserialize");
assert!(r.data.liked);
}
"FollowingResult" => {
let r: ApiResponse<FollowingResult> = serde_json::from_value(json)
.expect("FollowingResult action response must deserialize");
assert!(r.data.following);
}
"DeletedResult" => {
let r: ApiResponse<DeletedResult> = serde_json::from_value(json)
.expect("DeletedResult action response must deserialize");
assert!(r.data.deleted);
}
"RetweetedResult" => {
let r: ApiResponse<RetweetedResult> = serde_json::from_value(json)
.expect("RetweetedResult action response must deserialize");
assert!(r.data.retweeted);
}
"BookmarkedResult" => {
let r: ApiResponse<BookmarkedResult> = serde_json::from_value(json)
.expect("BookmarkedResult action response must deserialize");
assert!(r.data.bookmarked);
}
"BlockingResult" => {
let r: ApiResponse<BlockingResult> = serde_json::from_value(json)
.expect("BlockingResult action response must deserialize");
assert!(r.data.blocking);
}
"MutingResult" => {
let r: ApiResponse<MutingResult> = serde_json::from_value(json)
.expect("MutingResult action response must deserialize");
assert!(r.data.muting);
}
_ => unreachable!(),
}
}
}
#[test]
fn invalid_json_missing_required_field() {
let json = json!({"data": {"text": "no id"}});
let result = serde_json::from_value::<ApiResponse<Tweet>>(json);
assert!(result.is_err());
}
#[test]
fn round_trip_serialize_deserialize() {
let tweet = Tweet {
id: "456".into(),
text: "round trip".into(),
created_at: Some("2026-01-01T00:00:00Z".into()),
..Default::default()
};
let resp = ApiResponse {
data: tweet,
..Default::default()
};
let value = serde_json::to_value(&resp).expect("Tweet must serialize");
let back: ApiResponse<Tweet> =
serde_json::from_value(value).expect("round-tripped Tweet must deserialize");
assert_eq!(back.data.id, "456");
assert_eq!(back.data.text, "round trip");
}
#[test]
fn deserialize_helper_rejects_empty_object() {
let value = json!({});
let result = deserialize_response::<Tweet>(value);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("empty response body"), "Got: {err}");
}
#[test]
fn adversarial_array_where_object_expected() {
let json = json!({"data": [{"id": "1", "text": "oops"}]});
let result = serde_json::from_value::<ApiResponse<Tweet>>(json);
assert!(result.is_err(), "Should fail: array where object expected");
}
#[test]
fn adversarial_string_where_object_expected() {
let json = json!({"data": "not an object"});
let result = serde_json::from_value::<ApiResponse<Tweet>>(json);
assert!(result.is_err(), "Should fail: string where object expected");
}
#[test]
fn adversarial_null_data_field() {
let json = json!({"data": null});
let result = serde_json::from_value::<ApiResponse<Tweet>>(json);
assert!(result.is_err(), "Should fail: null data");
}
#[test]
fn adversarial_numeric_overflow_u64() {
let json = json!({
"data": {
"id": "123",
"text": "hi",
"public_metrics": {
"like_count": 99_999_999_999_999u64,
"retweet_count": 0,
"reply_count": 0,
"quote_count": 0,
"bookmark_count": 0,
"impression_count": 0
}
}
});
let resp: ApiResponse<Tweet> =
serde_json::from_value(json).expect("Tweet with large u64 must deserialize");
assert_eq!(
resp.data
.public_metrics
.expect("public_metrics must be present")
.like_count,
99_999_999_999_999
);
}
#[test]
fn adversarial_negative_count() {
let json = json!({
"data": {
"id": "123",
"text": "hi",
"public_metrics": {
"like_count": -1,
"retweet_count": 0,
"reply_count": 0,
"quote_count": 0,
"bookmark_count": 0,
"impression_count": 0
}
}
});
let result = serde_json::from_value::<ApiResponse<Tweet>>(json);
assert!(result.is_err(), "Should fail: negative u64");
}
#[test]
fn adversarial_deeply_nested_unknown_fields() {
let json = json!({
"data": {"id": "123", "text": "hi"},
"extra_field": {
"a": {"b": {"c": [1, 2, 3]}}
}
});
let resp: ApiResponse<Tweet> =
serde_json::from_value(json).expect("Tweet with deep unknown fields must deserialize");
assert!(resp.extra.contains_key("extra_field"));
}
#[test]
fn adversarial_empty_string_required_fields() {
let json = json!({"data": {"id": "", "text": ""}});
let resp: ApiResponse<Tweet> = serde_json::from_value(json)
.expect("Tweet with empty string required fields must deserialize");
assert_eq!(resp.data.id, "");
assert_eq!(resp.data.text, "");
}
#[test]
fn adversarial_errors_field_no_data_raw_serde() {
let json = json!({"errors": [{"message": "forbidden"}]});
let result = serde_json::from_value::<ApiResponse<Tweet>>(json);
assert!(result.is_err(), "Should fail: no data field");
}
#[test]
fn errors_only_response_returns_validation_error() {
let json = json!({
"errors": [{
"detail": "Could not find tweet with id: [123].",
"title": "Not Found Error",
"resource_type": "tweet",
"type": "https://api.twitter.com/2/problems/resource-not-found"
}]
});
let result = deserialize_response::<Tweet>(json);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_validation(), "Expected Validation error, got: {err}");
let msg = err.to_string();
assert!(
msg.contains("Not Found Error"),
"Error should contain API message: {msg}"
);
}
#[test]
fn adversarial_extra_top_level_on_action() {
let json = json!({
"data": {"liked": true},
"extra_top_level": "ignored_by_consumer"
});
let resp: ApiResponse<LikedResult> = serde_json::from_value(json)
.expect("LikedResult with extra top-level fields must deserialize");
assert!(resp.data.liked);
assert_eq!(resp.extra["extra_top_level"], "ignored_by_consumer");
}
#[test]
fn adversarial_wrong_bool_type_for_action() {
let json = json!({"data": {"liked": "true"}});
let result = serde_json::from_value::<ApiResponse<LikedResult>>(json);
assert!(result.is_err(), "Should fail: string instead of bool");
}
#[test]
fn adversarial_partial_errors_with_valid_data() {
let json = json!({
"data": {"id": "123", "text": "partial"},
"errors": [{"message": "some field unavailable", "title": "Partial Error"}]
});
let resp: ApiResponse<Tweet> = serde_json::from_value(json)
.expect("Tweet with partial errors payload must deserialize");
assert_eq!(resp.data.id, "123");
let errors = resp.errors.expect("errors field must be present");
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].message.as_deref(), Some("some field unavailable"));
}
#[test]
fn adversarial_huge_array_in_data() {
let tweets: Vec<Value> = (0..1000)
.map(|i| json!({"id": i.to_string(), "text": format!("tweet {i}")}))
.collect();
let json = json!({"data": tweets});
let resp: ApiResponse<Vec<Tweet>> =
serde_json::from_value(json).expect("1000-element Tweet list must deserialize");
assert_eq!(resp.data.len(), 1000);
}
#[test]
fn adversarial_completely_wrong_shape() {
let json = json!({"data": 42});
let result = serde_json::from_value::<ApiResponse<Tweet>>(json);
assert!(result.is_err());
}
#[test]
fn adversarial_api_error_with_extra_fields() {
let json = json!({
"data": {"id": "1", "text": "ok"},
"errors": [{
"message": "oops",
"new_error_field": "surprise"
}]
});
let resp: ApiResponse<Tweet> = serde_json::from_value(json)
.expect("Tweet with errors containing extra fields must deserialize");
let error = &resp.errors.expect("errors field must be present")[0];
assert_eq!(error.extra["new_error_field"], "surprise");
}
}