1use std::collections::HashMap;
9use std::sync::{Arc, Mutex};
10
11use jsdet_core::observation::Value;
12
13#[derive(Clone, Debug)]
15pub struct ExtensionState {
16 pub tabs: Arc<Mutex<Vec<Tab>>>,
18 pub cookies: Arc<Mutex<Vec<Cookie>>>,
20 pub storage_local: Arc<Mutex<HashMap<String, String>>>,
22 pub storage_sync: Arc<Mutex<HashMap<String, String>>>,
24 pub alarms: Arc<Mutex<Vec<Alarm>>>,
26 pub pending_messages: Arc<Mutex<Vec<PendingMessage>>>,
28 pub extension_id: String,
30}
31
32#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
34pub struct Tab {
35 pub id: u32,
36 pub url: String,
37 pub title: String,
38 pub active: bool,
39 pub index: u32,
40}
41
42#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
44pub struct Cookie {
45 pub name: String,
46 pub value: String,
47 pub domain: String,
48 pub path: String,
49 pub secure: bool,
50 pub http_only: bool,
51}
52
53#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
55pub struct Alarm {
56 pub name: String,
57 pub scheduled_time: f64,
58 pub period_in_minutes: Option<f64>,
59}
60
61#[derive(Clone, Debug)]
63pub struct PendingMessage {
64 pub data: Value,
66 pub sender_origin: Option<String>,
68 pub is_external: bool,
70 pub sender_tab_id: Option<u32>,
72}
73
74impl Default for ExtensionState {
75 fn default() -> Self {
76 Self {
77 tabs: Arc::new(Mutex::new(vec![Tab {
78 id: 1,
79 url: "https://example.com".into(),
80 title: "Example".into(),
81 active: true,
82 index: 0,
83 }])),
84 cookies: Arc::new(Mutex::new(vec![Cookie {
85 name: "session".into(),
86 value: "abc123".into(),
87 domain: ".example.com".into(),
88 path: "/".into(),
89 secure: true,
90 http_only: true,
91 }])),
92 storage_local: Arc::new(Mutex::new(HashMap::new())),
93 storage_sync: Arc::new(Mutex::new(HashMap::new())),
94 alarms: Arc::new(Mutex::new(Vec::new())),
95 pending_messages: Arc::new(Mutex::new(Vec::new())),
96 extension_id: "test-extension-id".into(),
97 }
98 }
99}
100
101impl ExtensionState {
102 pub fn default_with_id(id: &str) -> Self {
104 Self {
105 extension_id: id.to_string(),
106 ..Self::default()
107 }
108 }
109
110 pub fn with_tabs(mut self, tabs: Vec<Tab>) -> Self {
112 self.tabs = Arc::new(Mutex::new(tabs));
113 self
114 }
115
116 pub fn with_cookies(mut self, cookies: Vec<Cookie>) -> Self {
118 self.cookies = Arc::new(Mutex::new(cookies));
119 self
120 }
121
122 pub fn with_storage(mut self, data: HashMap<String, String>) -> Self {
124 self.storage_local = Arc::new(Mutex::new(data));
125 self
126 }
127
128 pub fn queue_message(&self, msg: PendingMessage) {
131 if let Ok(mut guard) = self.pending_messages.lock() {
132 guard.push(msg);
133 }
134 }
135
136 pub fn take_messages(&self) -> Vec<PendingMessage> {
139 let mut guard = self
140 .pending_messages
141 .lock()
142 .unwrap_or_else(std::sync::PoisonError::into_inner);
143 std::mem::take(&mut *guard)
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
156 fn default_state_has_one_tab() {
157 let state = ExtensionState::default();
158 let tabs = state.tabs.lock().unwrap();
159 assert_eq!(tabs.len(), 1);
160 assert!(tabs[0].active);
161 }
162
163 #[test]
164 fn default_state_has_session_cookie() {
165 let state = ExtensionState::default();
166 let cookies = state.cookies.lock().unwrap();
167 assert_eq!(cookies.len(), 1);
168 assert_eq!(cookies[0].name, "session");
169 }
170
171 #[test]
172 fn default_state_cookie_properties() {
173 let state = ExtensionState::default();
174 let cookies = state.cookies.lock().unwrap();
175 let cookie = &cookies[0];
176 assert_eq!(cookie.value, "abc123");
177 assert_eq!(cookie.domain, ".example.com");
178 assert_eq!(cookie.path, "/");
179 assert!(cookie.secure);
180 assert!(cookie.http_only);
181 }
182
183 #[test]
184 fn default_state_tab_properties() {
185 let state = ExtensionState::default();
186 let tabs = state.tabs.lock().unwrap();
187 let tab = &tabs[0];
188 assert_eq!(tab.id, 1);
189 assert_eq!(tab.url, "https://example.com");
190 assert_eq!(tab.title, "Example");
191 assert_eq!(tab.index, 0);
192 assert!(tab.active);
193 }
194
195 #[test]
196 fn default_state_storage_is_empty() {
197 let state = ExtensionState::default();
198 assert!(state.storage_local.lock().unwrap().is_empty());
199 assert!(state.storage_sync.lock().unwrap().is_empty());
200 }
201
202 #[test]
203 fn default_state_alarms_is_empty() {
204 let state = ExtensionState::default();
205 assert!(state.alarms.lock().unwrap().is_empty());
206 }
207
208 #[test]
209 fn default_state_pending_messages_is_empty() {
210 let state = ExtensionState::default();
211 assert!(state.pending_messages.lock().unwrap().is_empty());
212 }
213
214 #[test]
215 fn default_state_extension_id() {
216 let state = ExtensionState::default();
217 assert_eq!(state.extension_id, "test-extension-id");
218 }
219
220 #[test]
225 fn message_queue() {
226 let state = ExtensionState::default();
227 state.queue_message(PendingMessage {
228 data: Value::string("hello"),
229 sender_origin: Some("https://evil.com".into()),
230 is_external: true,
231 sender_tab_id: None,
232 });
233
234 let msgs = state.take_messages();
235 assert_eq!(msgs.len(), 1);
236 assert!(msgs[0].is_external);
237
238 assert!(state.take_messages().is_empty());
240 }
241
242 #[test]
243 fn message_queue_multiple_messages() {
244 let state = ExtensionState::default();
245
246 state.queue_message(PendingMessage {
247 data: Value::string("msg1"),
248 sender_origin: Some("https://a.com".into()),
249 is_external: true,
250 sender_tab_id: None,
251 });
252 state.queue_message(PendingMessage {
253 data: Value::string("msg2"),
254 sender_origin: Some("https://b.com".into()),
255 is_external: false,
256 sender_tab_id: Some(1),
257 });
258 state.queue_message(PendingMessage {
259 data: Value::json(r#"{"action": "test"}"#),
260 sender_origin: None,
261 is_external: false,
262 sender_tab_id: Some(2),
263 });
264
265 let msgs = state.take_messages();
266 assert_eq!(msgs.len(), 3);
267 assert!(msgs[0].is_external);
268 assert!(!msgs[1].is_external);
269 assert_eq!(msgs[1].sender_tab_id, Some(1));
270 }
271
272 #[test]
273 fn message_queue_take_drains_queue() {
274 let state = ExtensionState::default();
275
276 for i in 0..5 {
277 state.queue_message(PendingMessage {
278 data: Value::string(format!("msg{}", i)),
279 sender_origin: None,
280 is_external: false,
281 sender_tab_id: None,
282 });
283 }
284
285 let msgs1 = state.take_messages();
286 assert_eq!(msgs1.len(), 5);
287
288 let msgs2 = state.take_messages();
289 assert!(msgs2.is_empty());
290
291 let msgs3 = state.take_messages();
292 assert!(msgs3.is_empty());
293 }
294
295 #[test]
296 fn message_queue_order_preserved() {
297 let state = ExtensionState::default();
298
299 state.queue_message(PendingMessage {
300 data: Value::string("first"),
301 sender_origin: None,
302 is_external: false,
303 sender_tab_id: None,
304 });
305 state.queue_message(PendingMessage {
306 data: Value::string("second"),
307 sender_origin: None,
308 is_external: false,
309 sender_tab_id: None,
310 });
311 state.queue_message(PendingMessage {
312 data: Value::string("third"),
313 sender_origin: None,
314 is_external: false,
315 sender_tab_id: None,
316 });
317
318 let msgs = state.take_messages();
319 assert_eq!(msgs[0].data, Value::string("first"));
320 assert_eq!(msgs[1].data, Value::string("second"));
321 assert_eq!(msgs[2].data, Value::string("third"));
322 }
323
324 #[test]
325 fn message_queue_external_vs_internal() {
326 let state = ExtensionState::default();
327
328 state.queue_message(PendingMessage {
329 data: Value::Null,
330 sender_origin: Some("https://external.com".into()),
331 is_external: true,
332 sender_tab_id: None,
333 });
334 state.queue_message(PendingMessage {
335 data: Value::Null,
336 sender_origin: None,
337 is_external: false,
338 sender_tab_id: Some(1),
339 });
340
341 let msgs = state.take_messages();
342 assert!(msgs[0].is_external);
343 assert!(!msgs[1].is_external);
344 assert_eq!(msgs[0].sender_origin, Some("https://external.com".into()));
345 assert_eq!(msgs[1].sender_tab_id, Some(1));
346 }
347
348 #[test]
353 fn custom_state() {
354 let state = ExtensionState::default()
355 .with_tabs(vec![
356 Tab {
357 id: 1,
358 url: "https://a.com".into(),
359 title: "A".into(),
360 active: true,
361 index: 0,
362 },
363 Tab {
364 id: 2,
365 url: "https://b.com".into(),
366 title: "B".into(),
367 active: false,
368 index: 1,
369 },
370 ])
371 .with_storage(HashMap::from([("key".into(), "value".into())]));
372
373 assert_eq!(state.tabs.lock().unwrap().len(), 2);
374 assert_eq!(
375 state.storage_local.lock().unwrap().get("key").unwrap(),
376 "value"
377 );
378 }
379
380 #[test]
381 fn with_tabs_empty() {
382 let state = ExtensionState::default().with_tabs(vec![]);
383 assert!(state.tabs.lock().unwrap().is_empty());
384 }
385
386 #[test]
387 fn with_tabs_single() {
388 let state = ExtensionState::default().with_tabs(vec![Tab {
389 id: 42,
390 url: "https://test.com".into(),
391 title: "Test".into(),
392 active: true,
393 index: 0,
394 }]);
395 let tabs = state.tabs.lock().unwrap();
396 assert_eq!(tabs.len(), 1);
397 assert_eq!(tabs[0].id, 42);
398 }
399
400 #[test]
401 fn with_tabs_multiple() {
402 let tabs_vec: Vec<Tab> = (0..10)
403 .map(|i| Tab {
404 id: i,
405 url: format!("https://site{}.com", i),
406 title: format!("Site {}", i),
407 active: i == 0,
408 index: i as u32,
409 })
410 .collect();
411
412 let state = ExtensionState::default().with_tabs(tabs_vec);
413 assert_eq!(state.tabs.lock().unwrap().len(), 10);
414 }
415
416 #[test]
417 fn with_cookies_empty() {
418 let state = ExtensionState::default().with_cookies(vec![]);
419 assert!(state.cookies.lock().unwrap().is_empty());
420 }
421
422 #[test]
423 fn with_cookies_single() {
424 let state = ExtensionState::default().with_cookies(vec![Cookie {
425 name: "custom".into(),
426 value: "value".into(),
427 domain: ".custom.com".into(),
428 path: "/path".into(),
429 secure: false,
430 http_only: false,
431 }]);
432 let cookies = state.cookies.lock().unwrap();
433 assert_eq!(cookies.len(), 1);
434 assert_eq!(cookies[0].name, "custom");
435 }
436
437 #[test]
438 fn with_cookies_multiple() {
439 let cookies_vec: Vec<Cookie> = (0..10)
440 .map(|i| Cookie {
441 name: format!("cookie{}", i),
442 value: format!("value{}", i),
443 domain: format!(".site{}.com", i),
444 path: "/".into(),
445 secure: i % 2 == 0,
446 http_only: i % 2 == 1,
447 })
448 .collect();
449
450 let state = ExtensionState::default().with_cookies(cookies_vec);
451 assert_eq!(state.cookies.lock().unwrap().len(), 10);
452 }
453
454 #[test]
455 fn with_storage_empty() {
456 let state = ExtensionState::default().with_storage(HashMap::new());
457 assert!(state.storage_local.lock().unwrap().is_empty());
458 }
459
460 #[test]
461 fn with_storage_single() {
462 let mut map = HashMap::new();
463 map.insert("key1".into(), "value1".into());
464 let state = ExtensionState::default().with_storage(map);
465 assert_eq!(
466 state.storage_local.lock().unwrap().get("key1"),
467 Some(&"value1".into())
468 );
469 }
470
471 #[test]
472 fn with_storage_multiple() {
473 let map: HashMap<String, String> = (0..10)
474 .map(|i| (format!("key{}", i), format!("value{}", i)))
475 .collect();
476
477 let state = ExtensionState::default().with_storage(map);
478 let storage = state.storage_local.lock().unwrap();
479 assert_eq!(storage.len(), 10);
480 assert_eq!(storage.get("key5"), Some(&"value5".into()));
481 }
482
483 #[test]
484 fn with_storage_overwrites_default() {
485 let mut map = HashMap::new();
486 map.insert("custom".into(), "data".into());
487 let state = ExtensionState::default().with_storage(map);
488 let storage = state.storage_local.lock().unwrap();
489 assert!(storage.get("custom").is_some());
490 assert_eq!(storage.len(), 1); }
492
493 #[test]
494 fn chained_builders() {
495 let state = ExtensionState::default()
496 .with_tabs(vec![Tab {
497 id: 1,
498 url: "https://a.com".into(),
499 title: "A".into(),
500 active: true,
501 index: 0,
502 }])
503 .with_cookies(vec![Cookie {
504 name: "c".into(),
505 value: "v".into(),
506 domain: "d".into(),
507 path: "/".into(),
508 secure: true,
509 http_only: true,
510 }])
511 .with_storage(HashMap::from([("k".into(), "v".into())]));
512
513 assert_eq!(state.tabs.lock().unwrap().len(), 1);
514 assert_eq!(state.cookies.lock().unwrap().len(), 1);
515 assert_eq!(state.storage_local.lock().unwrap().len(), 1);
516 }
517
518 #[test]
523 fn empty_state_tabs() {
524 let state = ExtensionState::default().with_tabs(vec![]);
525 assert!(state.tabs.lock().unwrap().is_empty());
526 assert_eq!(state.tabs.lock().unwrap().len(), 0);
527 }
528
529 #[test]
530 fn empty_state_cookies() {
531 let state = ExtensionState::default().with_cookies(vec![]);
532 assert!(state.cookies.lock().unwrap().is_empty());
533 }
534
535 #[test]
536 fn empty_state_storage() {
537 let state = ExtensionState::default().with_storage(HashMap::new());
538 assert!(state.storage_local.lock().unwrap().is_empty());
539 }
540
541 #[test]
542 fn empty_state_all() {
543 let state = ExtensionState::default()
544 .with_tabs(vec![])
545 .with_cookies(vec![])
546 .with_storage(HashMap::new());
547
548 assert!(state.tabs.lock().unwrap().is_empty());
549 assert!(state.cookies.lock().unwrap().is_empty());
550 assert!(state.storage_local.lock().unwrap().is_empty());
551 assert!(state.storage_sync.lock().unwrap().is_empty());
552 assert!(state.alarms.lock().unwrap().is_empty());
553 assert!(state.pending_messages.lock().unwrap().is_empty());
554 }
555
556 #[test]
561 fn large_state_many_tabs() {
562 let tabs: Vec<Tab> = (0..100)
563 .map(|i| Tab {
564 id: i,
565 url: format!("https://site{}.com/page", i),
566 title: format!("Tab {} Title", i),
567 active: i == 0,
568 index: i as u32,
569 })
570 .collect();
571
572 let state = ExtensionState::default().with_tabs(tabs);
573 assert_eq!(state.tabs.lock().unwrap().len(), 100);
574 }
575
576 #[test]
577 fn large_state_many_cookies() {
578 let cookies: Vec<Cookie> = (0..1000)
579 .map(|i| Cookie {
580 name: format!("cookie_{}", i),
581 value: format!("value_{}_{}", i, "x".repeat(50)),
582 domain: format!(".domain{}.com", i % 100),
583 path: "/".into(),
584 secure: i % 3 == 0,
585 http_only: i % 5 == 0,
586 })
587 .collect();
588
589 let state = ExtensionState::default().with_cookies(cookies);
590 assert_eq!(state.cookies.lock().unwrap().len(), 1000);
591 }
592
593 #[test]
594 fn large_state_large_storage() {
595 let storage: HashMap<String, String> = (0..1000)
596 .map(|i| (format!("key_{}", i), format!("value_{}", i)))
597 .collect();
598
599 let state = ExtensionState::default().with_storage(storage);
600 assert_eq!(state.storage_local.lock().unwrap().len(), 1000);
601 }
602
603 #[test]
608 fn concurrent_access_tabs() {
609 let state = Arc::new(ExtensionState::default());
610 let state2 = state.clone();
611
612 state.tabs.lock().unwrap().push(Tab {
614 id: 999,
615 url: "https://new.com".into(),
616 title: "New".into(),
617 active: false,
618 index: 1,
619 });
620
621 assert_eq!(state2.tabs.lock().unwrap().len(), 2);
623 }
624
625 #[test]
626 fn concurrent_access_storage() {
627 let state = Arc::new(ExtensionState::default());
628 let state2 = state.clone();
629
630 state
631 .storage_local
632 .lock()
633 .unwrap()
634 .insert("key".into(), "value".into());
635
636 assert_eq!(
637 state2.storage_local.lock().unwrap().get("key"),
638 Some(&"value".into())
639 );
640 }
641
642 #[test]
643 fn concurrent_access_messages() {
644 let state = Arc::new(ExtensionState::default());
645 let state2 = state.clone();
646
647 state.queue_message(PendingMessage {
648 data: Value::string("test"),
649 sender_origin: None,
650 is_external: false,
651 sender_tab_id: None,
652 });
653
654 let msgs = state2.take_messages();
655 assert_eq!(msgs.len(), 1);
656 }
657
658 #[test]
659 fn state_clone_shares_data() {
660 let state1 = ExtensionState::default();
662 let state2 = state1.clone();
663
664 state1.tabs.lock().unwrap().clear();
666
667 assert!(state2.tabs.lock().unwrap().is_empty());
670 assert!(state1.tabs.lock().unwrap().is_empty());
671 }
672
673 #[test]
678 fn tab_struct_all_fields() {
679 let tab = Tab {
680 id: 123,
681 url: "https://example.com/path".into(),
682 title: "Example Title".into(),
683 active: true,
684 index: 5,
685 };
686 assert_eq!(tab.id, 123);
687 assert_eq!(tab.url, "https://example.com/path");
688 assert_eq!(tab.title, "Example Title");
689 assert!(tab.active);
690 assert_eq!(tab.index, 5);
691 }
692
693 #[test]
694 fn tab_inactive() {
695 let tab = Tab {
696 id: 1,
697 url: "https://example.com".into(),
698 title: "Example".into(),
699 active: false,
700 index: 0,
701 };
702 assert!(!tab.active);
703 }
704
705 #[test]
710 fn cookie_struct_all_fields() {
711 let cookie = Cookie {
712 name: "session_id".into(),
713 value: "abc123xyz".into(),
714 domain: ".example.com".into(),
715 path: "/api".into(),
716 secure: true,
717 http_only: true,
718 };
719 assert_eq!(cookie.name, "session_id");
720 assert_eq!(cookie.value, "abc123xyz");
721 assert_eq!(cookie.domain, ".example.com");
722 assert_eq!(cookie.path, "/api");
723 assert!(cookie.secure);
724 assert!(cookie.http_only);
725 }
726
727 #[test]
728 fn cookie_non_secure() {
729 let cookie = Cookie {
730 name: "test".into(),
731 value: "value".into(),
732 domain: "example.com".into(),
733 path: "/".into(),
734 secure: false,
735 http_only: false,
736 };
737 assert!(!cookie.secure);
738 assert!(!cookie.http_only);
739 }
740
741 #[test]
746 fn alarm_struct_all_fields() {
747 let alarm = Alarm {
748 name: "alarm1".into(),
749 scheduled_time: 1234567890.5,
750 period_in_minutes: Some(5.0),
751 };
752 assert_eq!(alarm.name, "alarm1");
753 assert_eq!(alarm.scheduled_time, 1234567890.5);
754 assert_eq!(alarm.period_in_minutes, Some(5.0));
755 }
756
757 #[test]
758 fn alarm_one_time() {
759 let alarm = Alarm {
760 name: "once".into(),
761 scheduled_time: 1234567890.0,
762 period_in_minutes: None,
763 };
764 assert!(alarm.period_in_minutes.is_none());
765 }
766
767 #[test]
772 fn pending_message_all_fields() {
773 let msg = PendingMessage {
774 data: Value::json(r#"{"action": "test"}"#),
775 sender_origin: Some("https://sender.com".into()),
776 is_external: true,
777 sender_tab_id: Some(42),
778 };
779 assert_eq!(msg.data, Value::json(r#"{"action": "test"}"#));
780 assert_eq!(msg.sender_origin, Some("https://sender.com".into()));
781 assert!(msg.is_external);
782 assert_eq!(msg.sender_tab_id, Some(42));
783 }
784
785 #[test]
786 fn pending_message_minimal() {
787 let msg = PendingMessage {
788 data: Value::Null,
789 sender_origin: None,
790 is_external: false,
791 sender_tab_id: None,
792 };
793 assert_eq!(msg.data, Value::Null);
794 assert!(msg.sender_origin.is_none());
795 assert!(!msg.is_external);
796 assert!(msg.sender_tab_id.is_none());
797 }
798}