From: Lukas Wagner <l.wagner@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [RFC datacenter-manager 6/6] tests: add basic integration tests for the remote updates API
Date: Thu, 29 Jan 2026 14:44:18 +0100 [thread overview]
Message-ID: <20260129134418.307552-8-l.wagner@proxmox.com> (raw)
In-Reply-To: <20260129134418.307552-1-l.wagner@proxmox.com>
To demonstrate that the principle works and can be used to meaningfully
test subsystems in PDM.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/lib.rs | 3 +-
server/src/test_support/mod.rs | 3 +-
server/tests/test_remote_updates.rs | 288 ++++++++++++++++++++++++++++
3 files changed, 292 insertions(+), 2 deletions(-)
create mode 100644 server/tests/test_remote_updates.rs
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 5ed10d69..af847286 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -20,7 +20,8 @@ pub mod connection;
pub mod pbs_client;
pub mod sdn_client;
-#[cfg(any(remote_config = "faked", test))]
+// FIXME:
+// #[cfg(any(remote_config = "faked", test))]
pub mod test_support;
use anyhow::Error;
diff --git a/server/src/test_support/mod.rs b/server/src/test_support/mod.rs
index f026011c..b7b2b8e1 100644
--- a/server/src/test_support/mod.rs
+++ b/server/src/test_support/mod.rs
@@ -1,5 +1,6 @@
#[cfg(remote_config = "faked")]
pub mod fake_remote;
-#[cfg(test)]
+// FIXME:
+// #[cfg(test)]
pub mod temp;
diff --git a/server/tests/test_remote_updates.rs b/server/tests/test_remote_updates.rs
new file mode 100644
index 00000000..b34beac9
--- /dev/null
+++ b/server/tests/test_remote_updates.rs
@@ -0,0 +1,288 @@
+use std::any::Any;
+use std::sync::{Arc, Once};
+
+use anyhow::{bail, Error};
+use serde::de::DeserializeOwned;
+
+use proxmox_router::RpcEnvironment;
+use proxmox_section_config::typed::SectionConfigData;
+use proxmox_sys::fs::CreateOptions;
+use pve_api_types::ClusterNodeIndexResponse;
+
+use pdm_api_types::remote_updates::{PackageVersion, ProductRepositoryStatus, RemoteUpdateStatus};
+use pdm_api_types::Authid;
+use pdm_api_types::{remotes::Remote, ConfigDigest};
+use pdm_config::remotes::RemoteConfig;
+
+use server::connection::{ClientFactory, DefaultClientFactory, PveClient};
+use server::context::PdmApplication;
+use server::pbs_client::PbsClient;
+use server::test_support::temp::NamedTempDir;
+
+struct TestRpcEnv {
+ context: Arc<dyn Any + Send + Sync>,
+}
+
+impl TestRpcEnv {
+ fn new(app: PdmApplication) -> Self {
+ Self {
+ context: Arc::new(app),
+ }
+ }
+}
+
+impl RpcEnvironment for TestRpcEnv {
+ fn result_attrib_mut(&mut self) -> &mut serde_json::Value {
+ unimplemented!()
+ }
+
+ fn result_attrib(&self) -> &serde_json::Value {
+ unimplemented!()
+ }
+
+ fn env_type(&self) -> proxmox_router::RpcEnvironmentType {
+ unimplemented!()
+ }
+
+ fn set_auth_id(&mut self, _user: Option<String>) {
+ unimplemented!()
+ }
+
+ fn get_auth_id(&self) -> Option<String> {
+ Some("root@pam".to_string())
+ }
+
+ fn application_context(&self) -> Option<Arc<dyn Any + Send + Sync>> {
+ Some(Arc::clone(&self.context))
+ }
+}
+
+struct MockRemoteConfig;
+
+impl RemoteConfig for MockRemoteConfig {
+ fn config(&self) -> Result<(SectionConfigData<Remote>, ConfigDigest), anyhow::Error> {
+ let mut sections = SectionConfigData::default();
+
+ for i in 0..4 {
+ let name = format!("pve-{i}");
+
+ sections.insert(
+ name.clone(),
+ Remote {
+ ty: pdm_api_types::remotes::RemoteType::Pve,
+ id: name.clone(),
+ nodes: Vec::new(),
+ authid: Authid::root_auth_id().clone(),
+ token: "".into(),
+ web_url: None,
+ },
+ );
+ }
+
+ Ok((sections, ConfigDigest::from_slice(&[])))
+ }
+
+ fn get_secret_token(
+ &self,
+ _remote: &pdm_api_types::remotes::Remote,
+ ) -> Result<String, anyhow::Error> {
+ unimplemented!()
+ }
+
+ fn lock_config(&self) -> Result<proxmox_product_config::ApiLockGuard, anyhow::Error> {
+ unimplemented!()
+ }
+
+ fn save_config(
+ &self,
+ _remotes: proxmox_section_config::typed::SectionConfigData<pdm_api_types::remotes::Remote>,
+ ) -> Result<(), anyhow::Error> {
+ unimplemented!()
+ }
+}
+
+struct TestClientFactory;
+
+#[async_trait::async_trait]
+impl ClientFactory for TestClientFactory {
+ fn make_pve_client(&self, _remote: &Remote) -> Result<Arc<PveClient>, Error> {
+ Ok(Arc::new(TestPveClient {}))
+ }
+ /// Create a new API client for PVE remotes, but with a specific endpoint.
+ fn make_pve_client_with_endpoint(
+ &self,
+ _remote: &Remote,
+ _target_endpoint: Option<&str>,
+ ) -> Result<Arc<PveClient>, Error> {
+ bail!("not implemented")
+ }
+
+ fn make_pbs_client(&self, _remote: &Remote) -> Result<Box<PbsClient>, Error> {
+ bail!("not implemented")
+ }
+
+ fn make_raw_client(&self, _remote: &Remote) -> Result<Box<proxmox_client::Client>, Error> {
+ bail!("not implemented")
+ }
+
+ async fn make_pve_client_and_login(&self, _remote: &Remote) -> Result<Arc<PveClient>, Error> {
+ bail!("not implemented")
+ }
+
+ async fn make_pbs_client_and_login(&self, _remote: &Remote) -> Result<Box<PbsClient>, Error> {
+ bail!("not implemented")
+ }
+}
+
+struct TestPveClient {}
+
+#[async_trait::async_trait]
+impl pve_api_types::client::PveClient for TestPveClient {
+ async fn list_available_updates(
+ &self,
+ _node: &str,
+ ) -> Result<Vec<pve_api_types::AptUpdateInfo>, proxmox_client::Error> {
+ let s = tokio::fs::read_to_string("tests/api_responses/pve/apt_update.json")
+ .await
+ .unwrap();
+
+ Ok(serde_json::from_str(&s).unwrap())
+ }
+
+ /// Cluster node index.
+ async fn list_nodes(
+ &self,
+ ) -> Result<Vec<pve_api_types::ClusterNodeIndexResponse>, proxmox_client::Error> {
+ let mut nodes = Vec::new();
+
+ for i in 0..3 {
+ nodes.push(ClusterNodeIndexResponse {
+ cpu: Some(0.0),
+ level: None,
+ maxcpu: Some(4),
+ maxmem: Some(4096),
+ mem: Some(1000),
+ node: format!("node-{i}"),
+ ssl_fingerprint: None,
+ status: pve_api_types::ClusterNodeIndexResponseStatus::Online,
+ uptime: Some(1000),
+ });
+ }
+
+ Ok(nodes)
+ }
+
+ /// Get package information for important Proxmox packages.
+ async fn get_package_versions(
+ &self,
+ _node: &str,
+ ) -> Result<Vec<pve_api_types::InstalledPackage>, proxmox_client::Error> {
+ read_captured_response("tests/api_responses/pve/apt_versions.json").await
+ }
+
+ /// Get APT repository information.
+ async fn get_apt_repositories(
+ &self,
+ _node: &str,
+ ) -> Result<pve_api_types::APTRepositoriesResult, proxmox_client::Error> {
+ read_captured_response("tests/api_responses/pve/apt_repos.json").await
+ }
+
+ /// Read subscription info.
+ async fn get_subscription(
+ &self,
+ _node: &str,
+ ) -> Result<pve_api_types::NodeSubscriptionInfo, proxmox_client::Error> {
+ read_captured_response("tests/api_responses/pve/node_subscription.json").await
+ }
+}
+
+async fn read_captured_response<T: DeserializeOwned>(
+ path: &str,
+) -> Result<T, proxmox_client::Error> {
+ let s = tokio::fs::read_to_string(path).await.unwrap();
+ Ok(serde_json::from_str(&s).unwrap())
+}
+
+fn test_setup() {
+ static INIT: Once = Once::new();
+
+ INIT.call_once(|| {
+ // FIXME: As long as we are 'root@pam' and do not add any new users or change the user
+ // config this *should* be fine
+ proxmox_access_control::init::init(&pdm_api_types::AccessControlConfig, "/tmp")
+ .expect("failed to setup access control config");
+ let file_opts = CreateOptions::new();
+
+ // TODO: Create some kind of temporary directory which exists for the entire
+ // duration of the test run.
+ proxmox_rest_server::init_worker_tasks("/tmp".into(), file_opts).unwrap();
+ });
+}
+
+#[tokio::test]
+async fn update_summary_unknown_if_not_polled() {
+ test_setup();
+
+ let test_dir = NamedTempDir::new().unwrap();
+
+ // TODO: Maybe some kind of builder pattern for PdmApplication would be nice
+ let mut env = TestRpcEnv::new(PdmApplication {
+ remote_config: Arc::new(MockRemoteConfig),
+ client_factory: Arc::new(DefaultClientFactory),
+ config_path: test_dir.path().into(),
+ cache_path: test_dir.path().into(),
+ default_create_options: CreateOptions::new(),
+ });
+
+ let summary = server::api::remote_updates::update_summary(&mut env).unwrap();
+
+ for val in summary.remotes.values() {
+ assert_eq!(val.status, RemoteUpdateStatus::Unknown);
+ }
+}
+
+#[tokio::test]
+async fn update_summary_populated_after_update() {
+ test_setup();
+
+ let test_dir = NamedTempDir::new().unwrap();
+ dbg!(test_dir.path());
+
+ // TODO: Maybe some kind of builder pattern for PdmApplication would be nice
+ let mut env = TestRpcEnv::new(PdmApplication {
+ remote_config: Arc::new(MockRemoteConfig),
+ client_factory: Arc::new(TestClientFactory),
+ config_path: test_dir.path().into(),
+ cache_path: test_dir.path().into(),
+ default_create_options: CreateOptions::new(),
+ });
+
+ let upid = server::api::remote_updates::refresh_remote_update_summaries(&mut env).unwrap();
+ proxmox_rest_server::wait_for_local_worker(&upid.to_string())
+ .await
+ .unwrap();
+
+ let summary = server::api::remote_updates::update_summary(&mut env).unwrap();
+
+ for val in summary.remotes.values() {
+ assert_eq!(val.status, RemoteUpdateStatus::Success);
+
+ for node_data in val.nodes.values() {
+ assert_eq!(
+ node_data.versions,
+ vec![PackageVersion {
+ package: "pve-manager".into(),
+ version: "9.1.2".into()
+ }]
+ );
+
+ assert_eq!(
+ node_data.repository_status,
+ ProductRepositoryStatus::NonProductionReady
+ );
+
+ assert_eq!(node_data.number_of_updates, 58);
+ }
+ }
+}
--
2.47.3
next prev parent reply other threads:[~2026-01-29 13:44 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-01-29 13:44 [RFC datacenter-manager/proxmox 0/7] inject application context via rpcenv for easier integration testing Lukas Wagner
2026-01-29 13:44 ` [RFC proxmox 1/1] router: rpc environment: allow to provide a application-specific context handle via rpcenv Lukas Wagner
2026-01-29 13:44 ` [RFC datacenter-manager 1/6] connection: store client factory in an Arc and add public getter Lukas Wagner
2026-01-29 13:44 ` [RFC datacenter-manager 2/6] parallel fetcher: allow to use custom client factory Lukas Wagner
2026-01-29 13:44 ` [RFC datacenter-manager 3/6] introduce PdmApplication struct and inject it during API server startup Lukas Wagner
2026-01-29 13:44 ` [RFC datacenter-manager 4/6] remote updates: use PdmApplication object to derive paths, permissions and client factory Lukas Wagner
2026-01-29 13:44 ` [RFC datacenter-manager 5/6] tests: add captured responses for integration tests Lukas Wagner
2026-01-29 13:44 ` Lukas Wagner [this message]
2026-02-03 11:02 ` [RFC datacenter-manager/proxmox 0/7] inject application context via rpcenv for easier integration testing Robert Obkircher
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260129134418.307552-8-l.wagner@proxmox.com \
--to=l.wagner@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox