use super::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cookie {
pub name: String,
pub value: String,
pub domain: String,
pub path: String,
#[serde(default)]
pub expires: f64,
#[serde(default)]
pub http_only: bool,
#[serde(default)]
pub secure: bool,
#[serde(default, rename = "sameSite")]
pub same_site: Option<String>,
#[serde(default, rename = "session")]
pub is_session: bool,
#[serde(default)]
pub size: Option<i64>,
#[serde(default, rename = "priority")]
pub priority: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageItems {
pub items: Vec<StorageEntry>,
#[serde(default)]
pub truncated: bool,
pub count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageEntry {
pub key: String,
pub value: String,
}
const STORAGE_VALUE_MAX_BYTES: usize = 1024;
const STORAGE_MAX_ENTRIES: usize = 64;
impl BrowserSession {
pub async fn cookies(&self) -> BrowserResult<Vec<Cookie>> {
self.policy.require(PolicyCapability::PersistentProfile)?;
self.cdp
.with_current_route(async {
let raw = self.cdp.get_cookies().await?;
let cookies: Vec<Cookie> = raw["cookies"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|c| serde_json::from_value(c.clone()).ok())
.collect()
})
.unwrap_or_default();
Ok(cookies)
})
.await
}
pub async fn set_cookies(&self, cookies: &[Cookie]) -> BrowserResult<()> {
self.policy.require(PolicyCapability::PersistentProfile)?;
if cookies.is_empty() {
return Ok(());
}
self.cdp
.with_current_route(async {
let value = serde_json::to_value(cookies)?;
self.cdp.set_cookies(value).await?;
Ok(())
})
.await
}
pub async fn clear_cookies(&self) -> BrowserResult<()> {
self.policy.require(PolicyCapability::PersistentProfile)?;
self.cdp
.with_current_route(async {
self.cdp.clear_browser_cookies().await?;
Ok(())
})
.await
}
pub async fn local_storage(&self) -> BrowserResult<StorageItems> {
self.read_dom_storage("localStorage").await
}
pub async fn session_storage(&self) -> BrowserResult<StorageItems> {
self.read_dom_storage("sessionStorage").await
}
async fn read_dom_storage(&self, storage_type: &str) -> BrowserResult<StorageItems> {
self.policy.require(PolicyCapability::PersistentProfile)?;
let expression = format!(
r#"JSON.stringify((function() {{
const store = window.{storage_type};
if (!store) return JSON.stringify({{items:[], count:0}});
const keys = Object.keys(store).slice(0, {STORAGE_MAX_ENTRIES});
const items = keys.map(k => {{
let v = store.getItem(k) || '';
if (v.length > {STORAGE_VALUE_MAX_BYTES}) {{
v = v.slice(0, {STORAGE_VALUE_MAX_BYTES}) + '…';
}}
return {{key: k, value: v}};
}});
return JSON.stringify({{
items: items,
truncated: Object.keys(store).length > {STORAGE_MAX_ENTRIES},
count: Object.keys(store).length
}});
}})())"#
);
self.cdp
.with_current_route(async {
let raw = self.cdp.evaluate(&expression).await?;
let value = runtime_value(&raw)?;
let json = value
.as_str()
.ok_or("DOM storage evaluation returned a non-string value")?;
Ok(serde_json::from_str(json)?)
})
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cookie_deserializes_from_cdp_json() {
let json = serde_json::json!({
"name": "session",
"value": "abc123",
"domain": ".example.com",
"path": "/",
"expires": 1735689600.0,
"http_only": true,
"secure": true,
"sameSite": "Lax",
"session": false,
"size": 64,
"priority": "Medium"
});
let cookie: Cookie = serde_json::from_value(json).unwrap();
assert_eq!(cookie.name, "session");
assert_eq!(cookie.value, "abc123");
assert_eq!(cookie.domain, ".example.com");
assert_eq!(cookie.path, "/");
assert!((cookie.expires - 1735689600.0).abs() < 1.0);
assert!(cookie.http_only);
assert!(cookie.secure);
assert_eq!(cookie.same_site, Some("Lax".to_string()));
assert!(!cookie.is_session);
assert_eq!(cookie.size, Some(64));
assert_eq!(cookie.priority, Some("Medium".to_string()));
}
#[test]
fn cookie_deserializes_with_minimal_fields() {
let json = serde_json::json!({
"name": "minimal",
"value": "val",
"domain": "example.com",
"path": "/"
});
let cookie: Cookie = serde_json::from_value(json).unwrap();
assert_eq!(cookie.name, "minimal");
assert_eq!(cookie.expires, 0.0);
assert!(!cookie.http_only);
assert!(!cookie.secure);
assert!(cookie.same_site.is_none());
}
#[test]
fn storage_items_deserializes_from_json() {
let json = serde_json::json!({
"items": [
{"key": "token", "value": "eyJhbGciOiJIUzI1NiJ9"},
{"key": "theme", "value": "dark"}
],
"truncated": false,
"count": 2
});
let items: StorageItems = serde_json::from_value(json).unwrap();
assert_eq!(items.count, 2);
assert!(!items.truncated);
assert_eq!(items.items.len(), 2);
assert_eq!(items.items[0].key, "token");
assert_eq!(items.items[1].value, "dark");
}
#[test]
fn storage_items_defaults_truncated_to_false() {
let json = serde_json::json!({
"items": [],
"count": 0
});
let items: StorageItems = serde_json::from_value(json).unwrap();
assert!(!items.truncated);
assert_eq!(items.count, 0);
assert!(items.items.is_empty());
}
#[test]
fn storage_entry_serializes_to_json() {
let entry = StorageEntry {
key: "session_token".to_string(),
value: "abc.def.ghi".to_string(),
};
let json = serde_json::to_value(&entry).unwrap();
assert_eq!(json["key"], "session_token");
assert_eq!(json["value"], "abc.def.ghi");
}
#[test]
fn storage_entry_roundtrip_through_json() {
let original = StorageEntry {
key: "theme".to_string(),
value: "dark".to_string(),
};
let json = serde_json::to_string(&original).unwrap();
let parsed: StorageEntry = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.key, "theme");
assert_eq!(parsed.value, "dark");
}
#[test]
fn storage_max_entries_is_64() {
assert_eq!(STORAGE_MAX_ENTRIES, 64);
}
#[test]
fn storage_max_entries_is_reasonable() {
const { assert!(STORAGE_MAX_ENTRIES >= 1) };
const { assert!(STORAGE_MAX_ENTRIES <= 256) };
}
#[test]
fn storage_max_entries_is_power_of_two() {
assert_eq!(STORAGE_MAX_ENTRIES.count_ones(), 1);
}
#[test]
fn storage_items_serializes_with_truncated_flag() {
let items = StorageItems {
items: vec![StorageEntry {
key: "k1".to_string(),
value: "v1".to_string(),
}],
truncated: true,
count: 100,
};
let json = serde_json::to_value(&items).unwrap();
assert_eq!(json["truncated"], true);
assert_eq!(json["count"], 100);
assert_eq!(json["items"].as_array().unwrap().len(), 1);
}
#[test]
fn cookie_roundtrip_through_json() {
let original = Cookie {
name: "sid".to_string(),
value: "secret".to_string(),
domain: "example.com".to_string(),
path: "/app".to_string(),
expires: 2000000000.0,
http_only: true,
secure: true,
same_site: Some("Strict".to_string()),
is_session: false,
size: Some(32),
priority: Some("High".to_string()),
};
let json = serde_json::to_string(&original).unwrap();
let parsed: Cookie = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name, "sid");
assert_eq!(parsed.value, "secret");
assert_eq!(parsed.same_site, Some("Strict".to_string()));
assert!(parsed.http_only);
assert_eq!(parsed.priority, Some("High".to_string()));
}
}