Skip to main content

socorro_cli/models/
search.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5use super::common::{deserialize_string_or_number, deserialize_string_or_number_required};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Serialize, Deserialize)]
10pub struct SearchResponse {
11    pub total: u64,
12    pub hits: Vec<CrashHit>,
13    #[serde(default)]
14    pub facets: HashMap<String, Vec<FacetBucket>>,
15}
16
17#[derive(Debug, Serialize, Deserialize)]
18pub struct CrashHit {
19    pub uuid: String,
20    pub date: String,
21    pub signature: String,
22    pub product: String,
23    pub version: String,
24    #[serde(default)]
25    pub platform: Option<String>,
26    #[serde(default, deserialize_with = "deserialize_string_or_number")]
27    pub build_id: Option<String>,
28    #[serde(default)]
29    pub release_channel: Option<String>,
30    #[serde(default)]
31    pub platform_version: Option<String>,
32}
33
34#[derive(Debug, Serialize, Deserialize)]
35pub struct FacetBucket {
36    #[serde(deserialize_with = "deserialize_string_or_number_required")]
37    pub term: String,
38    pub count: u64,
39}
40
41pub struct SearchParams {
42    pub signature: Option<String>,
43    pub proto_signature: Option<String>,
44    pub product: String,
45    pub version: Option<String>,
46    pub platform: Option<String>,
47    pub cpu_arch: Option<String>,
48    pub release_channel: Option<String>,
49    pub platform_version: Option<String>,
50    pub process_type: Option<String>,
51    pub date_from: String,
52    pub date_to: Option<String>,
53    pub limit: usize,
54    pub facets: Vec<String>,
55    pub facets_size: Option<usize>,
56    pub sort: String,
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn test_deserialize_search_response() {
65        let json = r#"{
66            "total": 42,
67            "hits": [
68                {
69                    "uuid": "247653e8-7a18-4836-97d1-42a720260120",
70                    "date": "2024-01-15T10:30:00",
71                    "signature": "mozilla::SomeFunction",
72                    "product": "Firefox",
73                    "version": "120.0",
74                    "platform": "Windows"
75                }
76            ],
77            "facets": {}
78        }"#;
79
80        let response: SearchResponse = serde_json::from_str(json).unwrap();
81        assert_eq!(response.total, 42);
82        assert_eq!(response.hits.len(), 1);
83        assert_eq!(
84            response.hits[0].uuid,
85            "247653e8-7a18-4836-97d1-42a720260120"
86        );
87        assert_eq!(response.hits[0].signature, "mozilla::SomeFunction");
88    }
89
90    #[test]
91    fn test_deserialize_search_response_with_facets() {
92        let json = r#"{
93            "total": 100,
94            "hits": [],
95            "facets": {
96                "version": [
97                    {"term": "120.0", "count": 50},
98                    {"term": "119.0", "count": 30},
99                    {"term": "118.0", "count": 20}
100                ],
101                "platform": [
102                    {"term": "Windows", "count": 60},
103                    {"term": "Linux", "count": 40}
104                ]
105            }
106        }"#;
107
108        let response: SearchResponse = serde_json::from_str(json).unwrap();
109        assert_eq!(response.total, 100);
110        assert_eq!(response.facets.len(), 2);
111
112        let version_facets = response.facets.get("version").unwrap();
113        assert_eq!(version_facets.len(), 3);
114        assert_eq!(version_facets[0].term, "120.0");
115        assert_eq!(version_facets[0].count, 50);
116    }
117
118    #[test]
119    fn test_deserialize_facet_with_integer_term() {
120        let json = r#"{
121            "total": 100,
122            "hits": [],
123            "facets": {
124                "build_id": [
125                    {"term": 20251116092356, "count": 47},
126                    {"term": 20251116210936, "count": 40},
127                    {"term": "20251115204042", "count": 22}
128                ]
129            }
130        }"#;
131
132        let response: SearchResponse = serde_json::from_str(json).unwrap();
133        let build_id_facets = response.facets.get("build_id").unwrap();
134        assert_eq!(build_id_facets.len(), 3);
135        assert_eq!(build_id_facets[0].term, "20251116092356");
136        assert_eq!(build_id_facets[0].count, 47);
137        assert_eq!(build_id_facets[1].term, "20251116210936");
138        assert_eq!(build_id_facets[2].term, "20251115204042");
139    }
140
141    #[test]
142    fn test_deserialize_crash_hit_missing_platform() {
143        let json = r#"{
144            "uuid": "test-id",
145            "date": "2024-01-15",
146            "signature": "crash_sig",
147            "product": "Firefox",
148            "version": "120.0"
149        }"#;
150
151        let hit: CrashHit = serde_json::from_str(json).unwrap();
152        assert_eq!(hit.platform, None);
153    }
154
155    #[test]
156    fn test_deserialize_empty_response() {
157        let json = r#"{
158            "total": 0,
159            "hits": [],
160            "facets": {}
161        }"#;
162
163        let response: SearchResponse = serde_json::from_str(json).unwrap();
164        assert_eq!(response.total, 0);
165        assert!(response.hits.is_empty());
166        assert!(response.facets.is_empty());
167    }
168}