| 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 | |
| 8 | use std::time::Duration; |
| 9 | |
| 10 | use futures_util::{SinkExt, StreamExt}; |
| 11 | use tokio_tungstenite::connect_async; |
| 12 | use tokio_tungstenite::tungstenite::Message; |
| 13 | use tokio_tungstenite::tungstenite::client::IntoClientRequest; |
| 14 | |
| 15 | use termbridge::{Config, Event, Server}; |
| 16 | |
| 17 | const GOOD_TOKEN: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; |
| 18 | const PAIRED: &str = "chrome-extension://abcdefghijklmnopabcdefghijklmnop"; |
| 19 | |
| 20 | fn 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 | |
| 27 | async 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] |
| 47 | async 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] |
| 68 | async 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] |
| 83 | async 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] |
| 104 | async 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] |
| 128 | async fn no_timeout_by_default() { |
| 129 | assert!(Config::new(GOOD_TOKEN, vec![]).idle_timeout.is_none()); |
| 130 | } |