1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5pub struct Issue {
6 pub id: i64,
7 pub title: String,
8 pub description: Option<String>,
9 pub status: String,
10 pub priority: String,
11 pub parent_id: Option<i64>,
12 pub created_at: DateTime<Utc>,
13 pub updated_at: DateTime<Utc>,
14 pub closed_at: Option<DateTime<Utc>>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18pub struct Comment {
19 pub id: i64,
20 pub issue_id: i64,
21 pub content: String,
22 pub created_at: DateTime<Utc>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
26pub struct Session {
27 pub id: i64,
28 pub started_at: DateTime<Utc>,
29 pub ended_at: Option<DateTime<Utc>>,
30 pub active_issue_id: Option<i64>,
31 pub handoff_notes: Option<String>,
32 pub last_action: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
36pub struct Milestone {
37 pub id: i64,
38 pub name: String,
39 pub description: Option<String>,
40 pub status: String,
41 pub created_at: DateTime<Utc>,
42 pub closed_at: Option<DateTime<Utc>>,
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use proptest::prelude::*;
49
50 #[test]
53 fn test_issue_serialization_json() {
54 let issue = Issue {
55 id: 1,
56 title: "Test issue".to_string(),
57 description: Some("A description".to_string()),
58 status: "open".to_string(),
59 priority: "high".to_string(),
60 parent_id: None,
61 created_at: Utc::now(),
62 updated_at: Utc::now(),
63 closed_at: None,
64 };
65
66 let json = serde_json::to_string(&issue).unwrap();
67 let deserialized: Issue = serde_json::from_str(&json).unwrap();
68
69 assert_eq!(issue.id, deserialized.id);
70 assert_eq!(issue.title, deserialized.title);
71 assert_eq!(issue.description, deserialized.description);
72 assert_eq!(issue.status, deserialized.status);
73 assert_eq!(issue.priority, deserialized.priority);
74 assert_eq!(issue.parent_id, deserialized.parent_id);
75 }
76
77 #[test]
78 fn test_issue_with_parent() {
79 let issue = Issue {
80 id: 2,
81 title: "Child issue".to_string(),
82 description: None,
83 status: "open".to_string(),
84 priority: "medium".to_string(),
85 parent_id: Some(1),
86 created_at: Utc::now(),
87 updated_at: Utc::now(),
88 closed_at: None,
89 };
90
91 let json = serde_json::to_string(&issue).unwrap();
92 let deserialized: Issue = serde_json::from_str(&json).unwrap();
93
94 assert_eq!(deserialized.parent_id, Some(1));
95 }
96
97 #[test]
98 fn test_issue_closed_at() {
99 let now = Utc::now();
100 let issue = Issue {
101 id: 1,
102 title: "Closed issue".to_string(),
103 description: None,
104 status: "closed".to_string(),
105 priority: "low".to_string(),
106 parent_id: None,
107 created_at: now,
108 updated_at: now,
109 closed_at: Some(now),
110 };
111
112 let json = serde_json::to_string(&issue).unwrap();
113 let deserialized: Issue = serde_json::from_str(&json).unwrap();
114
115 assert_eq!(deserialized.closed_at, Some(now));
116 }
117
118 #[test]
119 fn test_issue_unicode_fields() {
120 let issue = Issue {
121 id: 1,
122 title: "测试 🐛 αβγ".to_string(),
123 description: Some("Description with émojis 🎉".to_string()),
124 status: "open".to_string(),
125 priority: "high".to_string(),
126 parent_id: None,
127 created_at: Utc::now(),
128 updated_at: Utc::now(),
129 closed_at: None,
130 };
131
132 let json = serde_json::to_string(&issue).unwrap();
133 let deserialized: Issue = serde_json::from_str(&json).unwrap();
134
135 assert_eq!(deserialized.title, "测试 🐛 αβγ");
136 assert_eq!(
137 deserialized.description,
138 Some("Description with émojis 🎉".to_string())
139 );
140 }
141
142 #[test]
145 fn test_comment_serialization() {
146 let comment = Comment {
147 id: 1,
148 issue_id: 42,
149 content: "A comment".to_string(),
150 created_at: Utc::now(),
151 };
152
153 let json = serde_json::to_string(&comment).unwrap();
154 let deserialized: Comment = serde_json::from_str(&json).unwrap();
155
156 assert_eq!(comment.id, deserialized.id);
157 assert_eq!(comment.issue_id, deserialized.issue_id);
158 assert_eq!(comment.content, deserialized.content);
159 }
160
161 #[test]
162 fn test_comment_empty_content() {
163 let comment = Comment {
164 id: 1,
165 issue_id: 1,
166 content: "".to_string(),
167 created_at: Utc::now(),
168 };
169
170 let json = serde_json::to_string(&comment).unwrap();
171 let deserialized: Comment = serde_json::from_str(&json).unwrap();
172
173 assert_eq!(deserialized.content, "");
174 }
175
176 #[test]
179 fn test_session_serialization() {
180 let session = Session {
181 id: 1,
182 started_at: Utc::now(),
183 ended_at: None,
184 active_issue_id: Some(5),
185 handoff_notes: Some("Notes here".to_string()),
186 last_action: None,
187 };
188
189 let json = serde_json::to_string(&session).unwrap();
190 let deserialized: Session = serde_json::from_str(&json).unwrap();
191
192 assert_eq!(session.id, deserialized.id);
193 assert_eq!(session.active_issue_id, deserialized.active_issue_id);
194 assert_eq!(session.handoff_notes, deserialized.handoff_notes);
195 }
196
197 #[test]
198 fn test_session_ended() {
199 let now = Utc::now();
200 let session = Session {
201 id: 1,
202 started_at: now,
203 ended_at: Some(now),
204 active_issue_id: None,
205 handoff_notes: Some("Final notes".to_string()),
206 last_action: None,
207 };
208
209 let json = serde_json::to_string(&session).unwrap();
210 let deserialized: Session = serde_json::from_str(&json).unwrap();
211
212 assert_eq!(deserialized.ended_at, Some(now));
213 assert_eq!(deserialized.handoff_notes, Some("Final notes".to_string()));
214 }
215
216 #[test]
219 fn test_milestone_serialization() {
220 let milestone = Milestone {
221 id: 1,
222 name: "v1.0".to_string(),
223 description: Some("First release".to_string()),
224 status: "open".to_string(),
225 created_at: Utc::now(),
226 closed_at: None,
227 };
228
229 let json = serde_json::to_string(&milestone).unwrap();
230 let deserialized: Milestone = serde_json::from_str(&json).unwrap();
231
232 assert_eq!(milestone.id, deserialized.id);
233 assert_eq!(milestone.name, deserialized.name);
234 assert_eq!(milestone.description, deserialized.description);
235 assert_eq!(milestone.status, deserialized.status);
236 }
237
238 #[test]
239 fn test_milestone_closed() {
240 let now = Utc::now();
241 let milestone = Milestone {
242 id: 1,
243 name: "v1.0".to_string(),
244 description: None,
245 status: "closed".to_string(),
246 created_at: now,
247 closed_at: Some(now),
248 };
249
250 let json = serde_json::to_string(&milestone).unwrap();
251 let deserialized: Milestone = serde_json::from_str(&json).unwrap();
252
253 assert_eq!(deserialized.closed_at, Some(now));
254 assert_eq!(deserialized.status, "closed");
255 }
256
257 proptest! {
260 #[test]
261 fn prop_issue_json_roundtrip(
262 id in 1i64..10000,
263 title in "[a-zA-Z0-9 ]{1,100}",
264 status in "open|closed",
265 priority in "low|medium|high|critical"
266 ) {
267 let issue = Issue {
268 id,
269 title: title.clone(),
270 description: None,
271 status: status.clone(),
272 priority: priority.clone(),
273 parent_id: None,
274 created_at: Utc::now(),
275 updated_at: Utc::now(),
276 closed_at: None,
277 };
278
279 let json = serde_json::to_string(&issue).unwrap();
280 let deserialized: Issue = serde_json::from_str(&json).unwrap();
281
282 prop_assert_eq!(deserialized.id, id);
283 prop_assert_eq!(deserialized.title, title);
284 prop_assert_eq!(deserialized.status, status);
285 prop_assert_eq!(deserialized.priority, priority);
286 }
287
288 #[test]
289 fn prop_comment_json_roundtrip(
290 id in 1i64..10000,
291 issue_id in 1i64..10000,
292 content in "[a-zA-Z0-9 ]{0,500}"
293 ) {
294 let comment = Comment {
295 id,
296 issue_id,
297 content: content.clone(),
298 created_at: Utc::now(),
299 };
300
301 let json = serde_json::to_string(&comment).unwrap();
302 let deserialized: Comment = serde_json::from_str(&json).unwrap();
303
304 prop_assert_eq!(deserialized.id, id);
305 prop_assert_eq!(deserialized.issue_id, issue_id);
306 prop_assert_eq!(deserialized.content, content);
307 }
308
309 #[test]
310 fn prop_session_json_roundtrip(
311 id in 1i64..10000,
312 active_issue_id in prop::option::of(1i64..10000),
313 handoff_notes in prop::option::of("[a-zA-Z0-9 ]{0,200}")
314 ) {
315 let session = Session {
316 id,
317 started_at: Utc::now(),
318 ended_at: None,
319 active_issue_id,
320 handoff_notes: handoff_notes.clone(),
321 last_action: None,
322 };
323
324 let json = serde_json::to_string(&session).unwrap();
325 let deserialized: Session = serde_json::from_str(&json).unwrap();
326
327 prop_assert_eq!(deserialized.id, id);
328 prop_assert_eq!(deserialized.active_issue_id, active_issue_id);
329 prop_assert_eq!(deserialized.handoff_notes, handoff_notes);
330 }
331
332 #[test]
333 fn prop_milestone_json_roundtrip(
334 id in 1i64..10000,
335 name in "[a-zA-Z0-9.]{1,50}",
336 status in "open|closed"
337 ) {
338 let milestone = Milestone {
339 id,
340 name: name.clone(),
341 description: None,
342 status: status.clone(),
343 created_at: Utc::now(),
344 closed_at: None,
345 };
346
347 let json = serde_json::to_string(&milestone).unwrap();
348 let deserialized: Milestone = serde_json::from_str(&json).unwrap();
349
350 prop_assert_eq!(deserialized.id, id);
351 prop_assert_eq!(deserialized.name, name);
352 prop_assert_eq!(deserialized.status, status);
353 }
354
355 #[test]
356 fn prop_issue_with_optional_fields(
357 has_desc in proptest::bool::ANY,
358 has_parent in proptest::bool::ANY,
359 is_closed in proptest::bool::ANY
360 ) {
361 let now = Utc::now();
362 let issue = Issue {
363 id: 1,
364 title: "Test".to_string(),
365 description: if has_desc { Some("Desc".to_string()) } else { None },
366 status: if is_closed { "closed".to_string() } else { "open".to_string() },
367 priority: "medium".to_string(),
368 parent_id: if has_parent { Some(99) } else { None },
369 created_at: now,
370 updated_at: now,
371 closed_at: if is_closed { Some(now) } else { None },
372 };
373
374 let json = serde_json::to_string(&issue).unwrap();
375 let deserialized: Issue = serde_json::from_str(&json).unwrap();
376
377 prop_assert_eq!(deserialized.description.is_some(), has_desc);
378 prop_assert_eq!(deserialized.parent_id.is_some(), has_parent);
379 prop_assert_eq!(deserialized.closed_at.is_some(), is_closed);
380 }
381 }
382}