tuitbot_core/x_api/local_mode/
session.rs1use std::path::Path;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ScraperSession {
14 pub auth_token: String,
16 pub ct0: String,
18 #[serde(default)]
20 pub username: Option<String>,
21 #[serde(default)]
23 pub created_at: Option<String>,
24}
25
26impl ScraperSession {
27 pub fn load(path: &Path) -> Result<Option<Self>, std::io::Error> {
29 if !path.exists() {
30 return Ok(None);
31 }
32 let contents = std::fs::read_to_string(path)?;
33 let session: Self = serde_json::from_str(&contents)
34 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
35 if session.auth_token.is_empty() || session.ct0.is_empty() {
36 return Ok(None);
37 }
38 Ok(Some(session))
39 }
40
41 pub fn save(&self, path: &Path) -> Result<(), std::io::Error> {
43 let json = serde_json::to_string_pretty(self)
44 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
45 std::fs::write(path, &json)?;
46
47 #[cfg(unix)]
49 {
50 use std::os::unix::fs::PermissionsExt;
51 let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
52 }
53
54 Ok(())
55 }
56
57 pub fn delete(path: &Path) -> Result<bool, std::io::Error> {
59 match std::fs::remove_file(path) {
60 Ok(()) => Ok(true),
61 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
62 Err(e) => Err(e),
63 }
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use tempfile::TempDir;
71
72 #[test]
73 fn load_returns_none_when_missing() {
74 let dir = TempDir::new().unwrap();
75 let path = dir.path().join("session.json");
76 assert!(ScraperSession::load(&path).unwrap().is_none());
77 }
78
79 #[test]
80 fn save_and_load_roundtrip() {
81 let dir = TempDir::new().unwrap();
82 let path = dir.path().join("session.json");
83 let session = ScraperSession {
84 auth_token: "abc123".to_string(),
85 ct0: "csrf456".to_string(),
86 username: Some("testuser".to_string()),
87 created_at: Some("2026-03-05T12:00:00Z".to_string()),
88 };
89 session.save(&path).unwrap();
90 let loaded = ScraperSession::load(&path).unwrap().unwrap();
91 assert_eq!(loaded.auth_token, "abc123");
92 assert_eq!(loaded.ct0, "csrf456");
93 assert_eq!(loaded.username.as_deref(), Some("testuser"));
94 }
95
96 #[test]
97 fn load_returns_none_for_empty_tokens() {
98 let dir = TempDir::new().unwrap();
99 let path = dir.path().join("session.json");
100 let session = ScraperSession {
101 auth_token: String::new(),
102 ct0: "csrf".to_string(),
103 username: None,
104 created_at: None,
105 };
106 session.save(&path).unwrap();
107 assert!(ScraperSession::load(&path).unwrap().is_none());
108 }
109
110 #[test]
111 fn delete_returns_false_when_missing() {
112 let dir = TempDir::new().unwrap();
113 let path = dir.path().join("session.json");
114 assert!(!ScraperSession::delete(&path).unwrap());
115 }
116
117 #[test]
118 fn delete_removes_existing_file() {
119 let dir = TempDir::new().unwrap();
120 let path = dir.path().join("session.json");
121 std::fs::write(&path, "{}").unwrap();
122 assert!(ScraperSession::delete(&path).unwrap());
123 assert!(!path.exists());
124 }
125}