| 1 | //! Tests for `/v1/computer/*` (ARCHITECTURE §6 S2 acceptance). |
| 2 | //! |
| 3 | //! The fake Xvnc below speaks the server side of RFB 3.8 on a Unix socket |
| 4 | //! and records every client byte it receives after `ClientInit`. "Watcher |
| 5 | //! input produces 0 X events" is asserted as "Xvnc received 0 input bytes": |
| 6 | //! input that never reaches the X server cannot become an X event. |
| 7 | |
| 8 | use super::*; |
| 9 | |
| 10 | #[test] |
| 11 | fn parser_forwards_allowed_messages_split_across_frames() { |
| 12 | let mut parser = ClientParser::default(); |
| 13 | let mut out = Vec::new(); |
| 14 | // FramebufferUpdateRequest (10 bytes) split 3 + 7. |
| 15 | let fur = [3u8, 1, 0, 0, 0, 0, 5, 160, 3, 132]; |
| 16 | parser.feed(&fur[..3], false, &mut out).unwrap(); |
| 17 | assert!(out.is_empty()); |
| 18 | parser.feed(&fur[3..], false, &mut out).unwrap(); |
| 19 | assert_eq!(out, fur); |
| 20 | } |
| 21 | |
| 22 | #[test] |
| 23 | fn parser_drops_input_without_lease_and_forwards_with_it() { |
| 24 | let key = [4u8, 1, 0, 0, 0, 0, 0, 0x61]; |
| 25 | let pointer = [5u8, 1, 0, 10, 0, 20]; |
| 26 | let cut = [6u8, 0, 0, 0, 0, 0, 0, 2, b'h', b'i']; |
| 27 | let mut resize = vec![251u8, 0, 5, 0, 3, 32, 1, 0]; |
| 28 | resize.extend_from_slice(&[0u8; 16]); |
| 29 | let mut all = Vec::new(); |
| 30 | for m in [&key[..], &pointer[..], &cut[..], &resize[..]] { |
| 31 | all.extend_from_slice(m); |
| 32 | } |
| 33 | |
| 34 | let mut out = Vec::new(); |
| 35 | let stats = ClientParser::default().feed(&all, false, &mut out).unwrap(); |
| 36 | assert!(out.is_empty(), "watcher input must not be forwarded"); |
| 37 | assert_eq!(stats.input_dropped, 4); |
| 38 | |
| 39 | let mut out = Vec::new(); |
| 40 | let stats = ClientParser::default().feed(&all, true, &mut out).unwrap(); |
| 41 | assert_eq!(out, all); |
| 42 | assert_eq!(stats.input_forwarded, 4); |
| 43 | } |
| 44 | |
| 45 | #[test] |
| 46 | fn parser_closes_on_unknown_type_and_oversize() { |
| 47 | let mut out = Vec::new(); |
| 48 | assert_eq!( |
| 49 | ClientParser::default().feed(&[248, 0, 0, 0], true, &mut out), |
| 50 | Err(ParseError::UnknownType(248)) |
| 51 | ); |
| 52 | // A negative (extended-clipboard) or huge cut length is refused. |
| 53 | assert!(matches!( |
| 54 | ClientParser::default().feed(&[6, 0, 0, 0, 0xff, 0xff, 0xff, 0xfc], true, &mut out), |
| 55 | Err(ParseError::TooLarge { |
| 56 | message_type: 6, |
| 57 | .. |
| 58 | }) |
| 59 | )); |
| 60 | // Unknown type after a valid message still closes. |
| 61 | let mut parser = ClientParser::default(); |
| 62 | let mut bytes = vec![3u8, 1, 0, 0, 0, 0, 0, 1, 0, 1]; |
| 63 | bytes.push(0xff); |
| 64 | assert_eq!( |
| 65 | parser.feed(&bytes, true, &mut out), |
| 66 | Err(ParseError::UnknownType(0xff)) |
| 67 | ); |
| 68 | } |
| 69 | |
| 70 | #[test] |
| 71 | fn parser_strips_encodings_that_start_unparsed_subprotocols() { |
| 72 | let encodings: [i32; 5] = [16, -312, 7, -258, -239]; |
| 73 | let mut msg = vec![2u8, 0]; |
| 74 | msg.extend_from_slice(&(encodings.len() as u16).to_be_bytes()); |
| 75 | for e in encodings { |
| 76 | msg.extend_from_slice(&e.to_be_bytes()); |
| 77 | } |
| 78 | let mut out = Vec::new(); |
| 79 | ClientParser::default().feed(&msg, false, &mut out).unwrap(); |
| 80 | let mut expected = vec![2u8, 0, 0, 3]; |
| 81 | for e in [16i32, 7, -239] { |
| 82 | expected.extend_from_slice(&e.to_be_bytes()); |
| 83 | } |
| 84 | assert_eq!(out, expected); |
| 85 | } |
| 86 | |
| 87 | #[test] |
| 88 | fn display_socket_path_must_be_absolute_and_plain() { |
| 89 | assert_eq!( |
| 90 | validated_socket_path(" /run/cw/vnc.sock "), |
| 91 | Some(PathBuf::from("/run/cw/vnc.sock")) |
| 92 | ); |
| 93 | assert_eq!(validated_socket_path(""), None); |
| 94 | assert_eq!(validated_socket_path("vnc.sock"), None); |
| 95 | assert_eq!(validated_socket_path("./vnc.sock"), None); |
| 96 | assert_eq!(validated_socket_path("/run/cw/../../etc/passwd"), None); |
| 97 | assert_eq!(validated_socket_path("/run/./cw/vnc.sock"), None); |
| 98 | assert_eq!(validated_socket_path("/run/cw/vnc\0.sock"), None); |
| 99 | assert_eq!(validated_socket_path("/"), None); |
| 100 | } |
| 101 | |
| 102 | #[test] |
| 103 | fn redaction_hides_ticket_and_token_values() { |
| 104 | let redacted = redact_query_secrets( |
| 105 | "/v1/computer/display?ticket=cwdt_secret&mode=view&mobile_stream_ticket=abc&Token=x#frag", |
| 106 | ); |
| 107 | assert_eq!( |
| 108 | redacted, |
| 109 | "/v1/computer/display?ticket=redacted&mode=view&mobile_stream_ticket=redacted&Token=redacted#redacted" |
| 110 | ); |
| 111 | assert!(!redacted.contains("cwdt_secret")); |
| 112 | assert_eq!(redact_query_secrets("/v1/computer"), "/v1/computer"); |
| 113 | } |
| 114 | |
| 115 | #[tokio::test] |
| 116 | async fn parser_panic_is_contained_to_its_task() { |
| 117 | // Stand-in for an active turn running on the same runtime. |
| 118 | let turn = tokio::spawn(async { |
| 119 | tokio::time::sleep(Duration::from_millis(50)).await; |
| 120 | "turn finished" |
| 121 | }); |
| 122 | let parser = tokio::spawn(async { |
| 123 | if std::hint::black_box(true) { |
| 124 | panic!("parser bug"); |
| 125 | } |
| 126 | ExitReason::ClientClosed |
| 127 | }); |
| 128 | assert_eq!(parser_exit(parser.await), ExitReason::ParserPanic); |
| 129 | assert_eq!(turn.await.unwrap(), "turn finished"); |
| 130 | } |
| 131 | |
| 132 | #[cfg(unix)] |
| 133 | mod live { |
| 134 | use super::*; |
| 135 | use tokio::net::UnixListener; |
| 136 | use tokio_tungstenite::tungstenite::client::IntoClientRequest; |
| 137 | use tokio_tungstenite::tungstenite::protocol::Message as TMessage; |
| 138 | |
| 139 | const MASTER: &str = "master-token-for-tests"; |
| 140 | |
| 141 | struct Harness { |
| 142 | base: String, |
| 143 | ws_base: String, |
| 144 | computer: ComputerState, |
| 145 | received: Arc<parking_lot::Mutex<Vec<u8>>>, |
| 146 | _dir: tempfile::TempDir, |
| 147 | } |
| 148 | |
| 149 | async fn fake_xvnc(listener: UnixListener, received: Arc<parking_lot::Mutex<Vec<u8>>>) { |
| 150 | loop { |
| 151 | let Ok((mut s, _)) = listener.accept().await else { |
| 152 | return; |
| 153 | }; |
| 154 | let received = received.clone(); |
| 155 | tokio::spawn(async move { |
| 156 | s.write_all(RFB_VERSION_38).await.unwrap(); |
| 157 | let mut v = [0u8; 12]; |
| 158 | s.read_exact(&mut v).await.unwrap(); |
| 159 | s.write_all(&[1, 1]).await.unwrap(); |
| 160 | let mut one = [0u8; 1]; |
| 161 | s.read_exact(&mut one).await.unwrap(); |
| 162 | s.write_all(&[0, 0, 0, 0]).await.unwrap(); |
| 163 | s.read_exact(&mut one).await.unwrap(); // ClientInit |
| 164 | let mut init = Vec::new(); |
| 165 | init.extend_from_slice(&1440u16.to_be_bytes()); |
| 166 | init.extend_from_slice(&900u16.to_be_bytes()); |
| 167 | init.extend_from_slice(&[32, 24, 0, 1, 0, 255, 0, 255, 0, 255, 16, 8, 0, 0, 0, 0]); |
| 168 | init.extend_from_slice(&4u32.to_be_bytes()); |
| 169 | init.extend_from_slice(b"twin"); |
| 170 | s.write_all(&init).await.unwrap(); |
| 171 | let mut buf = [0u8; 4096]; |
| 172 | loop { |
| 173 | match s.read(&mut buf).await { |
| 174 | Ok(0) | Err(_) => return, |
| 175 | Ok(n) => received.lock().extend_from_slice(&buf[..n]), |
| 176 | } |
| 177 | } |
| 178 | }); |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | async fn harness() -> Harness { |
| 183 | // The workspace builds reqwest with `rustls-no-provider`; the binary |
| 184 | // installs ring at startup, so tests that build a Client must too. |
| 185 | let _ = rustls::crypto::ring::default_provider().install_default(); |
| 186 | let dir = tempfile::tempdir().unwrap(); |
| 187 | let sock = dir.path().join("vnc.sock"); |
| 188 | let listener = UnixListener::bind(&sock).unwrap(); |
| 189 | let received = Arc::new(parking_lot::Mutex::new(Vec::new())); |
| 190 | tokio::spawn(fake_xvnc(listener, received.clone())); |
| 191 | let computer = ComputerState::new(sock, DEFAULT_IDLE); |
| 192 | let app: Router = router(computer.clone(), Some(MASTER.to_string())); |
| 193 | let tcp = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); |
| 194 | let addr = tcp.local_addr().unwrap(); |
| 195 | tokio::spawn(async move { axum::serve(tcp, app).await.unwrap() }); |
| 196 | Harness { |
| 197 | base: format!("http://{addr}"), |
| 198 | ws_base: format!("ws://{addr}"), |
| 199 | computer, |
| 200 | received, |
| 201 | _dir: dir, |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | type Ws = tokio_tungstenite::WebSocketStream< |
| 206 | tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>, |
| 207 | >; |
| 208 | |
| 209 | async fn connect(h: &Harness, bearer: Option<&str>, query: &str) -> Result<Ws, u16> { |
| 210 | let mut req = format!("{}/v1/computer/display{query}", h.ws_base) |
| 211 | .into_client_request() |
| 212 | .unwrap(); |
| 213 | if let Some(token) = bearer { |
| 214 | req.headers_mut() |
| 215 | .insert("authorization", format!("Bearer {token}").parse().unwrap()); |
| 216 | } |
| 217 | match tokio_tungstenite::connect_async(req).await { |
| 218 | Ok((ws, _)) => Ok(ws), |
| 219 | Err(tokio_tungstenite::tungstenite::Error::Http(resp)) => Err(resp.status().as_u16()), |
| 220 | Err(other) => panic!("unexpected connect error: {other}"), |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | struct RfbClient { |
| 225 | ws: Ws, |
| 226 | buf: Vec<u8>, |
| 227 | } |
| 228 | |
| 229 | impl RfbClient { |
| 230 | async fn read(&mut self, n: usize) -> Vec<u8> { |
| 231 | while self.buf.len() < n { |
| 232 | match tokio::time::timeout(Duration::from_secs(5), self.ws.next()) |
| 233 | .await |
| 234 | .expect("frame within 5 s") |
| 235 | { |
| 236 | Some(Ok(TMessage::Binary(b))) => self.buf.extend_from_slice(&b), |
| 237 | other => panic!("expected binary frame, got {other:?}"), |
| 238 | } |
| 239 | } |
| 240 | let rest = self.buf.split_off(n); |
| 241 | std::mem::replace(&mut self.buf, rest) |
| 242 | } |
| 243 | |
| 244 | async fn send(&mut self, bytes: &[u8]) { |
| 245 | self.ws |
| 246 | .send(TMessage::Binary(bytes.to_vec().into())) |
| 247 | .await |
| 248 | .unwrap(); |
| 249 | } |
| 250 | |
| 251 | /// Handshake and return the ServerInit width/height. |
| 252 | async fn handshake(ws: Ws) -> (Self, u16, u16) { |
| 253 | let mut c = RfbClient { |
| 254 | ws, |
| 255 | buf: Vec::new(), |
| 256 | }; |
| 257 | assert_eq!(c.read(12).await, RFB_VERSION_38); |
| 258 | c.send(RFB_VERSION_38).await; |
| 259 | assert_eq!(c.read(2).await, [1, 1], "only security None offered"); |
| 260 | c.send(&[1]).await; |
| 261 | assert_eq!(c.read(4).await, [0, 0, 0, 0]); |
| 262 | c.send(&[0]).await; // ClientInit |
| 263 | let init = c.read(24).await; |
| 264 | let name_len = u32::from_be_bytes([init[20], init[21], init[22], init[23]]) as usize; |
| 265 | assert_eq!(c.read(name_len).await, b"twin"); |
| 266 | let w = u16::from_be_bytes([init[0], init[1]]); |
| 267 | let h = u16::from_be_bytes([init[2], init[3]]); |
| 268 | (c, w, h) |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | async fn wait_for_received(h: &Harness, len: usize) -> Vec<u8> { |
| 273 | for _ in 0..100 { |
| 274 | if h.received.lock().len() >= len { |
| 275 | break; |
| 276 | } |
| 277 | tokio::time::sleep(Duration::from_millis(20)).await; |
| 278 | } |
| 279 | h.received.lock().clone() |
| 280 | } |
| 281 | |
| 282 | fn event_kinds(h: &Harness) -> Vec<(String, Value)> { |
| 283 | h.computer |
| 284 | .events_since(0) |
| 285 | .0 |
| 286 | .into_iter() |
| 287 | .map(|e| (e.kind, e.data)) |
| 288 | .collect() |
| 289 | } |
| 290 | |
| 291 | const FUR: [u8; 10] = [3, 1, 0, 0, 0, 0, 5, 160, 3, 132]; |
| 292 | const KEY: [u8; 8] = [4, 1, 0, 0, 0, 0, 0, 0x61]; |
| 293 | const POINTER: [u8; 6] = [5, 1, 0, 10, 0, 20]; |
| 294 | |
| 295 | #[tokio::test] |
| 296 | async fn display_requires_a_token_or_a_single_use_ticket() { |
| 297 | let h = harness().await; |
| 298 | assert_eq!(connect(&h, None, "").await.err(), Some(401)); |
| 299 | assert_eq!(connect(&h, Some("wrong"), "").await.err(), Some(401)); |
| 300 | assert_eq!( |
| 301 | connect(&h, None, "?ticket=cwdt_forged").await.err(), |
| 302 | Some(401) |
| 303 | ); |
| 304 | |
| 305 | let http = codewhale_release::tls::reqwest_client(); |
| 306 | let resp = http |
| 307 | .post(format!("{}/v1/computer/display/tickets", h.base)) |
| 308 | .bearer_auth(MASTER) |
| 309 | .send() |
| 310 | .await |
| 311 | .unwrap(); |
| 312 | assert_eq!(resp.status().as_u16(), 201); |
| 313 | let body: Value = resp.json().await.unwrap(); |
| 314 | let ticket = body["ticket"].as_str().unwrap().to_string(); |
| 315 | |
| 316 | let ws = connect(&h, None, &format!("?ticket={ticket}")) |
| 317 | .await |
| 318 | .expect("ticket works once"); |
| 319 | let (_client, w, hgt) = RfbClient::handshake(ws).await; |
| 320 | assert_eq!((w, hgt), (1440, 900), "valid ticket reaches ServerInit"); |
| 321 | assert_eq!( |
| 322 | connect(&h, None, &format!("?ticket={ticket}")).await.err(), |
| 323 | Some(401), |
| 324 | "reused ticket is refused" |
| 325 | ); |
| 326 | } |
| 327 | |
| 328 | #[tokio::test] |
| 329 | async fn watcher_input_never_reaches_xvnc_and_lease_holder_input_does() { |
| 330 | let h = harness().await; |
| 331 | let ws = connect(&h, Some(MASTER), "").await.unwrap(); |
| 332 | let (mut c, _, _) = RfbClient::handshake(ws).await; |
| 333 | |
| 334 | // Watching: key + pointer are dropped, the update request passes. |
| 335 | let mut burst = Vec::new(); |
| 336 | burst.extend_from_slice(&KEY); |
| 337 | burst.extend_from_slice(&POINTER); |
| 338 | burst.extend_from_slice(&FUR); |
| 339 | c.send(&burst).await; |
| 340 | let got = wait_for_received(&h, FUR.len()).await; |
| 341 | tokio::time::sleep(Duration::from_millis(100)).await; |
| 342 | assert_eq!(got, FUR, "watcher input produced bytes at Xvnc"); |
| 343 | assert_eq!(h.received.lock().len(), FUR.len()); |
| 344 | |
| 345 | // Driving: after acquiring the lease the same input is forwarded. |
| 346 | let resp = codewhale_release::tls::reqwest_client() |
| 347 | .post(format!("{}/v1/computer/control/acquire", h.base)) |
| 348 | .bearer_auth(MASTER) |
| 349 | .send() |
| 350 | .await |
| 351 | .unwrap(); |
| 352 | assert_eq!(resp.status().as_u16(), 200); |
| 353 | c.send(&KEY).await; |
| 354 | let got = wait_for_received(&h, FUR.len() + KEY.len()).await; |
| 355 | assert_eq!(&got[FUR.len()..], KEY); |
| 356 | |
| 357 | let released = codewhale_release::tls::reqwest_client() |
| 358 | .post(format!("{}/v1/computer/control/release", h.base)) |
| 359 | .bearer_auth(MASTER) |
| 360 | .send() |
| 361 | .await |
| 362 | .unwrap(); |
| 363 | assert_eq!(released.status().as_u16(), 200); |
| 364 | let kinds: Vec<String> = event_kinds(&h).into_iter().map(|(k, _)| k).collect(); |
| 365 | assert!(kinds.contains(&"computer.display.attached".to_string())); |
| 366 | assert!(kinds.contains(&"computer.control.acquired".to_string())); |
| 367 | assert!(kinds.contains(&"computer.control.released".to_string())); |
| 368 | // Events carry counts, never key values. |
| 369 | let released = event_kinds(&h) |
| 370 | .into_iter() |
| 371 | .find(|(k, _)| k == "computer.control.released") |
| 372 | .unwrap() |
| 373 | .1; |
| 374 | assert_eq!(released["input_events"], 1); |
| 375 | } |
| 376 | |
| 377 | #[tokio::test] |
| 378 | async fn unknown_client_message_closes_the_stream_with_an_event() { |
| 379 | let h = harness().await; |
| 380 | let ws = connect(&h, Some(MASTER), "").await.unwrap(); |
| 381 | let (mut c, _, _) = RfbClient::handshake(ws).await; |
| 382 | c.send(&[200, 0, 0, 0]).await; |
| 383 | let close = loop { |
| 384 | match tokio::time::timeout(Duration::from_secs(5), c.ws.next()) |
| 385 | .await |
| 386 | .expect("close within 5 s") |
| 387 | { |
| 388 | Some(Ok(TMessage::Close(frame))) => break frame, |
| 389 | Some(Ok(_)) => continue, |
| 390 | other => panic!("expected close, got {other:?}"), |
| 391 | } |
| 392 | }; |
| 393 | assert_eq!(u16::from(close.unwrap().code), 1008); |
| 394 | for _ in 0..50 { |
| 395 | if event_kinds(&h) |
| 396 | .iter() |
| 397 | .any(|(k, _)| k == "computer.display.detached") |
| 398 | { |
| 399 | break; |
| 400 | } |
| 401 | tokio::time::sleep(Duration::from_millis(20)).await; |
| 402 | } |
| 403 | let detached = event_kinds(&h) |
| 404 | .into_iter() |
| 405 | .find(|(k, _)| k == "computer.display.detached") |
| 406 | .expect("detached event") |
| 407 | .1; |
| 408 | assert!( |
| 409 | detached["reason"] |
| 410 | .as_str() |
| 411 | .unwrap() |
| 412 | .contains("unknown client message type 200") |
| 413 | ); |
| 414 | assert!(h.received.lock().is_empty()); |
| 415 | } |
| 416 | |
| 417 | #[tokio::test] |
| 418 | async fn client_tokens_are_owner_minted_scoped_and_revocable() { |
| 419 | let h = harness().await; |
| 420 | let http = codewhale_release::tls::reqwest_client(); |
| 421 | let resp = http |
| 422 | .post(format!("{}/v1/auth/client-tokens", h.base)) |
| 423 | .bearer_auth(MASTER) |
| 424 | .json(&json!({ "device_id": "mac-1", "ttl_seconds": 999999 })) |
| 425 | .send() |
| 426 | .await |
| 427 | .unwrap(); |
| 428 | assert_eq!(resp.status().as_u16(), 201); |
| 429 | let body: Value = resp.json().await.unwrap(); |
| 430 | let token = body["token"].as_str().unwrap().to_string(); |
| 431 | let id = body["id"].as_str().unwrap().to_string(); |
| 432 | let expires: DateTime<Utc> = body["expires_at"].as_str().unwrap().parse().unwrap(); |
| 433 | assert!( |
| 434 | expires <= Utc::now() + chrono::Duration::seconds(3601), |
| 435 | "ttl clamps to 1 h" |
| 436 | ); |
| 437 | |
| 438 | // The client token works on the computer surface... |
| 439 | let status = http |
| 440 | .get(format!("{}/v1/computer", h.base)) |
| 441 | .bearer_auth(&token) |
| 442 | .send() |
| 443 | .await |
| 444 | .unwrap(); |
| 445 | assert_eq!(status.status().as_u16(), 200); |
| 446 | // ...and on the display, whose lease is per device. |
| 447 | let ws = connect(&h, Some(&token), "").await.unwrap(); |
| 448 | let (_c, _, _) = RfbClient::handshake(ws).await; |
| 449 | let lease: Value = http |
| 450 | .post(format!("{}/v1/computer/control/acquire", h.base)) |
| 451 | .bearer_auth(&token) |
| 452 | .send() |
| 453 | .await |
| 454 | .unwrap() |
| 455 | .json() |
| 456 | .await |
| 457 | .unwrap(); |
| 458 | assert_eq!(lease["lease"]["holder"], "device:mac-1"); |
| 459 | // The owner is refused without force, and takes over with it. |
| 460 | let conflict = http |
| 461 | .post(format!("{}/v1/computer/control/acquire", h.base)) |
| 462 | .bearer_auth(MASTER) |
| 463 | .send() |
| 464 | .await |
| 465 | .unwrap(); |
| 466 | assert_eq!(conflict.status().as_u16(), 409); |
| 467 | |
| 468 | // A client token cannot mint or list client tokens. |
| 469 | let forbidden = http |
| 470 | .post(format!("{}/v1/auth/client-tokens", h.base)) |
| 471 | .bearer_auth(&token) |
| 472 | .json(&json!({ "device_id": "evil" })) |
| 473 | .send() |
| 474 | .await |
| 475 | .unwrap(); |
| 476 | assert_eq!(forbidden.status().as_u16(), 403); |
| 477 | let bad_device = http |
| 478 | .post(format!("{}/v1/auth/client-tokens", h.base)) |
| 479 | .bearer_auth(MASTER) |
| 480 | .json(&json!({ "device_id": "has space" })) |
| 481 | .send() |
| 482 | .await |
| 483 | .unwrap(); |
| 484 | assert_eq!(bad_device.status().as_u16(), 400); |
| 485 | |
| 486 | // Revoke: the token stops working and its lease expires. |
| 487 | let revoked = http |
| 488 | .delete(format!("{}/v1/auth/client-tokens/{id}", h.base)) |
| 489 | .bearer_auth(MASTER) |
| 490 | .send() |
| 491 | .await |
| 492 | .unwrap(); |
| 493 | assert_eq!(revoked.status().as_u16(), 204); |
| 494 | let after = http |
| 495 | .get(format!("{}/v1/computer", h.base)) |
| 496 | .bearer_auth(&token) |
| 497 | .send() |
| 498 | .await |
| 499 | .unwrap(); |
| 500 | assert_eq!(after.status().as_u16(), 401); |
| 501 | let status: Value = http |
| 502 | .get(format!("{}/v1/computer", h.base)) |
| 503 | .bearer_auth(MASTER) |
| 504 | .send() |
| 505 | .await |
| 506 | .unwrap() |
| 507 | .json() |
| 508 | .await |
| 509 | .unwrap(); |
| 510 | assert!(status["control"]["lease"].is_null()); |
| 511 | assert!( |
| 512 | event_kinds(&h) |
| 513 | .iter() |
| 514 | .any(|(k, _)| k == "computer.control.expired") |
| 515 | ); |
| 516 | } |
| 517 | |
| 518 | #[tokio::test] |
| 519 | async fn missing_display_socket_is_503_not_a_hang() { |
| 520 | let dir = tempfile::tempdir().unwrap(); |
| 521 | let computer = ComputerState::new(dir.path().join("absent.sock"), DEFAULT_IDLE); |
| 522 | let app: Router = router(computer, Some(MASTER.to_string())); |
| 523 | let tcp = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); |
| 524 | let addr = tcp.local_addr().unwrap(); |
| 525 | tokio::spawn(async move { axum::serve(tcp, app).await.unwrap() }); |
| 526 | let mut req = format!("ws://{addr}/v1/computer/display") |
| 527 | .into_client_request() |
| 528 | .unwrap(); |
| 529 | req.headers_mut() |
| 530 | .insert("authorization", format!("Bearer {MASTER}").parse().unwrap()); |
| 531 | match tokio_tungstenite::connect_async(req).await { |
| 532 | Err(tokio_tungstenite::tungstenite::Error::Http(resp)) => { |
| 533 | assert_eq!(resp.status().as_u16(), 503) |
| 534 | } |
| 535 | other => panic!("expected 503, got {other:?}"), |
| 536 | } |
| 537 | } |
| 538 | } |
| 539 |