1use crate::diff::ComparisonResult;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct SnapshotData {
15 pub status_code: u16,
17 pub headers: HashMap<String, String>,
19 pub body: Vec<u8>,
21 pub body_json: Option<Value>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SyncSnapshot {
28 pub id: String,
30 pub endpoint: String,
32 pub method: String,
34 pub sync_cycle_id: String,
36 pub timestamp: DateTime<Utc>,
38 pub before: SnapshotData,
40 pub after: SnapshotData,
42 pub changes: ComparisonResult,
44 pub response_time_before: Option<u64>,
46 pub response_time_after: Option<u64>,
48}
49
50impl SyncSnapshot {
51 #[allow(clippy::too_many_arguments)]
53 pub fn new(
54 endpoint: String,
55 method: String,
56 sync_cycle_id: String,
57 before: SnapshotData,
58 after: SnapshotData,
59 changes: ComparisonResult,
60 response_time_before: Option<u64>,
61 response_time_after: Option<u64>,
62 ) -> Self {
63 let id = format!(
64 "snapshot_{}_{}_{}",
65 endpoint.replace('/', "_").replace(['{', '}'], ""),
66 method.to_lowercase(),
67 &uuid::Uuid::new_v4().to_string()[..8]
68 );
69
70 Self {
71 id,
72 endpoint,
73 method,
74 sync_cycle_id,
75 timestamp: Utc::now(),
76 before,
77 after,
78 changes,
79 response_time_before,
80 response_time_after,
81 }
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct EndpointTimeline {
88 pub endpoint: String,
90 pub method: String,
92 pub snapshots: Vec<SyncSnapshot>,
94 pub response_time_trends: Vec<(DateTime<Utc>, Option<u64>)>,
96 pub status_code_history: Vec<(DateTime<Utc>, u16)>,
98 pub error_patterns: Vec<ErrorPattern>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct ErrorPattern {
105 pub status_code: u16,
107 pub message_pattern: Option<String>,
109 pub occurrences: usize,
111 pub first_seen: DateTime<Utc>,
113 pub last_seen: DateTime<Utc>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct EndpointEvolutionSummary {
120 pub endpoint: String,
122 pub method: String,
124 pub total_snapshots: usize,
126 pub total_changes: usize,
128 pub avg_response_time: Option<f64>,
130 pub most_common_status: Option<u16>,
132 pub field_change_frequency: HashMap<String, usize>,
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 fn create_test_summary(total_differences: usize) -> crate::diff::ComparisonSummary {
141 crate::diff::ComparisonSummary {
142 total_differences,
143 added_fields: 0,
144 removed_fields: 0,
145 changed_fields: total_differences,
146 type_changes: 0,
147 }
148 }
149
150 fn create_test_comparison_result(
151 matches: bool,
152 differences: Vec<crate::diff::Difference>,
153 ) -> crate::diff::ComparisonResult {
154 crate::diff::ComparisonResult {
155 matches,
156 status_match: matches,
157 headers_match: matches,
158 body_match: matches,
159 differences: differences.clone(),
160 summary: create_test_summary(differences.len()),
161 }
162 }
163
164 fn create_test_snapshot_data() -> SnapshotData {
165 let mut headers = HashMap::new();
166 headers.insert("content-type".to_string(), "application/json".to_string());
167
168 SnapshotData {
169 status_code: 200,
170 headers,
171 body: b"test body".to_vec(),
172 body_json: Some(serde_json::json!({"status": "ok"})),
173 }
174 }
175
176 #[test]
177 fn test_snapshot_data_creation() {
178 let snapshot = create_test_snapshot_data();
179
180 assert_eq!(snapshot.status_code, 200);
181 assert_eq!(snapshot.headers.get("content-type").unwrap(), "application/json");
182 assert_eq!(snapshot.body, b"test body");
183 assert!(snapshot.body_json.is_some());
184 }
185
186 #[test]
187 fn test_sync_snapshot_new() {
188 let before = create_test_snapshot_data();
189 let mut after = create_test_snapshot_data();
190 after.status_code = 201;
191
192 let differences = vec![crate::diff::Difference::new(
193 "$.status_code".to_string(),
194 crate::diff::DifferenceType::Changed {
195 path: "$.status_code".to_string(),
196 original: "200".to_string(),
197 current: "201".to_string(),
198 },
199 )];
200 let comparison = create_test_comparison_result(false, differences);
201
202 let snapshot = SyncSnapshot::new(
203 "/api/users".to_string(),
204 "GET".to_string(),
205 "cycle-123".to_string(),
206 before.clone(),
207 after.clone(),
208 comparison,
209 Some(100),
210 Some(120),
211 );
212
213 assert!(snapshot.id.contains("snapshot"));
214 assert_eq!(snapshot.endpoint, "/api/users");
215 assert_eq!(snapshot.method, "GET");
216 assert_eq!(snapshot.sync_cycle_id, "cycle-123");
217 assert_eq!(snapshot.before.status_code, 200);
218 assert_eq!(snapshot.after.status_code, 201);
219 assert_eq!(snapshot.response_time_before, Some(100));
220 assert_eq!(snapshot.response_time_after, Some(120));
221 }
222
223 #[test]
224 fn test_sync_snapshot_id_generation() {
225 let before = create_test_snapshot_data();
226 let after = create_test_snapshot_data();
227
228 let comparison = create_test_comparison_result(true, vec![]);
229
230 let snapshot1 = SyncSnapshot::new(
231 "/api/users/{id}".to_string(),
232 "GET".to_string(),
233 "cycle-1".to_string(),
234 before.clone(),
235 after.clone(),
236 comparison.clone(),
237 None,
238 None,
239 );
240
241 let snapshot2 = SyncSnapshot::new(
242 "/api/users/{id}".to_string(),
243 "GET".to_string(),
244 "cycle-2".to_string(),
245 before.clone(),
246 after.clone(),
247 comparison,
248 None,
249 None,
250 );
251
252 assert_ne!(snapshot1.id, snapshot2.id);
254
255 assert!(snapshot1.id.starts_with("snapshot_"));
257 assert!(snapshot2.id.starts_with("snapshot_"));
258 }
259
260 #[test]
261 fn test_sync_snapshot_serialization() {
262 let before = create_test_snapshot_data();
263 let after = create_test_snapshot_data();
264
265 let comparison = create_test_comparison_result(true, vec![]);
266
267 let snapshot = SyncSnapshot::new(
268 "/api/test".to_string(),
269 "POST".to_string(),
270 "cycle-abc".to_string(),
271 before,
272 after,
273 comparison,
274 Some(50),
275 Some(55),
276 );
277
278 let json = serde_json::to_string(&snapshot).unwrap();
279
280 assert!(json.contains("/api/test"));
281 assert!(json.contains("POST"));
282 assert!(json.contains("cycle-abc"));
283 }
284
285 #[test]
286 fn test_sync_snapshot_deserialization() {
287 let before = create_test_snapshot_data();
288 let after = create_test_snapshot_data();
289
290 let comparison = create_test_comparison_result(true, vec![]);
291
292 let original = SyncSnapshot::new(
293 "/api/test".to_string(),
294 "GET".to_string(),
295 "cycle-xyz".to_string(),
296 before,
297 after,
298 comparison,
299 Some(100),
300 Some(105),
301 );
302
303 let json = serde_json::to_string(&original).unwrap();
304 let deserialized: SyncSnapshot = serde_json::from_str(&json).unwrap();
305
306 assert_eq!(deserialized.endpoint, original.endpoint);
307 assert_eq!(deserialized.method, original.method);
308 assert_eq!(deserialized.sync_cycle_id, original.sync_cycle_id);
309 assert_eq!(deserialized.response_time_before, original.response_time_before);
310 assert_eq!(deserialized.response_time_after, original.response_time_after);
311 }
312
313 #[test]
314 fn test_endpoint_timeline_creation() {
315 let snapshots = vec![];
316
317 let timeline = EndpointTimeline {
318 endpoint: "/api/users".to_string(),
319 method: "GET".to_string(),
320 snapshots,
321 response_time_trends: vec![],
322 status_code_history: vec![],
323 error_patterns: vec![],
324 };
325
326 assert_eq!(timeline.endpoint, "/api/users");
327 assert_eq!(timeline.method, "GET");
328 assert!(timeline.snapshots.is_empty());
329 }
330
331 #[test]
332 fn test_endpoint_timeline_with_snapshots() {
333 let before = create_test_snapshot_data();
334 let after = create_test_snapshot_data();
335
336 let comparison = create_test_comparison_result(true, vec![]);
337
338 let snapshot = SyncSnapshot::new(
339 "/api/users".to_string(),
340 "GET".to_string(),
341 "cycle-1".to_string(),
342 before,
343 after,
344 comparison,
345 Some(100),
346 Some(100),
347 );
348
349 let now = Utc::now();
350 let response_time_trends = vec![(now, Some(100))];
351 let status_code_history = vec![(now, 200)];
352
353 let timeline = EndpointTimeline {
354 endpoint: "/api/users".to_string(),
355 method: "GET".to_string(),
356 snapshots: vec![snapshot],
357 response_time_trends,
358 status_code_history,
359 error_patterns: vec![],
360 };
361
362 assert_eq!(timeline.snapshots.len(), 1);
363 assert_eq!(timeline.response_time_trends.len(), 1);
364 assert_eq!(timeline.status_code_history.len(), 1);
365 }
366
367 #[test]
368 fn test_error_pattern_creation() {
369 let now = Utc::now();
370
371 let pattern = ErrorPattern {
372 status_code: 404,
373 message_pattern: Some("Not found".to_string()),
374 occurrences: 5,
375 first_seen: now,
376 last_seen: now,
377 };
378
379 assert_eq!(pattern.status_code, 404);
380 assert_eq!(pattern.message_pattern, Some("Not found".to_string()));
381 assert_eq!(pattern.occurrences, 5);
382 }
383
384 #[test]
385 fn test_error_pattern_serialization() {
386 let now = Utc::now();
387
388 let pattern = ErrorPattern {
389 status_code: 500,
390 message_pattern: Some("Internal server error".to_string()),
391 occurrences: 3,
392 first_seen: now,
393 last_seen: now,
394 };
395
396 let json = serde_json::to_string(&pattern).unwrap();
397
398 assert!(json.contains("500"));
399 assert!(json.contains("Internal server error"));
400 assert!(json.contains("3"));
401 }
402
403 #[test]
404 fn test_endpoint_evolution_summary_creation() {
405 let mut field_change_frequency = HashMap::new();
406 field_change_frequency.insert("$.user.name".to_string(), 5);
407 field_change_frequency.insert("$.user.email".to_string(), 3);
408
409 let summary = EndpointEvolutionSummary {
410 endpoint: "/api/users".to_string(),
411 method: "GET".to_string(),
412 total_snapshots: 10,
413 total_changes: 8,
414 avg_response_time: Some(125.5),
415 most_common_status: Some(200),
416 field_change_frequency,
417 };
418
419 assert_eq!(summary.endpoint, "/api/users");
420 assert_eq!(summary.method, "GET");
421 assert_eq!(summary.total_snapshots, 10);
422 assert_eq!(summary.total_changes, 8);
423 assert_eq!(summary.avg_response_time, Some(125.5));
424 assert_eq!(summary.most_common_status, Some(200));
425 assert_eq!(summary.field_change_frequency.len(), 2);
426 }
427
428 #[test]
429 fn test_endpoint_evolution_summary_serialization() {
430 let mut field_change_frequency = HashMap::new();
431 field_change_frequency.insert("$.status".to_string(), 2);
432
433 let summary = EndpointEvolutionSummary {
434 endpoint: "/api/test".to_string(),
435 method: "POST".to_string(),
436 total_snapshots: 5,
437 total_changes: 3,
438 avg_response_time: Some(100.0),
439 most_common_status: Some(201),
440 field_change_frequency,
441 };
442
443 let json = serde_json::to_string(&summary).unwrap();
444
445 assert!(json.contains("/api/test"));
446 assert!(json.contains("POST"));
447 assert!(json.contains("5"));
448 }
449
450 #[test]
451 fn test_snapshot_data_with_no_json() {
452 let mut headers = HashMap::new();
453 headers.insert("content-type".to_string(), "text/plain".to_string());
454
455 let snapshot = SnapshotData {
456 status_code: 200,
457 headers,
458 body: b"plain text".to_vec(),
459 body_json: None,
460 };
461
462 assert!(snapshot.body_json.is_none());
463 assert_eq!(snapshot.body, b"plain text");
464 }
465
466 #[test]
467 fn test_sync_snapshot_with_no_response_times() {
468 let before = create_test_snapshot_data();
469 let after = create_test_snapshot_data();
470
471 let comparison = create_test_comparison_result(true, vec![]);
472
473 let snapshot = SyncSnapshot::new(
474 "/api/test".to_string(),
475 "GET".to_string(),
476 "cycle-1".to_string(),
477 before,
478 after,
479 comparison,
480 None,
481 None,
482 );
483
484 assert_eq!(snapshot.response_time_before, None);
485 assert_eq!(snapshot.response_time_after, None);
486 }
487
488 #[test]
489 fn test_endpoint_timeline_serialization() {
490 let timeline = EndpointTimeline {
491 endpoint: "/api/users".to_string(),
492 method: "GET".to_string(),
493 snapshots: vec![],
494 response_time_trends: vec![],
495 status_code_history: vec![],
496 error_patterns: vec![],
497 };
498
499 let json = serde_json::to_string(&timeline).unwrap();
500
501 assert!(json.contains("/api/users"));
502 assert!(json.contains("GET"));
503 }
504
505 #[test]
506 fn test_snapshot_data_clone() {
507 let snapshot = create_test_snapshot_data();
508 let cloned = snapshot.clone();
509
510 assert_eq!(snapshot.status_code, cloned.status_code);
511 assert_eq!(snapshot.body, cloned.body);
512 }
513
514 #[test]
515 fn test_sync_snapshot_clone() {
516 let before = create_test_snapshot_data();
517 let after = create_test_snapshot_data();
518
519 let comparison = create_test_comparison_result(true, vec![]);
520
521 let snapshot = SyncSnapshot::new(
522 "/api/test".to_string(),
523 "GET".to_string(),
524 "cycle-1".to_string(),
525 before,
526 after,
527 comparison,
528 Some(100),
529 Some(100),
530 );
531
532 let cloned = snapshot.clone();
533
534 assert_eq!(snapshot.id, cloned.id);
535 assert_eq!(snapshot.endpoint, cloned.endpoint);
536 assert_eq!(snapshot.method, cloned.method);
537 }
538
539 #[test]
540 fn test_error_pattern_clone() {
541 let now = Utc::now();
542
543 let pattern = ErrorPattern {
544 status_code: 404,
545 message_pattern: Some("Not found".to_string()),
546 occurrences: 5,
547 first_seen: now,
548 last_seen: now,
549 };
550
551 let cloned = pattern.clone();
552
553 assert_eq!(pattern.status_code, cloned.status_code);
554 assert_eq!(pattern.message_pattern, cloned.message_pattern);
555 assert_eq!(pattern.occurrences, cloned.occurrences);
556 }
557}