use super::*;
fn cfg() -> TerminalConfig {
TerminalConfig {
token: "a".repeat(64),
}
}
fn ours() -> Vec<String> {
vec![
"http://127.0.0.1:7777".to_string(),
"http://localhost:7777".to_string(),
]
}
#[test]
fn a_store_served_without_a_terminal_has_none() {
let why = admit(None, None, &ours(), &"a".repeat(64)).unwrap_err();
assert!(why.contains("--terminal"), "{why}");
}
#[test]
fn the_right_token_from_our_own_page_is_admitted() {
assert!(
admit(
Some(&cfg()),
Some("http://127.0.0.1:7777"),
&ours(),
&"a".repeat(64)
)
.is_ok()
);
}
#[test]
fn a_page_from_somewhere_else_is_refused_even_with_the_token() {
let why = admit(
Some(&cfg()),
Some("https://example.com"),
&ours(),
&"a".repeat(64),
)
.unwrap_err();
assert!(why.contains("example.com"), "{why}");
}
#[test]
fn a_client_that_is_not_a_browser_is_admitted_with_the_token() {
assert!(admit(Some(&cfg()), None, &ours(), &"a".repeat(64)).is_ok());
}
#[test]
fn a_wrong_or_missing_token_is_refused() {
for token in ["", "b", &"b".repeat(64), &"a".repeat(63)] {
let why = admit(Some(&cfg()), None, &ours(), token).unwrap_err();
assert!(why.contains("token"), "{token:?}: {why}");
}
}
#[test]
fn the_token_is_long_and_different_every_run() {
let a = TerminalConfig::new();
let b = TerminalConfig::new();
assert_eq!(a.token.len(), 64);
assert_ne!(a.token, b.token);
assert!(a.token.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn comparing_tokens_does_not_stop_at_the_first_difference() {
assert!(constant_time_eq(b"abc", b"abc"));
assert!(!constant_time_eq(b"abc", b"abd"));
assert!(!constant_time_eq(b"abc", b"ab"));
assert!(constant_time_eq(b"", b""));
}
use crate::app::App;
use crate::serve::router_with;
use axum::body::Body;
use axum::http::{Method, Request, StatusCode};
use cyberbrain_policy::Actor;
use std::path::PathBuf;
use std::sync::Arc;
use tower::ServiceExt;
fn store() -> (tempfile::TempDir, Arc<App>) {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("store");
App::init(&root, &Actor::Operator).unwrap();
let app = Arc::new(App::open(Some(&root), Actor::Operator).unwrap());
(dir, app)
}
async fn get_profiles(terminal: Option<TerminalConfig>, token: Option<&str>) -> StatusCode {
let (_dir, app) = store();
let router = router_with(app, PathBuf::new(), terminal, Vec::new());
let mut req = Request::builder()
.method(Method::GET)
.uri("/api/v1/terminal/profiles");
if let Some(t) = token {
req = req.header("x-cyberbrain-terminal-token", t);
}
router
.oneshot(req.body(Body::empty()).unwrap())
.await
.unwrap()
.status()
}
#[tokio::test]
async fn the_saved_list_needs_the_token() {
assert_eq!(
get_profiles(Some(cfg()), None).await,
StatusCode::FORBIDDEN,
"no token"
);
assert_eq!(
get_profiles(Some(cfg()), Some("wrong")).await,
StatusCode::FORBIDDEN,
"wrong token"
);
}
#[tokio::test]
async fn a_store_without_a_terminal_has_no_saved_list_either() {
assert_eq!(
get_profiles(None, Some(&"a".repeat(64))).await,
StatusCode::FORBIDDEN
);
}
#[cfg(unix)]
#[tokio::test]
async fn a_paste_into_a_program_that_is_not_reading_does_not_block() {
use super::pty::{Pty, Spawn};
use std::time::{Duration, Instant};
let dir = tempfile::tempdir().unwrap();
let mut pty = Pty::spawn(Spawn {
command: &[
"/bin/sh".into(),
"-c".into(),
"read one; exec sleep 30".into(),
],
cwd: dir.path(),
cols: 80,
rows: 24,
})
.unwrap();
let tx = spawn_writer(pty.writer().unwrap());
let started = Instant::now();
let mut line = vec![b'x'; 4096];
line.push(b'\n');
for _ in 0..200 {
let line = line.clone();
if tx.try_send(line).is_err() && tx.is_closed() {
break;
}
}
let elapsed = started.elapsed();
assert!(
elapsed < Duration::from_secs(2),
"the write half blocked the caller for {elapsed:?}; on the real server that is every \
route hanging until the program inside reads again"
);
pty.kill();
}