anvilsign in

collin/browser-terminal-extension

main / daemon / tests / activation.rs
1//! Serving a socket we did not bind, and going away when nobody is using it.
2//!
3//! Together these are what make the daemon start itself: systemd owns the port
4//! and execs us on the first connection, and we exit again once the sidebar is
5//! gone. Both halves have to hold or the user is back to starting it by hand —
6//! or, worse, ends up with a daemon that exits while a terminal is open.
7
8use std::time::Duration;
9
10use futures_util::{SinkExt, StreamExt};
11use tokio_tungstenite::connect_async;
12use tokio_tungstenite::tungstenite::Message;
13use tokio_tungstenite::tungstenite::client::IntoClientRequest;
14
15use termbridge::{Config, Event, Server};
16
17const GOOD_TOKEN: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
18const PAIRED: &str = "chrome-extension://abcdefghijklmnopabcdefghijklmnop";
19
20fn test_config() -> Config {
21 let mut c = Config::new(GOOD_TOKEN, vec![PAIRED.to_string()]);
22 c.auth_timeout = Duration::from_millis(300);
23 c.echo_only = true;
24 c
25}
26
27async fn authed(
28 server: &Server,
29) -> tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>> {
30 let mut req = server.url().into_client_request().unwrap();
31 req.headers_mut().insert("origin", PAIRED.parse().unwrap());
32 let (mut ws, _) = connect_async(req).await.expect("connect");
33 ws.send(Message::Text(
34 format!(r#"{{"type":"auth","token":"{GOOD_TOKEN}"}}"#).into(),
35 ))
36 .await
37 .unwrap();
38 let reply = ws.next().await.unwrap().unwrap();
39 assert!(reply.to_text().unwrap().contains("ok"), "auth: {reply:?}");
40 ws
41}
42
43/// A socket bound by somebody else is served exactly like one we bound. This is
44/// the systemd handoff minus the environment variables, which `activation`'s
45/// own tests cover.
46#[tokio::test]
47async fn serves_a_socket_it_did_not_bind() {
48 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
49 let addr = listener.local_addr().unwrap();
50 listener.set_nonblocking(true).unwrap();
51
52 let server = Server::from_std(test_config(), listener).expect("adopt listener");
53 assert_eq!(server.addr(), addr, "kept the port systemd chose");
54
55 let mut ws = authed(&server).await;
56 ws.send(Message::Binary(b"hi".to_vec().into()))
57 .await
58 .unwrap();
59 assert_eq!(
60 ws.next().await.unwrap().unwrap(),
61 Message::Binary(b"hi".to_vec().into())
62 );
63}
64
65/// The whole point of the idle timeout: a daemon nobody is talking to should
66/// stop, so the next connection gets a fresh one from the socket unit.
67#[tokio::test]
68async fn idle_daemon_asks_to_exit() {
69 let mut config = test_config();
70 config.idle_timeout = Some(Duration::from_millis(200));
71 let mut server = Server::start(config, 0).await.unwrap();
72
73 let event = tokio::time::timeout(Duration::from_secs(5), server.next_event())
74 .await
75 .expect("idle event within 5s");
76 assert_eq!(event, Some(Event::Idle));
77}
78
79/// And the failure that would actually hurt: killing the daemon out from under
80/// an open terminal. A connected client must hold it open indefinitely, however
81/// quiet the user is being.
82#[tokio::test]
83async fn a_connected_client_is_not_idle() {
84 let mut config = test_config();
85 config.idle_timeout = Some(Duration::from_millis(200));
86 let mut server = Server::start(config, 0).await.unwrap();
87
88 let _ws = authed(&server).await;
89 // Accepted, then nothing. Well past several timeouts.
90 assert_eq!(
91 server.next_event().await,
92 Some(Event::Accepted {
93 origin: PAIRED.into()
94 })
95 );
96 let quiet = tokio::time::timeout(Duration::from_millis(1500), server.next_event()).await;
97 assert!(quiet.is_err(), "exited under an open client: {quiet:?}");
98}
99
100/// The clock has to measure time since the last client *left*, not since
101/// startup — otherwise the first disconnect after a long session triggers an
102/// immediate exit, and the sidebar's reconnect races the shutdown.
103#[tokio::test]
104async fn the_clock_restarts_when_a_client_leaves() {
105 let mut config = test_config();
106 config.idle_timeout = Some(Duration::from_millis(600));
107 let mut server = Server::start(config, 0).await.unwrap();
108
109 let ws = authed(&server).await;
110 assert_eq!(
111 server.next_event().await,
112 Some(Event::Accepted {
113 origin: PAIRED.into()
114 })
115 );
116 tokio::time::sleep(Duration::from_millis(700)).await;
117 drop(ws);
118
119 let event = tokio::time::timeout(Duration::from_secs(5), server.next_event())
120 .await
121 .expect("idle event after the client left");
122 assert_eq!(event, Some(Event::Idle));
123}
124
125/// Left off by default. A hand-started `termbridge serve` that vanished a few
126/// minutes after the user closed one tab would be a surprising regression.
127#[tokio::test]
128async fn no_timeout_by_default() {
129 assert!(Config::new(GOOD_TOKEN, vec![]).idle_timeout.is_none());
130}