implement some tests

This commit is contained in:
snow flurry 2024-08-14 17:31:26 -07:00
parent e4df2e5075
commit 997478801c
11 changed files with 483 additions and 8 deletions

2
Cargo.lock generated
View file

@ -1800,7 +1800,6 @@ dependencies = [
"nzr-api",
"serde_json",
"tabled",
"tarpc",
"tokio",
"tokio-serde 0.9.0",
]
@ -1811,6 +1810,7 @@ version = "0.1.0"
dependencies = [
"diesel",
"figment",
"futures",
"hickory-proto",
"log",
"serde",

View file

@ -9,12 +9,6 @@ clap = { version = "4.0.26", features = ["derive"] }
home = "0.5.4"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
tokio-serde = { version = "0.9", features = ["bincode"] }
tarpc = { version = "0.34", features = [
"tokio1",
"unix",
"serde-transport",
"serde-transport-bincode",
] }
tabled = "0.15"
serde_json = "1"
log = "0.4.17"

View file

@ -6,13 +6,23 @@ edition = "2021"
[dependencies]
figment = { version = "0.10.8", features = ["json", "toml", "env"] }
serde = { version = "1", features = ["derive"] }
tarpc = { version = "0.34", features = ["tokio1", "unix"] }
tarpc = { version = "0.34", features = [
"tokio1",
"unix",
"serde-transport",
"serde-transport-bincode",
] }
tokio = { version = "1.0", features = ["macros"] }
uuid = { version = "1.2.2", features = ["serde"] }
hickory-proto = { version = "0.24", features = ["serde-config"] }
log = "0.4.17"
sqlx = "0.8"
diesel = { version = "2.2", optional = true }
futures = { version = "0.3", optional = true }
[dev-dependencies]
uuid = { version = "1.2.2", features = ["serde", "v4"] }
[features]
diesel = ["dep:diesel"]
mock = ["dep:futures"]

View file

@ -4,6 +4,8 @@ use model::{CreateStatus, Instance, Subnet};
pub mod args;
pub mod config;
#[cfg(feature = "mock")]
pub mod mock;
pub mod model;
pub mod net;
@ -63,4 +65,5 @@ pub fn new_client(sock: tokio::net::UnixStream) -> NazrinClient {
NazrinClient::new(Default::default(), transport).spawn()
}
pub use tarpc::client::RpcError;
pub use tarpc::context::current as default_ctx;

View file

@ -0,0 +1,70 @@
use std::net::Ipv4Addr;
use crate::{args, model, net::cidr::CidrV4};
pub trait NzrClientExt {
#[allow(async_fn_in_trait)]
async fn new_mock_instance(
&mut self,
name: impl AsRef<str>,
) -> Result<Result<model::Instance, String>, crate::RpcError>;
}
impl NzrClientExt for crate::NazrinClient {
async fn new_mock_instance(
&mut self,
name: impl AsRef<str>,
) -> Result<Result<model::Instance, String>, crate::RpcError> {
let name = name.as_ref().to_owned();
let subnet = self
.new_subnet(
crate::default_ctx(),
model::Subnet {
name: "mock".to_owned(),
data: model::SubnetData {
ifname: "eth0".to_string(),
network: CidrV4::new(Ipv4Addr::new(192, 0, 2, 0), 24),
start_host: Ipv4Addr::new(192, 0, 2, 10),
end_host: Ipv4Addr::new(192, 0, 2, 254),
gateway4: Some(Ipv4Addr::new(192, 0, 2, 1)),
dns: vec![Ipv4Addr::new(192, 0, 2, 5)],
domain_name: None,
vlan_id: None,
},
},
)
.await
.unwrap()
.ok();
let uuid = self
.new_instance(
crate::default_ctx(),
args::NewInstance {
name: name.clone(),
title: None,
description: None,
subnet: subnet.map_or_else(|| "mock".to_owned(), |m| m.name),
base_image: "linux2".to_owned(),
cores: 2,
memory: 1024,
disk_sizes: (10, None),
ci_userdata: None,
},
)
.await?
.unwrap();
// poll to "complete"
self.poll_new_instance(crate::default_ctx(), uuid)
.await?
.unwrap();
let inst = self
.poll_new_instance(crate::default_ctx(), uuid)
.await?
.and_then(|cs| cs.result)
.unwrap();
Ok(inst)
}
}

274
nzr-api/src/mock/mod.rs Normal file
View file

@ -0,0 +1,274 @@
pub mod client;
#[cfg(test)]
mod test;
use std::{collections::HashMap, sync::Arc};
use tarpc::server::{BaseChannel, Channel as _};
use futures::{future, StreamExt};
use tokio::{sync::RwLock, task::JoinHandle};
use crate::{
model,
net::{cidr::CidrV4, mac::MacAddr},
InstanceQuery, Nazrin, NazrinClient,
};
pub struct MockServerHandle<T>(JoinHandle<T>);
impl<T> Drop for MockServerHandle<T> {
fn drop(&mut self) {
self.0.abort();
}
}
impl<T> From<JoinHandle<T>> for MockServerHandle<T> {
fn from(value: JoinHandle<T>) -> Self {
Self(value)
}
}
#[derive(Default)]
struct MockDb {
instances: Vec<Option<model::Instance>>,
subnets: Vec<Option<model::Subnet>>,
subnet_lease: HashMap<i32, u32>,
ci_userdatas: HashMap<String, Vec<u8>>,
create_tasks: HashMap<uuid::Uuid, (model::Instance, bool)>,
}
/// Mock Nazrin RPC server for testing, where the full server isn't required.
///
/// Note that this intentionally does not perform SQL model testing!
#[derive(Clone, Default)]
pub struct MockServer {
db: Arc<RwLock<MockDb>>,
}
impl MockServer {
/// Marks a create_task as complete, assuming it exists
pub async fn complete_task(&mut self, task_id: uuid::Uuid) {
let mut db = self.db.write().await;
if let Some((_inst, done)) = db.create_tasks.get_mut(&task_id) {
let _ = std::mem::replace(done, true);
}
}
}
impl Nazrin for MockServer {
async fn new_instance(
self,
_: tarpc::context::Context,
build_args: crate::args::NewInstance,
) -> Result<uuid::Uuid, String> {
let mut db = self.db.write().await;
let Some(net_pos) = db
.subnets
.iter()
.position(|s| s.as_ref().filter(|s| s.name == build_args.subnet).is_some())
else {
return Err("Subnet doesn't exist".to_owned());
};
let subnet = db.subnets[net_pos].as_ref().unwrap().clone();
let cur_lease = *(db
.subnet_lease
.get(&(net_pos as i32))
.unwrap_or(&(subnet.data.start_bytes() as u32)));
let instance = model::Instance {
name: build_args.name.clone(),
id: -1,
lease: model::Lease {
subnet: build_args.subnet,
addr: CidrV4::new(
subnet
.data
.network
.make_ip(cur_lease)
.map_err(|e| e.to_string())?,
subnet.data.network.cidr(),
),
mac_addr: MacAddr::new(0x02, 0x04, 0x08, 0x0a, 0x0c, 0x0f),
},
state: model::DomainState::NoState,
};
db.ci_userdatas
.insert(build_args.name, build_args.ci_userdata.unwrap_or_default());
let id = uuid::Uuid::new_v4();
db.create_tasks.insert(id, (instance, false));
Ok(id)
}
async fn poll_new_instance(
mut self,
_: tarpc::context::Context,
task_id: uuid::Uuid,
) -> Option<crate::model::CreateStatus> {
let db = self.db.read().await;
let (inst, done) = db.create_tasks.get(&task_id)?;
let done = *done;
if done {
Some(model::CreateStatus {
status_text: "Done!".to_owned(),
completion: 1.0,
result: Some(Ok(inst.clone())),
})
} else {
let mut inst = inst.clone();
// Drop the read-only DB to get a write lock
std::mem::drop(db);
let mut db = self.db.write().await;
inst.id = (db.instances.len() + 1) as i32;
db.instances.push(Some(inst.clone()));
// Drop the writeable DB to avoid deadlock
std::mem::drop(db);
self.complete_task(task_id).await;
Some(model::CreateStatus {
status_text: "Working on it...".to_owned(),
completion: 0.50,
result: None,
})
}
}
async fn delete_instance(self, _: tarpc::context::Context, name: String) -> Result<(), String> {
let mut db = self.db.write().await;
let Some(inst) = db
.instances
.iter_mut()
.find(|i| i.as_ref().filter(|i| i.name == name).is_some())
.take()
else {
return Err("Instance doesn't exist".to_owned());
};
inst.take();
Ok(())
}
async fn find_instance(
self,
_: tarpc::context::Context,
query: crate::InstanceQuery,
) -> Result<Option<crate::model::Instance>, String> {
let db = self.db.read().await;
let res = {
db.instances
.iter()
.find(|opt| {
opt.as_ref()
.map(|inst| match &query {
InstanceQuery::Ipv4Addr(addr) => &inst.lease.addr.addr == addr,
InstanceQuery::MacAddr(addr) => &inst.lease.mac_addr == addr,
InstanceQuery::Name(name) => &inst.name == name,
})
.is_some()
})
.and_then(|opt| opt.as_ref().cloned())
};
Ok(res)
}
async fn get_instance_userdata(
self,
_: tarpc::context::Context,
id: i32,
) -> Result<Vec<u8>, String> {
let db = self.db.read().await;
let Some(inst) = db.instances.get(id as usize).and_then(|o| o.as_ref()) else {
return Err("No such instance".to_owned());
};
Ok(db.ci_userdatas.get(&inst.name).cloned().unwrap_or_default())
}
async fn get_instances(
self,
_: tarpc::context::Context,
_with_status: bool,
) -> Result<Vec<crate::model::Instance>, String> {
let db = self.db.read().await;
Ok(db
.instances
.iter()
.filter_map(|inst| inst.clone())
.collect())
}
async fn new_subnet(
self,
_: tarpc::context::Context,
build_args: crate::model::Subnet,
) -> Result<crate::model::Subnet, String> {
let mut db = self.db.write().await;
let subnet = build_args.clone();
db.subnets.push(Some(build_args));
Ok(subnet)
}
async fn modify_subnet(
self,
_: tarpc::context::Context,
_edit_args: crate::model::Subnet,
) -> Result<crate::model::Subnet, String> {
todo!()
}
async fn get_subnets(
self,
_: tarpc::context::Context,
) -> Result<Vec<crate::model::Subnet>, String> {
let db = self.db.read().await;
Ok(db.subnets.iter().filter_map(|net| net.clone()).collect())
}
async fn delete_subnet(
self,
_: tarpc::context::Context,
interface: String,
) -> Result<(), String> {
let mut db = self.db.write().await;
db.instances
.iter()
.filter_map(|inst| inst.as_ref())
.for_each(|inst| {
if inst.lease.subnet == interface {
todo!("what now")
}
});
let Some(subnet) = db
.subnets
.iter_mut()
.find(|net| net.as_ref().filter(|n| n.name == interface).is_some())
else {
return Err("Subnet doesn't exist".to_owned());
};
subnet.take();
Ok(())
}
async fn garbage_collect(self, _: tarpc::context::Context) -> Result<(), String> {
todo!()
}
}
/// Generates a MockServer task and connected client.
pub async fn spawn_c2s() -> (NazrinClient, MockServerHandle<()>) {
let (client_transport, server_transport) = tarpc::transport::channel::unbounded();
let server: MockServerHandle<()> = {
tokio::spawn(async move {
BaseChannel::with_defaults(server_transport)
.execute(MockServer::default().serve())
.for_each(|rpc| {
tokio::spawn(rpc);
future::ready(())
})
.await;
})
.into()
};
let client = NazrinClient::new(Default::default(), client_transport).spawn();
(client, server)
}

68
nzr-api/src/mock/test.rs Normal file
View file

@ -0,0 +1,68 @@
use crate::{args, model};
#[tokio::test]
async fn test_the_tester() {
let (client, _server) = super::spawn_c2s().await;
client
.new_subnet(
crate::default_ctx(),
model::Subnet {
name: "test".to_owned(),
data: model::SubnetData {
ifname: "eth0".into(),
network: "192.0.2.0/24".parse().unwrap(),
start_host: "192.0.2.10".parse().unwrap(),
end_host: "192.0.2.254".parse().unwrap(),
gateway4: Some("192.0.2.1".parse().unwrap()),
dns: Vec::new(),
domain_name: None,
vlan_id: None,
},
},
)
.await
.expect("RPC error")
.expect("create subnet failed");
let task_id = client
.new_instance(
crate::default_ctx(),
args::NewInstance {
name: "my-inst".to_owned(),
title: None,
description: None,
subnet: "test".to_owned(),
base_image: "some-kinda-linux".to_owned(),
cores: 42,
memory: 1337,
disk_sizes: (10, None),
ci_userdata: None,
},
)
.await
.expect("RPC error")
.expect("create instance failed");
// Poll the instance creation to "complete" it
let poll_inst = client
.poll_new_instance(crate::default_ctx(), task_id)
.await
.unwrap()
.unwrap();
assert!(poll_inst.result.is_none());
assert!(poll_inst.completion < 1.0);
let poll_inst = client
.poll_new_instance(crate::default_ctx(), task_id)
.await
.unwrap()
.unwrap();
assert!(poll_inst.result.is_some());
assert_eq!(poll_inst.completion, 1.0);
let instances = client
.get_instances(crate::default_ctx(), false)
.await
.expect("RPC error")
.expect("get instances failed");
assert_eq!(instances.len(), 1);
assert_eq!(&instances[0].name, "my-inst");
assert_eq!(&instances[0].lease.subnet, "test");
}

View file

@ -12,3 +12,6 @@ tracing-subscriber = "0.3"
anyhow = "1"
askama = "0.12"
moka = { version = "0.12.8", features = ["future"] }
[dev-dependencies]
nzr-api = { path = "../nzr-api", features = ["mock"] }

View file

@ -46,6 +46,15 @@ impl Context {
})
}
#[cfg(test)]
pub fn new_mock(cfg: Config, api_client: NazrinClient) -> Self {
Self {
api_client,
config: Arc::new(cfg),
host_cache: Cache::new(5),
}
}
// Internal function to hydrate the instance metadata, if needed
async fn get_instmeta(&self, addr: Ipv4Addr) -> Result<Option<InstanceMeta>> {
if let Some(meta) = self.host_cache.get(&addr).await {

View file

@ -1,5 +1,7 @@
mod ctx;
mod model;
#[cfg(test)]
mod test;
use std::{
net::{IpAddr, SocketAddr},

42
omyacid/src/test.rs Normal file
View file

@ -0,0 +1,42 @@
use std::net::SocketAddr;
use axum::extract::{ConnectInfo, State};
use nzr_api::{
config::{CloudConfig, Config},
mock::{self, client::NzrClientExt},
};
use crate::ctx;
#[tokio::test]
async fn get_metadata() {
let (mut client, _server) = mock::spawn_c2s().await;
let inst = client
.new_mock_instance("something")
.await
.unwrap()
.unwrap();
let cfg = Config {
cloud: CloudConfig {
listen_addr: "0.0.0.0".into(),
port: 80,
admin_user: "admin".to_owned(),
},
..Default::default()
};
let ctx = ctx::Context::new_mock(cfg, client);
let inst_sock: SocketAddr = (inst.lease.addr.addr, 54545).into();
let metadata = crate::get_meta_data(State(ctx.clone()), ConnectInfo(inst_sock))
.await
.unwrap();
assert_eq!(
metadata,
"instance_id: \"iid-something\"\nlocal_hostname: \"something\"\ndefault_username: \"admin\""
)
// TODO: Instance with SSH keys
}