use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpListener;
use std::sync::mpsc;
use upcloud_api::{connect, Credential, Endpoint, Options, Stop, UpCloudApi};
#[derive(Debug)]
struct Seen {
line: String,
headers: Vec<(String, String)>,
body: Vec<u8>,
}
impl Seen {
fn header(&self, k: &str) -> Option<&str> {
self.headers.iter().find(|(h, _)| h.eq_ignore_ascii_case(k)).map(|(_, v)| v.as_str())
}
}
fn serve(n: usize) -> (String, mpsc::Receiver<Seen>) {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let base = format!("http://127.0.0.1:{}", l.local_addr().unwrap().port());
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
for _ in 0..n {
let (s, _) = l.accept().unwrap();
let mut r = BufReader::new(s.try_clone().unwrap());
let mut line = String::new();
r.read_line(&mut line).unwrap();
let mut headers = Vec::new();
loop {
let mut h = String::new();
r.read_line(&mut h).unwrap();
let h = h.trim_end().to_string();
if h.is_empty() {
break;
}
let (k, v) = h.split_once(':').unwrap();
headers.push((k.trim().to_string(), v.trim().to_string()));
}
let len: usize = headers.iter().find(|(k, _)| k.eq_ignore_ascii_case("content-length")).map(|(_, v)| v.parse().unwrap()).unwrap_or(0);
let mut body = vec![0u8; len];
r.read_exact(&mut body).unwrap();
let mut s = s;
s.write_all(b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}").unwrap();
tx.send(Seen { line: line.trim_end().to_string(), headers, body }).unwrap();
}
});
(base, rx)
}
#[test]
fn an_api_call_carries_the_versioned_path_and_the_bearer() {
let (base, rx) = serve(2);
let api = connect(&Endpoint::mock(&base).unwrap(), Credential::Token("tok".into()), Options::default());
assert!(api.server("abc").unwrap().ok());
let seen = rx.recv().unwrap();
assert_eq!(seen.line, "GET /1.3/server/abc HTTP/1.1");
assert_eq!(seen.header("authorization"), Some("Bearer tok"));
api.stop_server("abc", Stop::Soft { timeout_s: 60 }).unwrap();
let seen = rx.recv().unwrap();
assert_eq!(seen.line, "POST /1.3/server/abc/stop HTTP/1.1");
let v: serde_json::Value = serde_json::from_slice(&seen.body).unwrap();
assert_eq!(v["stop_server"]["timeout"], serde_json::json!("60"), "a number is accepted and IGNORED by UpCloud");
}
#[test]
fn the_medium_goes_up_with_a_declared_length_and_no_bearer() {
let (base, rx) = serve(1);
let api = connect(&Endpoint::mock(&base).unwrap(), Credential::Token("tok".into()), Options::default());
let dir = std::env::temp_dir().join(format!("upcloud-api-wire-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let f = dir.join("medium.iso");
std::fs::write(&f, b"0123456789abcdef").unwrap();
let url = format!("{base}/uploader/session/SECRETSESSION");
assert!(api.upload_direct(&url, &f).unwrap().ok());
let seen = rx.recv().unwrap();
assert_eq!(seen.line, "PUT /uploader/session/SECRETSESSION HTTP/1.1");
assert_eq!(seen.header("authorization"), None, "the API bearer has no business at the upload host");
assert_eq!(seen.header("content-length"), Some("16"), "a chunked PUT is answered 411 by the real upload host");
assert_eq!(seen.header("transfer-encoding"), None);
assert_eq!(seen.header("content-type"), Some("application/octet-stream"));
assert_eq!(seen.body, b"0123456789abcdef");
}
#[test]
fn a_mock_wire_refuses_an_upload_url_that_is_not_its_own() {
let api = connect(&Endpoint::mock("http://127.0.0.1:1").unwrap(), Credential::Token("t".into()), Options::default());
let f = std::env::temp_dir().join("upcloud-api-wire-never-read");
let e = api.upload_direct("https://fi-hel1.img.upcloud.com/uploader/session/SECRET", &f).unwrap_err();
assert!(e.contains("upload-url-foreign") && !e.contains("SECRET"), "{e}");
}