1use super::*;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Cookie {
13 pub name: String,
15 pub value: String,
17 pub domain: String,
19 pub path: String,
21 #[serde(default)]
23 pub expires: f64,
24 #[serde(default)]
26 pub http_only: bool,
27 #[serde(default)]
29 pub secure: bool,
30 #[serde(default, rename = "sameSite")]
32 pub same_site: Option<String>,
33 #[serde(default, rename = "session")]
35 pub is_session: bool,
36 #[serde(default)]
38 pub size: Option<i64>,
39 #[serde(default, rename = "priority")]
41 pub priority: Option<String>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct StorageItems {
47 pub items: Vec<StorageEntry>,
49 #[serde(default)]
51 pub truncated: bool,
52 pub count: usize,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct StorageEntry {
59 pub key: String,
61 pub value: String,
63}
64
65const STORAGE_VALUE_MAX_BYTES: usize = 1024;
67const STORAGE_MAX_ENTRIES: usize = 64;
69
70impl BrowserSession {
71 pub async fn cookies(&self) -> BrowserResult<Vec<Cookie>> {
76 self.policy.require(PolicyCapability::PersistentProfile)?;
77 self.cdp
78 .with_current_route(async {
79 let raw = self.cdp.get_cookies().await?;
80 let cookies: Vec<Cookie> = raw["cookies"]
81 .as_array()
82 .map(|arr| {
83 arr.iter()
84 .filter_map(|c| serde_json::from_value(c.clone()).ok())
85 .collect()
86 })
87 .unwrap_or_default();
88 Ok(cookies)
89 })
90 .await
91 }
92
93 pub async fn set_cookies(&self, cookies: &[Cookie]) -> BrowserResult<()> {
98 self.policy.require(PolicyCapability::PersistentProfile)?;
99 if cookies.is_empty() {
100 return Ok(());
101 }
102 self.cdp
103 .with_current_route(async {
104 let value = serde_json::to_value(cookies)?;
105 self.cdp.set_cookies(value).await?;
106 Ok(())
107 })
108 .await
109 }
110
111 pub async fn clear_cookies(&self) -> BrowserResult<()> {
115 self.policy.require(PolicyCapability::PersistentProfile)?;
116 self.cdp
117 .with_current_route(async {
118 self.cdp.clear_browser_cookies().await?;
119 Ok(())
120 })
121 .await
122 }
123
124 pub async fn local_storage(&self) -> BrowserResult<StorageItems> {
129 self.read_dom_storage("localStorage").await
130 }
131
132 pub async fn session_storage(&self) -> BrowserResult<StorageItems> {
137 self.read_dom_storage("sessionStorage").await
138 }
139
140 async fn read_dom_storage(&self, storage_type: &str) -> BrowserResult<StorageItems> {
141 self.policy.require(PolicyCapability::PersistentProfile)?;
142 let expression = format!(
143 r#"JSON.stringify((function() {{
144 const store = window.{storage_type};
145 if (!store) return JSON.stringify({{items:[], count:0}});
146 const keys = Object.keys(store).slice(0, {STORAGE_MAX_ENTRIES});
147 const items = keys.map(k => {{
148 let v = store.getItem(k) || '';
149 if (v.length > {STORAGE_VALUE_MAX_BYTES}) {{
150 v = v.slice(0, {STORAGE_VALUE_MAX_BYTES}) + '…';
151 }}
152 return {{key: k, value: v}};
153 }});
154 return JSON.stringify({{
155 items: items,
156 truncated: Object.keys(store).length > {STORAGE_MAX_ENTRIES},
157 count: Object.keys(store).length
158 }});
159 }})())"#
160 );
161 self.cdp
162 .with_current_route(async {
163 let raw = self.cdp.evaluate(&expression).await?;
164 let value = runtime_value(&raw)?;
165 let json = value
166 .as_str()
167 .ok_or("DOM storage evaluation returned a non-string value")?;
168 Ok(serde_json::from_str(json)?)
169 })
170 .await
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn cookie_deserializes_from_cdp_json() {
180 let json = serde_json::json!({
181 "name": "session",
182 "value": "abc123",
183 "domain": ".example.com",
184 "path": "/",
185 "expires": 1735689600.0,
186 "http_only": true,
187 "secure": true,
188 "sameSite": "Lax",
189 "session": false,
190 "size": 64,
191 "priority": "Medium"
192 });
193 let cookie: Cookie = serde_json::from_value(json).unwrap();
194 assert_eq!(cookie.name, "session");
195 assert_eq!(cookie.value, "abc123");
196 assert_eq!(cookie.domain, ".example.com");
197 assert_eq!(cookie.path, "/");
198 assert!((cookie.expires - 1735689600.0).abs() < 1.0);
199 assert!(cookie.http_only);
200 assert!(cookie.secure);
201 assert_eq!(cookie.same_site, Some("Lax".to_string()));
202 assert!(!cookie.is_session);
203 assert_eq!(cookie.size, Some(64));
204 assert_eq!(cookie.priority, Some("Medium".to_string()));
205 }
206
207 #[test]
208 fn cookie_deserializes_with_minimal_fields() {
209 let json = serde_json::json!({
210 "name": "minimal",
211 "value": "val",
212 "domain": "example.com",
213 "path": "/"
214 });
215 let cookie: Cookie = serde_json::from_value(json).unwrap();
216 assert_eq!(cookie.name, "minimal");
217 assert_eq!(cookie.expires, 0.0);
218 assert!(!cookie.http_only);
219 assert!(!cookie.secure);
220 assert!(cookie.same_site.is_none());
221 }
222
223 #[test]
224 fn storage_items_deserializes_from_json() {
225 let json = serde_json::json!({
226 "items": [
227 {"key": "token", "value": "eyJhbGciOiJIUzI1NiJ9"},
228 {"key": "theme", "value": "dark"}
229 ],
230 "truncated": false,
231 "count": 2
232 });
233 let items: StorageItems = serde_json::from_value(json).unwrap();
234 assert_eq!(items.count, 2);
235 assert!(!items.truncated);
236 assert_eq!(items.items.len(), 2);
237 assert_eq!(items.items[0].key, "token");
238 assert_eq!(items.items[1].value, "dark");
239 }
240
241 #[test]
242 fn storage_items_defaults_truncated_to_false() {
243 let json = serde_json::json!({
244 "items": [],
245 "count": 0
246 });
247 let items: StorageItems = serde_json::from_value(json).unwrap();
248 assert!(!items.truncated);
249 assert_eq!(items.count, 0);
250 assert!(items.items.is_empty());
251 }
252
253 #[test]
254 fn storage_entry_serializes_to_json() {
255 let entry = StorageEntry {
256 key: "session_token".to_string(),
257 value: "abc.def.ghi".to_string(),
258 };
259 let json = serde_json::to_value(&entry).unwrap();
260 assert_eq!(json["key"], "session_token");
261 assert_eq!(json["value"], "abc.def.ghi");
262 }
263
264 #[test]
265 fn storage_entry_roundtrip_through_json() {
266 let original = StorageEntry {
267 key: "theme".to_string(),
268 value: "dark".to_string(),
269 };
270 let json = serde_json::to_string(&original).unwrap();
271 let parsed: StorageEntry = serde_json::from_str(&json).unwrap();
272 assert_eq!(parsed.key, "theme");
273 assert_eq!(parsed.value, "dark");
274 }
275
276 #[test]
277 fn storage_max_entries_is_64() {
278 assert_eq!(STORAGE_MAX_ENTRIES, 64);
279 }
280
281 #[test]
282 fn storage_max_entries_is_reasonable() {
283 const { assert!(STORAGE_MAX_ENTRIES >= 1) };
285 const { assert!(STORAGE_MAX_ENTRIES <= 256) };
287 }
288
289 #[test]
290 fn storage_max_entries_is_power_of_two() {
291 assert_eq!(STORAGE_MAX_ENTRIES.count_ones(), 1);
292 }
293
294 #[test]
295 fn storage_items_serializes_with_truncated_flag() {
296 let items = StorageItems {
297 items: vec![StorageEntry {
298 key: "k1".to_string(),
299 value: "v1".to_string(),
300 }],
301 truncated: true,
302 count: 100,
303 };
304 let json = serde_json::to_value(&items).unwrap();
305 assert_eq!(json["truncated"], true);
306 assert_eq!(json["count"], 100);
307 assert_eq!(json["items"].as_array().unwrap().len(), 1);
308 }
309
310 #[test]
311 fn cookie_roundtrip_through_json() {
312 let original = Cookie {
313 name: "sid".to_string(),
314 value: "secret".to_string(),
315 domain: "example.com".to_string(),
316 path: "/app".to_string(),
317 expires: 2000000000.0,
318 http_only: true,
319 secure: true,
320 same_site: Some("Strict".to_string()),
321 is_session: false,
322 size: Some(32),
323 priority: Some("High".to_string()),
324 };
325 let json = serde_json::to_string(&original).unwrap();
326 let parsed: Cookie = serde_json::from_str(&json).unwrap();
327 assert_eq!(parsed.name, "sid");
328 assert_eq!(parsed.value, "secret");
329 assert_eq!(parsed.same_site, Some("Strict".to_string()));
330 assert!(parsed.http_only);
331 assert_eq!(parsed.priority, Some("High".to_string()));
332 }
333}