public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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





  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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal