From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 69CFA1FF13F for ; Thu, 29 Jan 2026 14:44:14 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id D2AF75605; Thu, 29 Jan 2026 14:44:39 +0100 (CET) From: Lukas Wagner 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 Message-ID: <20260129134418.307552-8-l.wagner@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260129134418.307552-1-l.wagner@proxmox.com> References: <20260129134418.307552-1-l.wagner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1769694200500 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.037 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: CDL5TRFXTJFUKX6DREG3TPSBOFJNKBLE X-Message-ID-Hash: CDL5TRFXTJFUKX6DREG3TPSBOFJNKBLE X-MailFrom: l.wagner@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: To demonstrate that the principle works and can be used to meaningfully test subsystems in PDM. Signed-off-by: Lukas Wagner --- 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, +} + +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) { + unimplemented!() + } + + fn get_auth_id(&self) -> Option { + Some("root@pam".to_string()) + } + + fn application_context(&self) -> Option> { + Some(Arc::clone(&self.context)) + } +} + +struct MockRemoteConfig; + +impl RemoteConfig for MockRemoteConfig { + fn config(&self) -> Result<(SectionConfigData, 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 { + unimplemented!() + } + + fn lock_config(&self) -> Result { + unimplemented!() + } + + fn save_config( + &self, + _remotes: proxmox_section_config::typed::SectionConfigData, + ) -> Result<(), anyhow::Error> { + unimplemented!() + } +} + +struct TestClientFactory; + +#[async_trait::async_trait] +impl ClientFactory for TestClientFactory { + fn make_pve_client(&self, _remote: &Remote) -> Result, 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, Error> { + bail!("not implemented") + } + + fn make_pbs_client(&self, _remote: &Remote) -> Result, Error> { + bail!("not implemented") + } + + fn make_raw_client(&self, _remote: &Remote) -> Result, Error> { + bail!("not implemented") + } + + async fn make_pve_client_and_login(&self, _remote: &Remote) -> Result, Error> { + bail!("not implemented") + } + + async fn make_pbs_client_and_login(&self, _remote: &Remote) -> Result, 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, 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, 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, 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 { + read_captured_response("tests/api_responses/pve/apt_repos.json").await + } + + /// Read subscription info. + async fn get_subscription( + &self, + _node: &str, + ) -> Result { + read_captured_response("tests/api_responses/pve/node_subscription.json").await + } +} + +async fn read_captured_response( + path: &str, +) -> Result { + 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