implement some tests
This commit is contained in:
parent
e4df2e5075
commit
997478801c
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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;
|
||||
|
|
70
nzr-api/src/mock/client.rs
Normal file
70
nzr-api/src/mock/client.rs
Normal 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
274
nzr-api/src/mock/mod.rs
Normal 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
68
nzr-api/src/mock/test.rs
Normal 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");
|
||||
}
|
|
@ -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"] }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
mod ctx;
|
||||
mod model;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
|
|
42
omyacid/src/test.rs
Normal file
42
omyacid/src/test.rs
Normal 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
|
||||
}
|
Loading…
Reference in a new issue