mod attachment;
mod bug;
mod comment;
mod common;
mod group;
mod product;
mod user;
pub use attachment::{Attachment, UpdateAttachmentParams, UploadAttachmentParams};
pub use bug::{
partition_filters, Bug, BugTemplate, CreateBugParams, FieldChange, FieldValue, HistoryEntry,
IdListUpdate, QueryKind, SavedQuery, SearchParams, StatusTransition, UpdateBugParams,
BOOLEAN_CHART_FIELD_NAMES,
};
pub use comment::{Comment, UpdateCommentTagsParams};
pub use common::{
ApiMode, AuthMethod, ExtensionInfo, FlagStatus, FlagUpdate, OutputFormat, ServerExtensions,
ServerInfoResponse, ServerVersion,
};
pub use group::{CreateGroupParams, GroupInfo, GroupMember, UpdateGroupParams};
pub use product::{
Classification, ClassificationProduct, Component, CreateComponentParams, CreateProductParams,
Milestone, Product, ProductListType, UpdateComponentParams, UpdateProductParams, Version,
};
pub use user::{BugzillaUser, CreateUserParams, UpdateUserParams, UserGroup, WhoamiResponse};
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn product_list_type_from_str_valid() {
assert_eq!(
"accessible".parse::<ProductListType>().unwrap(),
ProductListType::Accessible
);
assert_eq!(
"selectable".parse::<ProductListType>().unwrap(),
ProductListType::Selectable
);
assert_eq!(
"enterable".parse::<ProductListType>().unwrap(),
ProductListType::Enterable
);
}
#[test]
fn product_list_type_from_str_invalid() {
let err = "bogus".parse::<ProductListType>().unwrap_err();
assert!(err.contains("invalid product type"));
}
#[test]
fn product_list_type_as_api_path() {
assert_eq!(
ProductListType::Accessible.as_api_path(),
"product_accessible"
);
assert_eq!(
ProductListType::Selectable.as_api_path(),
"product_selectable"
);
assert_eq!(
ProductListType::Enterable.as_api_path(),
"product_enterable"
);
}
#[test]
fn product_list_type_default_is_accessible() {
assert_eq!(ProductListType::default(), ProductListType::Accessible);
}
#[test]
fn search_params_has_filters_empty() {
let params = SearchParams::default();
assert!(!params.has_filters());
}
#[test]
fn search_params_has_filters_with_product() {
let params = SearchParams {
product: vec!["TestProduct".into()],
..Default::default()
};
assert!(params.has_filters());
}
#[test]
fn search_params_has_filters_with_ids() {
let params = SearchParams {
id: vec![1, 2, 3],
..Default::default()
};
assert!(params.has_filters());
}
#[test]
fn search_params_has_filters_with_quicksearch() {
let params = SearchParams {
quicksearch: Some("crash".into()),
..Default::default()
};
assert!(params.has_filters());
}
#[test]
fn bug_deserialize_minimal() {
let json = r#"{"id": 42}"#;
let bug: Bug = serde_json::from_str(json).unwrap();
assert_eq!(bug.id, 42);
assert_eq!(bug.summary, "");
assert_eq!(bug.status, "");
assert!(bug.resolution.is_none());
}
#[test]
fn bug_deserialize_full() {
let json = r#"{
"id": 1,
"summary": "Test bug",
"status": "NEW",
"resolution": "FIXED",
"product": "TestProduct",
"component": "General",
"assigned_to": "user@example.com",
"keywords": ["crash", "regression"],
"blocks": [2, 3],
"depends_on": [4]
}"#;
let bug: Bug = serde_json::from_str(json).unwrap();
assert_eq!(bug.id, 1);
assert_eq!(bug.summary, "Test bug");
assert_eq!(bug.keywords, vec!["crash", "regression"]);
assert_eq!(bug.blocks, vec![2, 3]);
}
#[test]
fn comment_deserialize_minimal() {
let json = r#"{"id": 100}"#;
let comment: Comment = serde_json::from_str(json).unwrap();
assert_eq!(comment.id, 100);
assert_eq!(comment.text, "");
assert!(!comment.is_private);
}
#[test]
fn attachment_deserialize_minimal() {
let json = r#"{"id": 50}"#;
let att: Attachment = serde_json::from_str(json).unwrap();
assert_eq!(att.id, 50);
assert_eq!(att.file_name, "");
assert!(!att.is_obsolete);
}
#[test]
fn whoami_deserialize() {
let json = r#"{"id": 1, "name": "admin@example.com", "real_name": "Admin"}"#;
let whoami: WhoamiResponse = serde_json::from_str(json).unwrap();
assert_eq!(whoami.id, 1);
assert_eq!(whoami.name, "admin@example.com");
assert_eq!(whoami.real_name.as_deref(), Some("Admin"));
}
#[test]
fn update_bug_params_skips_none_fields() {
let params = UpdateBugParams {
status: Some("RESOLVED".into()),
..Default::default()
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["status"], "RESOLVED");
assert!(json.get("resolution").is_none());
assert!(json.get("flags").is_none());
}
#[test]
fn group_info_deserialize() {
let json = r#"{
"id": 10,
"name": "admin",
"description": "Administrators",
"is_active": true,
"membership": [{"id": 1, "name": "user@test.com"}]
}"#;
let group: GroupInfo = serde_json::from_str(json).unwrap();
assert_eq!(group.id, 10);
assert_eq!(group.name, "admin");
assert!(group.is_active);
assert_eq!(group.membership.len(), 1);
}
#[test]
fn classification_deserialize() {
let json = r#"{
"id": 1,
"name": "Unclassified",
"description": "Default",
"sort_key": 0,
"products": [{"id": 1, "name": "TestProduct", "description": "Test"}]
}"#;
let cls: Classification = serde_json::from_str(json).unwrap();
assert_eq!(cls.name, "Unclassified");
assert_eq!(cls.products.len(), 1);
}
#[test]
fn history_entry_deserialize() {
let json = r#"{
"who": "user@test.com",
"when": "2025-01-01T00:00:00Z",
"changes": [{"field_name": "status", "removed": "NEW", "added": "RESOLVED"}]
}"#;
let entry: HistoryEntry = serde_json::from_str(json).unwrap();
assert_eq!(entry.who, "user@test.com");
assert_eq!(entry.changes.len(), 1);
assert_eq!(entry.changes[0].field_name, "status");
assert_eq!(entry.changes[0].added, "RESOLVED");
}
}