rustant_core/browser/
session.rs1use crate::browser::cdp::CdpClient;
4use crate::browser::security::BrowserSecurityGuard;
5use crate::browser::snapshot::{PageSnapshot, SnapshotMode};
6use crate::error::BrowserError;
7use std::sync::Arc;
8
9pub struct BrowserSession {
11 client: Arc<dyn CdpClient>,
13 security: Arc<BrowserSecurityGuard>,
15 max_pages: usize,
17 open_pages: usize,
19 active: bool,
21}
22
23impl BrowserSession {
24 pub fn new(
26 client: Arc<dyn CdpClient>,
27 security: Arc<BrowserSecurityGuard>,
28 max_pages: usize,
29 ) -> Self {
30 Self {
31 client,
32 security,
33 max_pages,
34 open_pages: 1, active: true,
36 }
37 }
38
39 pub fn client(&self) -> &Arc<dyn CdpClient> {
41 &self.client
42 }
43
44 pub fn security(&self) -> &Arc<BrowserSecurityGuard> {
46 &self.security
47 }
48
49 pub async fn navigate(&self, url: &str) -> Result<(), BrowserError> {
51 if !self.active {
52 return Err(BrowserError::SessionError {
53 message: "Session is closed".to_string(),
54 });
55 }
56 self.security
57 .check_url(url)
58 .map_err(|msg| BrowserError::UrlBlocked { url: msg })?;
59 self.client.navigate(url).await
60 }
61
62 pub async fn snapshot(&self, mode: SnapshotMode) -> Result<PageSnapshot, BrowserError> {
64 if !self.active {
65 return Err(BrowserError::SessionError {
66 message: "Session is closed".to_string(),
67 });
68 }
69
70 let url = self.client.get_url().await?;
71 let title = self.client.get_title().await?;
72
73 let content = match &mode {
74 SnapshotMode::Html => self.client.get_html().await?,
75 SnapshotMode::Text => self.client.get_text().await?,
76 SnapshotMode::AriaTree => self.client.get_aria_tree().await?,
77 SnapshotMode::Screenshot => {
78 let bytes = self.client.screenshot().await?;
79 base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes)
80 }
81 };
82
83 let content = if mode != SnapshotMode::Screenshot {
85 BrowserSecurityGuard::mask_credentials(&content)
86 } else {
87 content
88 };
89
90 Ok(PageSnapshot::new(url, title, mode, content))
91 }
92
93 pub fn can_open_page(&self) -> bool {
95 self.open_pages < self.max_pages
96 }
97
98 pub fn open_page(&mut self) -> Result<(), BrowserError> {
100 if self.open_pages >= self.max_pages {
101 return Err(BrowserError::PageLimitExceeded {
102 max: self.max_pages,
103 });
104 }
105 self.open_pages += 1;
106 Ok(())
107 }
108
109 pub fn close_page(&mut self) {
111 if self.open_pages > 0 {
112 self.open_pages -= 1;
113 }
114 }
115
116 pub async fn close(&mut self) -> Result<(), BrowserError> {
118 if self.active {
119 self.active = false;
120 self.client.close().await?;
121 }
122 Ok(())
123 }
124
125 pub fn is_active(&self) -> bool {
127 self.active
128 }
129
130 pub fn open_page_count(&self) -> usize {
132 self.open_pages
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::browser::cdp::MockCdpClient;
140
141 fn make_session(max_pages: usize) -> (BrowserSession, Arc<MockCdpClient>) {
142 let client = Arc::new(MockCdpClient::new());
143 let security = Arc::new(BrowserSecurityGuard::default());
144 let session = BrowserSession::new(client.clone(), security, max_pages);
145 (session, client)
146 }
147
148 fn make_session_with_security(
149 security: BrowserSecurityGuard,
150 ) -> (BrowserSession, Arc<MockCdpClient>) {
151 let client = Arc::new(MockCdpClient::new());
152 let security = Arc::new(security);
153 let session = BrowserSession::new(client.clone(), security, 5);
154 (session, client)
155 }
156
157 #[tokio::test]
158 async fn test_session_navigate() {
159 let (session, client) = make_session(5);
160 session.navigate("https://example.com").await.unwrap();
161 assert_eq!(*client.current_url.lock().unwrap(), "https://example.com");
162 }
163
164 #[tokio::test]
165 async fn test_session_navigate_blocked_url() {
166 let security = BrowserSecurityGuard::new(vec![], vec!["evil.com".to_string()]);
167 let (session, _client) = make_session_with_security(security);
168 let result = session.navigate("https://evil.com").await;
169 assert!(result.is_err());
170 }
171
172 #[tokio::test]
173 async fn test_session_snapshot_html() {
174 let (session, client) = make_session(5);
175 client.set_url("https://example.com");
176 client.set_title("Example");
177 client.set_html("<html><body>Hello</body></html>");
178 let snap = session.snapshot(SnapshotMode::Html).await.unwrap();
179 assert_eq!(snap.url, "https://example.com");
180 assert_eq!(snap.title, "Example");
181 assert_eq!(snap.mode, SnapshotMode::Html);
182 assert!(snap.content.contains("Hello"));
183 }
184
185 #[tokio::test]
186 async fn test_session_snapshot_text() {
187 let (session, client) = make_session(5);
188 client.set_url("https://docs.rs");
189 client.set_title("Docs");
190 client.set_text("Welcome to docs.rs");
191 let snap = session.snapshot(SnapshotMode::Text).await.unwrap();
192 assert_eq!(snap.mode, SnapshotMode::Text);
193 assert_eq!(snap.content, "Welcome to docs.rs");
194 }
195
196 #[tokio::test]
197 async fn test_session_snapshot_aria_tree() {
198 let (session, client) = make_session(5);
199 client.set_url("https://example.com");
200 client.set_title("Example");
201 client.set_aria_tree("document\n heading 'Title'\n button 'Submit'");
202 let snap = session.snapshot(SnapshotMode::AriaTree).await.unwrap();
203 assert_eq!(snap.mode, SnapshotMode::AriaTree);
204 assert!(snap.content.contains("heading"));
205 }
206
207 #[tokio::test]
208 async fn test_session_snapshot_screenshot() {
209 let (session, client) = make_session(5);
210 client.set_url("https://example.com");
211 client.set_title("Example");
212 client.set_screenshot(vec![1, 2, 3, 4, 5]);
213 let snap = session.snapshot(SnapshotMode::Screenshot).await.unwrap();
214 assert_eq!(snap.mode, SnapshotMode::Screenshot);
215 assert!(!snap.content.is_empty());
217 }
218
219 #[tokio::test]
220 async fn test_session_page_limit() {
221 let (mut session, _client) = make_session(2);
222 assert_eq!(session.open_page_count(), 1);
224 assert!(session.can_open_page());
225 session.open_page().unwrap();
226 assert_eq!(session.open_page_count(), 2);
227 assert!(!session.can_open_page());
228 let result = session.open_page();
229 assert!(result.is_err());
230 }
231
232 #[tokio::test]
233 async fn test_session_close_page() {
234 let (mut session, _client) = make_session(3);
235 session.open_page().unwrap();
236 assert_eq!(session.open_page_count(), 2);
237 session.close_page();
238 assert_eq!(session.open_page_count(), 1);
239 }
240
241 #[tokio::test]
242 async fn test_session_close() {
243 let (mut session, client) = make_session(5);
244 assert!(session.is_active());
245 session.close().await.unwrap();
246 assert!(!session.is_active());
247 assert!(*client.closed.lock().unwrap());
248 }
249
250 #[tokio::test]
251 async fn test_session_navigate_after_close() {
252 let (mut session, _client) = make_session(5);
253 session.close().await.unwrap();
254 let result = session.navigate("https://example.com").await;
255 assert!(result.is_err());
256 }
257
258 #[tokio::test]
259 async fn test_session_snapshot_after_close() {
260 let (mut session, _client) = make_session(5);
261 session.close().await.unwrap();
262 let result = session.snapshot(SnapshotMode::Html).await;
263 assert!(result.is_err());
264 }
265
266 #[tokio::test]
267 async fn test_session_security_guard_gates_navigate() {
268 let security = BrowserSecurityGuard::new(vec!["allowed.com".to_string()], vec![]);
269 let (session, _client) = make_session_with_security(security);
270 assert!(session.navigate("https://allowed.com/page").await.is_ok());
272 assert!(session.navigate("https://other.com").await.is_err());
274 }
275
276 #[tokio::test]
277 async fn test_session_double_close_is_safe() {
278 let (mut session, _client) = make_session(5);
279 session.close().await.unwrap();
280 session.close().await.unwrap();
282 assert!(!session.is_active());
283 }
284}