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