room_cli/oneshot/
token.rs1use std::path::{Path, PathBuf};
2
3use super::transport::join_session;
4
5pub fn token_file_path(room_id: &str, username: &str) -> PathBuf {
10 PathBuf::from(format!("/tmp/room-{room_id}-{username}.token"))
11}
12
13pub async fn cmd_join(room_id: &str, username: &str) -> anyhow::Result<()> {
19 let socket_path = PathBuf::from(format!("/tmp/room-{room_id}.sock"));
20 let (returned_user, token) = join_session(&socket_path, username).await?;
21 let token_data = serde_json::json!({"username": returned_user, "token": token});
22 let token_path = token_file_path(room_id, &returned_user);
23 std::fs::write(&token_path, format!("{token_data}\n"))?;
24 println!("{token_data}");
25 Ok(())
26}
27
28pub fn username_from_token(room_id: &str, token: &str) -> anyhow::Result<String> {
35 let prefix = format!("room-{room_id}-");
36 let suffix = ".token";
37 let files: Vec<PathBuf> = std::fs::read_dir("/tmp")
38 .map_err(|e| anyhow::anyhow!("cannot read /tmp: {e}"))?
39 .filter_map(|e| e.ok())
40 .map(|e| e.path())
41 .filter(|p| {
42 p.file_name()
43 .and_then(|n| n.to_str())
44 .map(|n| n.starts_with(&prefix) && n.ends_with(suffix))
45 .unwrap_or(false)
46 })
47 .collect();
48
49 for path in files {
50 if let Ok(data) = std::fs::read_to_string(&path) {
51 if let Ok(v) = serde_json::from_str::<serde_json::Value>(data.trim()) {
52 if v["token"].as_str() == Some(token) {
53 if let Some(u) = v["username"].as_str() {
54 return Ok(u.to_owned());
55 }
56 }
57 }
58 }
59 }
60
61 anyhow::bail!("token not recognised — run: room join {room_id} <username> to get a fresh token")
62}
63
64pub fn read_cursor(cursor_path: &Path) -> Option<String> {
66 std::fs::read_to_string(cursor_path)
67 .ok()
68 .map(|s| s.trim().to_owned())
69 .filter(|s| !s.is_empty())
70}
71
72pub fn write_cursor(cursor_path: &Path, id: &str) -> anyhow::Result<()> {
74 std::fs::write(cursor_path, id)?;
75 Ok(())
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use std::fs;
82 use tempfile::TempDir;
83
84 fn write_token_file(dir: &std::path::Path, room_id: &str, username: &str, token: &str) {
86 let name = format!("room-{room_id}-{username}.token");
87 let data = serde_json::json!({"username": username, "token": token});
88 fs::write(dir.join(name), format!("{data}\n")).unwrap();
89 }
90
91 fn username_from_token_in(
93 dir: &std::path::Path,
94 room_id: &str,
95 token: &str,
96 ) -> anyhow::Result<String> {
97 let prefix = format!("room-{room_id}-");
98 let suffix = ".token";
99 let files: Vec<PathBuf> = fs::read_dir(dir)
100 .unwrap()
101 .filter_map(|e| e.ok())
102 .map(|e| e.path())
103 .filter(|p| {
104 p.file_name()
105 .and_then(|n| n.to_str())
106 .map(|n| n.starts_with(&prefix) && n.ends_with(suffix))
107 .unwrap_or(false)
108 })
109 .collect();
110
111 for path in files {
112 if let Ok(data) = fs::read_to_string(&path) {
113 if let Ok(v) = serde_json::from_str::<serde_json::Value>(data.trim()) {
114 if v["token"].as_str() == Some(token) {
115 if let Some(u) = v["username"].as_str() {
116 return Ok(u.to_owned());
117 }
118 }
119 }
120 }
121 }
122 anyhow::bail!("token not recognised — run: room join {room_id} <username>")
123 }
124
125 #[test]
126 fn token_file_path_is_per_user() {
127 let alice = token_file_path("myroom", "alice");
128 let bob = token_file_path("myroom", "bob");
129 assert_ne!(alice, bob);
130 assert!(alice.to_str().unwrap().contains("alice"));
131 assert!(bob.to_str().unwrap().contains("bob"));
132 }
133
134 #[test]
135 fn username_from_token_finds_correct_user() {
136 let dir = TempDir::new().unwrap();
137 write_token_file(dir.path(), "r1", "alice", "tok-alice");
138 let user = username_from_token_in(dir.path(), "r1", "tok-alice").unwrap();
139 assert_eq!(user, "alice");
140 }
141
142 #[test]
143 fn username_from_token_disambiguates_multiple_users() {
144 let dir = TempDir::new().unwrap();
145 write_token_file(dir.path(), "r2", "alice", "tok-alice");
146 write_token_file(dir.path(), "r2", "bob", "tok-bob");
147
148 assert_eq!(
149 username_from_token_in(dir.path(), "r2", "tok-alice").unwrap(),
150 "alice"
151 );
152 assert_eq!(
153 username_from_token_in(dir.path(), "r2", "tok-bob").unwrap(),
154 "bob"
155 );
156 }
157
158 #[test]
159 fn username_from_token_unknown_errors_with_join_hint() {
160 let dir = TempDir::new().unwrap();
161 let err = username_from_token_in(dir.path(), "r3", "not-a-real-token").unwrap_err();
162 assert!(
163 err.to_string().contains("room join"),
164 "expected 'room join' hint in: {err}"
165 );
166 }
167
168 #[test]
169 fn two_agents_tokens_do_not_collide() {
170 let dir = TempDir::new().unwrap();
171 write_token_file(dir.path(), "r4", "alice", "tok-alice");
172 write_token_file(dir.path(), "r4", "bob", "tok-bob");
173
174 assert_eq!(
175 username_from_token_in(dir.path(), "r4", "tok-alice").unwrap(),
176 "alice"
177 );
178 assert_eq!(
179 username_from_token_in(dir.path(), "r4", "tok-bob").unwrap(),
180 "bob"
181 );
182 }
183
184 #[test]
185 fn read_cursor_returns_none_when_file_absent() {
186 let dir = TempDir::new().unwrap();
187 let path = dir.path().join("cursor");
188 assert!(read_cursor(&path).is_none());
189 }
190
191 #[test]
192 fn write_then_read_cursor_round_trips() {
193 let dir = TempDir::new().unwrap();
194 let path = dir.path().join("cursor");
195 write_cursor(&path, "abc-123").unwrap();
196 assert_eq!(read_cursor(&path).unwrap(), "abc-123");
197 }
198}