use std::sync::Arc;
use serde_json::Value;
use upcloud_api::{Call, Exchange, Over, Reply};
use crate::http::{answer, Mock, Outcome};
pub struct FakeUpCloud {
mock: Arc<Mock>,
credential: String,
}
impl FakeUpCloud {
pub fn over(mock: Arc<Mock>, credential: &str) -> Over<FakeUpCloud> {
{
let mut e = mock.estate.lock().unwrap();
if e.upload_base.is_empty() {
e.upload_base = IN_PROCESS_UPLOAD_BASE.to_string();
}
}
Over(FakeUpCloud { mock, credential: credential.to_string() })
}
}
pub const IN_PROCESS_UPLOAD_BASE: &str = "mock-upcloud-in-process:";
impl Exchange for FakeUpCloud {
fn describe(&self) -> String {
"MOCK_UPCLOUD in-process — a FAKE".to_string()
}
fn is_the_account(&self) -> bool {
false
}
fn exchange(&self, call: Call<'_>) -> Result<Reply, String> {
let line = call.line();
let out = match call {
Call::Api { method, path, body } => {
let raw = body.map(|b| b.to_string().into_bytes()).unwrap_or_default();
answer(&self.mock, method.as_str(), &format!("/1.3{path}"), Some(&self.credential), raw)
}
Call::Upload { url, file } => {
let base = self.mock.estate.lock().unwrap().upload_base.clone();
let Some(session) = url.strip_prefix(base.as_str()) else {
return Err(format!(
"REFUSED [upload-url-foreign] {line} does not belong to this mock (its upload base is \
{base:?}) — nothing is uploaded to a host the run was not aimed at"
));
};
let bytes = std::fs::read(file).map_err(|e| format!("{}: {e}", file.display()))?;
answer(&self.mock, "PUT", session, None, bytes)
}
};
match out {
Outcome::Reset => Err(format!("{line} (the fake): transport — the connection was closed with no reply")),
Outcome::Reply { status, body } => {
let text = if body.is_null() { String::new() } else { body.to_string() };
Ok(Reply { status, body: if body.is_null() { Value::Null } else { body }, text })
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock::Clock;
use crate::estate::Estate;
use crate::faults::Faults;
use upcloud_api::{DeviceKind, NewStorage, UpCloudApi};
fn mock() -> Arc<Mock> {
Mock::recording(Estate::new(Clock::virtual_only(), Faults::default(), 7))
}
#[test]
fn the_face_is_heard_and_is_never_the_account() {
let m = mock();
let api = FakeUpCloud::over(m.clone(), "ucat_mock_face");
assert!(!api.is_the_account() && api.describe().contains("FAKE"));
assert!(api.account().unwrap().ok());
let h = m.heard_from(&crate::digest::sha256_hex(b"ucat_mock_face"));
assert_eq!((h.requests, h.account_calls), (1, 1));
}
#[test]
fn a_storage_the_face_creates_is_the_storage_the_router_reads() {
let m = mock();
let api = FakeUpCloud::over(m.clone(), "t");
let r = api
.create_storage(&NewStorage { title: "gunnar-seed", zone: "se-sto1", size_gib: 1, tier: "maxiops", labels: &[] })
.unwrap();
assert!(r.ok(), "{}", r.text);
let uuid = r.body["storage"]["uuid"].as_str().unwrap().to_string();
let (status, v) = crate::http::probe(&m, "GET", &format!("/1.3/storage/{uuid}"), Value::Null);
assert_eq!(status, 200);
assert_eq!(v["storage"]["title"], "gunnar-seed");
let calls = m.calls.as_ref().unwrap().lock().unwrap();
assert_eq!(calls[0].1, "POST");
assert_eq!(calls[0].2, "/1.3/storage");
}
#[test]
fn a_refusal_is_the_routers_refusal() {
let m = mock();
let api = FakeUpCloud::over(m, "t");
let r = api.server("00000000-0000-0000-0000-000000000000").unwrap();
assert_eq!(r.status, 404, "{}", r.text);
let r = api.attach_storage("00000000-0000-0000-0000-000000000000", DeviceKind::Disk, "nope", None).unwrap();
assert!(!r.ok());
}
#[test]
fn an_upload_follows_the_answered_url_and_refuses_a_foreign_one() {
let m = mock();
let api = FakeUpCloud::over(m.clone(), "t");
let seed = api
.create_storage(&NewStorage { title: "seed", zone: "se-sto1", size_gib: 1, tier: "maxiops", labels: &[] })
.unwrap();
let uuid = seed.body["storage"]["uuid"].as_str().unwrap().to_string();
m.estate.lock().unwrap().run_to_quiet();
let imp = api.import_direct_upload(&uuid).unwrap();
assert!(imp.ok(), "{}", imp.text);
let url = imp.body["storage_import"]["direct_upload_url"].as_str().unwrap().to_string();
assert!(url.starts_with(IN_PROCESS_UPLOAD_BASE), "{url}");
let f = std::env::temp_dir().join(format!("mock-upcloud-face-{}", std::process::id()));
std::fs::write(&f, b"an installer medium").unwrap();
let up = api.upload_direct(&url, &f).unwrap();
assert!(up.ok(), "{}", up.text);
assert_eq!(
up.body["sha256sum"].as_str().unwrap(),
crate::digest::sha256_hex(b"an installer medium"),
"the digest is REAL — the ladder verifies it"
);
let e = api.upload_direct("https://fi-hel1.img.upcloud.com/uploader/session/x", &f).unwrap_err();
assert!(e.contains("upload-url-foreign"), "{e}");
}
}
#[cfg(all(test, feature = "test-inject"))]
mod injection {
use super::*;
use crate::clock::Clock;
use crate::estate::Estate;
use crate::faults::Faults;
use upcloud_api::UpCloudApi;
#[test]
fn an_injection_answers_once_and_is_then_spent() {
let m = Mock::recording(Estate::new(Clock::virtual_only(), Faults::default(), 1));
m.inject(crate::http::Injection {
method: "GET".into(),
matches: Box::new(|path, _| path == "/1.3/account"),
status: 409,
code: "INJECTED".into(),
message: "by the test".into(),
times: 1,
});
let api = FakeUpCloud::over(m.clone(), "t");
let r = api.account().unwrap();
assert_eq!((r.status, r.error_code()), (409, "INJECTED"));
assert!(api.account().unwrap().ok(), "spent after one");
assert_eq!(m.calls.as_ref().unwrap().lock().unwrap().len(), 2);
}
}