From 60ccd06f2b5cd6ca0a595c437c298eb77b7f8422 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 29 Aug 2024 10:47:57 +0200 Subject: [PATCH 01/14] testing_using_container: set password and run shell even in case of failure it is more useful that way --- testing_using_container.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/testing_using_container.sh b/testing_using_container.sh index 988d3d8fd4..715a14ac11 100755 --- a/testing_using_container.sh +++ b/testing_using_container.sh @@ -44,7 +44,10 @@ podman run --name ${CNAME?} \ # shortcut for the following CEXEC="podman exec ${CNAME?} bash -c" -${CEXEC?} "cd /checkout && ./setup.sh" +# Set it up +# but continue in case of failure, the remaining steps (password and shell) +# are useful even then +${CEXEC?} "cd /checkout && ./setup.sh || true" echo "Set the Agama (root) password:" podman exec -it ${CNAME?} passwd From e604568757ef157975af9fec643dff97b8e3f5cf Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 30 Aug 2024 10:46:25 +0200 Subject: [PATCH 02/14] WITH_RUBY_DBUS=1, fix up for renamed gemspec --- setup-services.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup-services.sh b/setup-services.sh index 5e3f82e8e1..1c2c01464c 100755 --- a/setup-services.sh +++ b/setup-services.sh @@ -112,7 +112,7 @@ fi # we are in a container, told to use that one # instead of a released version # edit +Gemfile and -gemspec - sed -e '/ruby-dbus/d' -i Gemfile agama.gemspec + sed -e '/ruby-dbus/d' -i Gemfile agama-yast.gemspec sed -e '/gemspec/a gem "ruby-dbus", path: "/checkout-ruby-dbus"' -i Gemfile fi From 9dfa73410c9af87694e64afe53b724b675028e49 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 30 Aug 2024 11:03:47 +0200 Subject: [PATCH 03/14] refactor(rust): crate::software::model::{RegistrationParams,RegistrationInfo} move them from web.rs for use by ProductHTTPClient --- rust/agama-lib/src/product.rs | 3 +- rust/agama-lib/src/product/client.rs | 32 ++--------------- rust/agama-lib/src/software/model.rs | 51 +++++++++++++++++++++++++++ rust/agama-server/src/software/web.rs | 28 +++------------ 4 files changed, 59 insertions(+), 55 deletions(-) diff --git a/rust/agama-lib/src/product.rs b/rust/agama-lib/src/product.rs index c7a0a6582e..c21484b59d 100644 --- a/rust/agama-lib/src/product.rs +++ b/rust/agama-lib/src/product.rs @@ -5,6 +5,7 @@ pub mod proxies; mod settings; mod store; -pub use client::{Product, ProductClient, RegistrationRequirement}; +pub use crate::software::model::RegistrationRequirement; +pub use client::{Product, ProductClient}; pub use settings::ProductSettings; pub use store::ProductStore; diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index 3283678694..5740df2c2a 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; use crate::error::ServiceError; +use crate::software::model::RegistrationRequirement; use crate::software::proxies::SoftwareProductProxy; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use zbus::Connection; use super::proxies::RegistrationProxy; @@ -20,35 +21,6 @@ pub struct Product { pub icon: String, } -#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] -pub enum RegistrationRequirement { - /// Product does not require registration - NotRequired = 0, - /// Product has optional registration - Optional = 1, - /// It is mandatory to register the product - Mandatory = 2, -} - -impl TryFrom for RegistrationRequirement { - type Error = (); - - fn try_from(v: u32) -> Result { - match v { - x if x == RegistrationRequirement::NotRequired as u32 => { - Ok(RegistrationRequirement::NotRequired) - } - x if x == RegistrationRequirement::Optional as u32 => { - Ok(RegistrationRequirement::Optional) - } - x if x == RegistrationRequirement::Mandatory as u32 => { - Ok(RegistrationRequirement::Mandatory) - } - _ => Err(()), - } - } -} - /// D-Bus client for the software service #[derive(Clone)] pub struct ProductClient<'a> { diff --git a/rust/agama-lib/src/software/model.rs b/rust/agama-lib/src/software/model.rs index 868afe8097..e6ed9573d2 100644 --- a/rust/agama-lib/src/software/model.rs +++ b/rust/agama-lib/src/software/model.rs @@ -9,3 +9,54 @@ pub struct SoftwareConfig { /// Name of the product to install. pub product: Option, } + +/// Software service configuration (product, patterns, etc.). +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RegistrationParams { + /// Registration key. + pub key: String, + /// Registration email. + pub email: String, +} + +/// Information about registration configuration (product, patterns, etc.). +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RegistrationInfo { + /// Registration key. Empty value mean key not used or not registered. + pub key: String, + /// Registration email. Empty value mean email not used or not registered. + pub email: String, + /// if registration is required, optional or not needed for current product. + /// Change only if selected product is changed. + pub requirement: RegistrationRequirement, +} + +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] +pub enum RegistrationRequirement { + /// Product does not require registration + NotRequired = 0, + /// Product has optional registration + Optional = 1, + /// It is mandatory to register the product + Mandatory = 2, +} + +impl TryFrom for RegistrationRequirement { + type Error = (); + + fn try_from(v: u32) -> Result { + match v { + x if x == RegistrationRequirement::NotRequired as u32 => { + Ok(RegistrationRequirement::NotRequired) + } + x if x == RegistrationRequirement::Optional as u32 => { + Ok(RegistrationRequirement::Optional) + } + x if x == RegistrationRequirement::Mandatory as u32 => { + Ok(RegistrationRequirement::Mandatory) + } + _ => Err(()), + } + } +} diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index ab8526b151..baeefc4200 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -12,11 +12,12 @@ use crate::{ Event, }, }; + use agama_lib::{ error::ServiceError, - product::{proxies::RegistrationProxy, Product, ProductClient, RegistrationRequirement}, + product::{proxies::RegistrationProxy, Product, ProductClient}, software::{ - model::SoftwareConfig, + model::{RegistrationInfo, RegistrationParams, SoftwareConfig}, proxies::{Software1Proxy, SoftwareProductProxy}, Pattern, SelectedBy, SoftwareClient, UnknownSelectedBy, }, @@ -229,19 +230,6 @@ async fn products(State(state): State>) -> Result Date: Fri, 30 Aug 2024 11:04:02 +0200 Subject: [PATCH 04/14] ProductHTTPClient --- rust/agama-lib/src/product.rs | 2 + rust/agama-lib/src/product/http_client.rs | 75 +++++++++++++++++++++++ rust/agama-lib/src/product/store.rs | 6 +- 3 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 rust/agama-lib/src/product/http_client.rs diff --git a/rust/agama-lib/src/product.rs b/rust/agama-lib/src/product.rs index c21484b59d..2903f10b4d 100644 --- a/rust/agama-lib/src/product.rs +++ b/rust/agama-lib/src/product.rs @@ -1,11 +1,13 @@ //! Implements support for handling the product settings mod client; +mod http_client; pub mod proxies; mod settings; mod store; pub use crate::software::model::RegistrationRequirement; pub use client::{Product, ProductClient}; +pub use http_client::ProductHTTPClient; pub use settings::ProductSettings; pub use store::ProductStore; diff --git a/rust/agama-lib/src/product/http_client.rs b/rust/agama-lib/src/product/http_client.rs new file mode 100644 index 0000000000..04d78473fe --- /dev/null +++ b/rust/agama-lib/src/product/http_client.rs @@ -0,0 +1,75 @@ +use crate::software::model::RegistrationInfo; +use crate::software::model::RegistrationParams; +use crate::software::model::SoftwareConfig; +use crate::{base_http_client::BaseHTTPClient, error::ServiceError}; + +pub struct ProductHTTPClient { + client: BaseHTTPClient, +} + +impl ProductHTTPClient { + pub fn new() -> Result { + Ok(Self { + client: BaseHTTPClient::new()?, + }) + } + + pub fn new_with_base(base: BaseHTTPClient) -> Self { + Self { client: base } + } + + // FIXME get_software_config ? + pub async fn get_config(&self) -> Result { + self.client.get("/software/config").await + } + + pub async fn set_config(&self, config: &SoftwareConfig) -> Result<(), ServiceError> { + self.client.put_void("/software/config", config).await + } + + /// Returns the id of the selected product to install + pub async fn product(&self) -> Result { + let config = self.get_config().await?; + if let Some(product) = config.product { + Ok(product) + } else { + Ok("".to_owned()) + } + } + + /// Selects the product to install + pub async fn select_product(&self, product_id: &str) -> Result<(), ServiceError> { + let config = SoftwareConfig { + product: Some(product_id.to_owned()), + patterns: None, + }; + self.set_config(&config).await + } + + pub async fn get_registration(&self) -> Result { + self.client.get("/software/registration").await + } + + /// registration code used to register product + pub async fn registration_code(&self) -> Result { + let ri = self.get_registration().await?; + Ok(ri.key) + } + + /// email used to register product + pub async fn email(&self) -> Result { + let ri = self.get_registration().await?; + Ok(ri.email) + } + + /// register product + pub async fn register(&self, code: &str, email: &str) -> Result<(u32, String), ServiceError> { + // note RegistrationParams != RegistrationInfo, fun! + let params = RegistrationParams { + key: code.to_owned(), + email: email.to_owned(), + }; + + self.client.post("/software/registration", ¶ms).await + } +} diff --git a/rust/agama-lib/src/product/store.rs b/rust/agama-lib/src/product/store.rs index 5ed3e72745..7e8abbaae2 100644 --- a/rust/agama-lib/src/product/store.rs +++ b/rust/agama-lib/src/product/store.rs @@ -1,20 +1,20 @@ //! Implements the store for the product settings. -use super::{ProductClient, ProductSettings}; +use super::{ProductHTTPClient, ProductSettings}; use crate::error::ServiceError; use crate::manager::ManagerClient; use zbus::Connection; /// Loads and stores the product settings from/to the D-Bus service. pub struct ProductStore<'a> { - product_client: ProductClient<'a>, + product_client: ProductHTTPClient, manager_client: ManagerClient<'a>, } impl<'a> ProductStore<'a> { pub async fn new(connection: Connection) -> Result, ServiceError> { Ok(Self { - product_client: ProductClient::new(connection.clone()).await?, + product_client: ProductHTTPClient::new()?, manager_client: ManagerClient::new(connection).await?, }) } From 69fe745061d9073df83a3bf0743b7381ff0ac05c Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Mon, 2 Sep 2024 14:56:54 +0200 Subject: [PATCH 05/14] add and use ManagerHTTPClient compared to the D-Bus ManagerClient, it has only the probe method which is the only one used by CLI BUT now config load with product and patterns SOMETIMES fails with no such pattern... does the probe, which spawns a new thread in the web server, make a race? --- rust/agama-lib/src/manager.rs | 3 +++ rust/agama-lib/src/manager/http_client.rs | 23 +++++++++++++++++++++++ rust/agama-lib/src/product/store.rs | 14 ++++++-------- rust/agama-lib/src/store.rs | 4 ++-- 4 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 rust/agama-lib/src/manager/http_client.rs diff --git a/rust/agama-lib/src/manager.rs b/rust/agama-lib/src/manager.rs index ef7d9f7bc2..577af76ae7 100644 --- a/rust/agama-lib/src/manager.rs +++ b/rust/agama-lib/src/manager.rs @@ -1,5 +1,8 @@ //! This module implements the web API for the manager module. +pub mod http_client; +pub use http_client::ManagerHTTPClient; + use crate::error::ServiceError; use crate::proxies::ServiceStatusProxy; use crate::{ diff --git a/rust/agama-lib/src/manager/http_client.rs b/rust/agama-lib/src/manager/http_client.rs new file mode 100644 index 0000000000..0a6f424b4d --- /dev/null +++ b/rust/agama-lib/src/manager/http_client.rs @@ -0,0 +1,23 @@ +use crate::{base_http_client::BaseHTTPClient, error::ServiceError}; + +pub struct ManagerHTTPClient { + client: BaseHTTPClient, +} + +impl ManagerHTTPClient { + pub fn new() -> Result { + Ok(Self { + client: BaseHTTPClient::new()?, + }) + } + + pub fn new_with_base(base: BaseHTTPClient) -> Self { + Self { client: base } + } + + pub async fn probe(&self) -> Result<(), ServiceError> { + // BaseHTTPClient did not anticipate POST without request body + let empty_body: Vec = vec![]; + self.client.post_void("/manager/probe", &empty_body).await + } +} diff --git a/rust/agama-lib/src/product/store.rs b/rust/agama-lib/src/product/store.rs index 7e8abbaae2..1f99dd6c6b 100644 --- a/rust/agama-lib/src/product/store.rs +++ b/rust/agama-lib/src/product/store.rs @@ -1,21 +1,19 @@ //! Implements the store for the product settings. - use super::{ProductHTTPClient, ProductSettings}; use crate::error::ServiceError; -use crate::manager::ManagerClient; -use zbus::Connection; +use crate::manager::http_client::ManagerHTTPClient; /// Loads and stores the product settings from/to the D-Bus service. -pub struct ProductStore<'a> { +pub struct ProductStore { product_client: ProductHTTPClient, - manager_client: ManagerClient<'a>, + manager_client: ManagerHTTPClient, } -impl<'a> ProductStore<'a> { - pub async fn new(connection: Connection) -> Result, ServiceError> { +impl ProductStore { + pub fn new() -> Result { Ok(Self { product_client: ProductHTTPClient::new()?, - manager_client: ManagerClient::new(connection).await?, + manager_client: ManagerHTTPClient::new()?, }) } diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index 036d6cfc9b..8a043e6dcb 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -18,7 +18,7 @@ use zbus::Connection; pub struct Store<'a> { users: UsersStore, network: NetworkStore, - product: ProductStore<'a>, + product: ProductStore, software: SoftwareStore, storage: StorageStore<'a>, localization: LocalizationStore, @@ -33,7 +33,7 @@ impl<'a> Store<'a> { localization: LocalizationStore::new()?, users: UsersStore::new()?, network: NetworkStore::new(http_client).await?, - product: ProductStore::new(connection.clone()).await?, + product: ProductStore::new()?, software: SoftwareStore::new()?, storage: StorageStore::new(connection).await?, }) From e8b7d9dcaecd9bbd6cc79048aae5cc009b9af1b7 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Mon, 2 Sep 2024 14:58:23 +0200 Subject: [PATCH 06/14] Fix openapi for /api/manager, it uses POST not GET --- rust/agama-server/src/manager/web.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/agama-server/src/manager/web.rs b/rust/agama-server/src/manager/web.rs index 79d87c4f2c..8977c4774c 100644 --- a/rust/agama-server/src/manager/web.rs +++ b/rust/agama-server/src/manager/web.rs @@ -105,7 +105,7 @@ pub async fn manager_service(dbus: zbus::Connection) -> Result(State(state): State>) -> Result<(), E /// Starts the probing process. #[utoipa::path( - get, + post, path = "/install", context_path = "/api/manager", responses( @@ -150,7 +150,7 @@ async fn install_action(State(state): State>) -> Result<(), Err /// Executes the post installation tasks (e.g., rebooting the system). #[utoipa::path( - get, + post, path = "/install", context_path = "/api/manager", responses( From e02f52d3051bc92ac20da9dc4a8e91c3ba1b664e Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Mon, 2 Sep 2024 15:59:29 +0200 Subject: [PATCH 07/14] testing_using_container: link built binaries to /usr/bin --- setup-services.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup-services.sh b/setup-services.sh index 1c2c01464c..c573ea566e 100755 --- a/setup-services.sh +++ b/setup-services.sh @@ -146,6 +146,8 @@ $SUDO $ZYPPER install \ ( cd $MYDIR/rust cargo build + + ln -st /usr/bin $MYDIR/rust/target/debug/agama{,*server} ) # - D-Bus configuration From 6ba18731aeb89d64b577c41b77b56a985c4c46a9 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Tue, 3 Sep 2024 13:26:06 +0200 Subject: [PATCH 08/14] web server+client: (re)introduce /api/manager/probe_sync This fixes the following scenario: ```console agama # cat rust/agama-lib/share/examples/profile_tw_gnome.json { "software": { "patterns": [ "gnome" ] } } agama # systemctl restart agama agama # agama config load < rust/agama-lib/share/examples/profile_gnome.json Anyhow(Backend call failed with status 400 and text '{"error":"Agama service error: Failed to find these patterns: [\"gnome\"]"}') agama # systemctl restart agama agama # PROBE_SYNC=1 agama config load < rust/agama-lib/share/examples/profile_gnome.json agama # ``` The asynchronous probing, introduced in PR#1272 results in a race(?) and an error when performing the second PUT: ``` Sep 03 11:20:06 2cf2b88a0524 agama-web-server[9357]: request: GET /api/software/config Sep 03 11:20:06 2cf2b88a0524 agama-web-server[9357]: request: PUT /api/software/config Sep 03 11:20:06 2cf2b88a0524 agama-web-server[9357]: request: POST /api/manager/probe Sep 03 11:20:06 2cf2b88a0524 agama-web-server[9357]: request: PUT /api/software/config ``` --- rust/agama-lib/src/manager/http_client.rs | 6 ++++-- rust/agama-server/src/manager/web.rs | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/rust/agama-lib/src/manager/http_client.rs b/rust/agama-lib/src/manager/http_client.rs index 0a6f424b4d..71e869989c 100644 --- a/rust/agama-lib/src/manager/http_client.rs +++ b/rust/agama-lib/src/manager/http_client.rs @@ -17,7 +17,9 @@ impl ManagerHTTPClient { pub async fn probe(&self) -> Result<(), ServiceError> { // BaseHTTPClient did not anticipate POST without request body - let empty_body: Vec = vec![]; - self.client.post_void("/manager/probe", &empty_body).await + let dummy_body: Vec = vec![]; + self.client + .post_void("/manager/probe_sync", &dummy_body) + .await } } diff --git a/rust/agama-server/src/manager/web.rs b/rust/agama-server/src/manager/web.rs index 8977c4774c..fbf5aea530 100644 --- a/rust/agama-server/src/manager/web.rs +++ b/rust/agama-server/src/manager/web.rs @@ -91,6 +91,7 @@ pub async fn manager_service(dbus: zbus::Connection) -> Result(State(state): State>) -> Result<(), E Ok(()) } -/// Starts the probing process. +/// Starts the probing process and waits until it is done. +/// We need this because the CLI (agama_lib::Store) only does sync calls. +#[utoipa::path( + post, + path = "/probe_sync", + context_path = "/api/manager", + responses( + (status = 200, description = "Probing done.") + ) +)] +async fn probe_sync_action(State(state): State>) -> Result<(), Error> { + state.manager.probe().await?; + Ok(()) +} + +/// Starts the installation process. #[utoipa::path( post, path = "/install", From d565036aaa05416145bcb548fd81786fb2977fd7 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 5 Sep 2024 10:49:22 +0200 Subject: [PATCH 09/14] test Productstore::load --- rust/agama-lib/src/base_http_client.rs | 1 + rust/agama-lib/src/product/settings.rs | 2 +- rust/agama-lib/src/product/store.rs | 77 +++++++++++++++++++++++++- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index a080ac4420..8838a6e434 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -20,6 +20,7 @@ use crate::{auth::AuthToken, error::ServiceError}; /// client.get("/questions").await /// } /// ``` +#[derive(Clone)] pub struct BaseHTTPClient { client: reqwest::Client, pub base_url: String, diff --git a/rust/agama-lib/src/product/settings.rs b/rust/agama-lib/src/product/settings.rs index b4674a3d9c..701444fe72 100644 --- a/rust/agama-lib/src/product/settings.rs +++ b/rust/agama-lib/src/product/settings.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; /// Software settings for installation -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProductSettings { /// ID of the product to install (e.g., "ALP", "Tumbleweed", etc.) diff --git a/rust/agama-lib/src/product/store.rs b/rust/agama-lib/src/product/store.rs index 1f99dd6c6b..fdebe53088 100644 --- a/rust/agama-lib/src/product/store.rs +++ b/rust/agama-lib/src/product/store.rs @@ -17,6 +17,14 @@ impl ProductStore { }) } + fn non_empty_string(s: String) -> Option { + if s.is_empty() { + None + } else { + Some(s) + } + } + pub async fn load(&self) -> Result { let product = self.product_client.product().await?; let registration_code = self.product_client.registration_code().await?; @@ -24,8 +32,8 @@ impl ProductStore { Ok(ProductSettings { id: Some(product), - registration_code: Some(registration_code), - registration_email: Some(email), + registration_code: Self::non_empty_string(registration_code), + registration_email: Self::non_empty_string(email), }) } @@ -61,3 +69,68 @@ impl ProductStore { Ok(()) } } + +#[cfg(test)] +mod test { + use super::*; + use crate::base_http_client::BaseHTTPClient; + use httpmock::prelude::*; + use std::error::Error; + use tokio::test; // without this, "error: async functions cannot be used for tests" + + fn product_store(mock_server_url: String) -> ProductStore { + let mut bhc = BaseHTTPClient::default(); + bhc.base_url = mock_server_url; + let p_client = ProductHTTPClient::new_with_base(bhc.clone()); + let m_client = ManagerHTTPClient::new_with_base(bhc); + ProductStore { + product_client: p_client, + manager_client: m_client, + } + } + + #[test] + async fn test_getting_product() -> Result<(), Box> { + let server = MockServer::start(); + let software_mock = server.mock(|when, then| { + when.method(GET).path("/api/software/config"); + then.status(200) + .header("content-type", "application/json") + .body( + r#"{ + "patterns": {"xfce":true}, + "product": "Tumbleweed" + }"#, + ); + }); + let registration_mock = server.mock(|when, then| { + when.method(GET).path("/api/software/registration"); + then.status(200) + .header("content-type", "application/json") + .body( + r#"{ + "key": "", + "email": "", + "requirement": "NotRequired" + }"#, + ); + }); + let url = server.url("/api"); + + let store = product_store(url); + let settings = store.load().await?; + + let expected = ProductSettings { + id: Some("Tumbleweed".to_owned()), + registration_code: None, + registration_email: None, + }; + // main assertion + assert_eq!(settings, expected); + + // Ensure the specified mock was called exactly one time (or fail with a detailed error description). + software_mock.assert(); + registration_mock.assert_hits(2); + Ok(()) + } +} From a20aed346a57e1ee5c472dbcf58e1b228fa60c54 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 5 Sep 2024 11:56:24 +0200 Subject: [PATCH 10/14] test ProductStore::store --- rust/agama-lib/src/product/store.rs | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/rust/agama-lib/src/product/store.rs b/rust/agama-lib/src/product/store.rs index fdebe53088..9ebb23e61a 100644 --- a/rust/agama-lib/src/product/store.rs +++ b/rust/agama-lib/src/product/store.rs @@ -133,4 +133,54 @@ mod test { registration_mock.assert_hits(2); Ok(()) } + + #[test] + async fn test_setting_product_ok() -> Result<(), Box> { + let server = MockServer::start(); + // no product selected at first + let get_software_mock = server.mock(|when, then| { + when.method(GET).path("/api/software/config"); + then.status(200) + .header("content-type", "application/json") + .body( + r#"{ + "patterns": {}, + "product": "" + }"#, + ); + }); + let software_mock = server.mock(|when, then| { + when.method(PUT) + .path("/api/software/config") + .header("content-type", "application/json") + .body(r#"{"patterns":null,"product":"Tumbleweed"}"#); + then.status(200); + }); + let manager_mock = server.mock(|when, then| { + when.method(POST) + .path("/api/manager/probe_sync") + .header("content-type", "application/json") + .body(r#"[]"#); + then.status(200); + }); + let url = server.url("/api"); + + let store = product_store(url); + let settings = ProductSettings { + id: Some("Tumbleweed".to_owned()), + registration_code: None, + registration_email: None, + }; + + let result = store.store(&settings).await; + + // main assertion + result?; + + // Ensure the specified mock was called exactly one time (or fail with a detailed error description). + get_software_mock.assert(); + software_mock.assert(); + manager_mock.assert(); + Ok(()) + } } From 996b4ce14731b502203ffd6340d61840edfe0a20 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 5 Sep 2024 16:12:12 +0200 Subject: [PATCH 11/14] changelog --- rust/package/agama.changes | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rust/package/agama.changes b/rust/package/agama.changes index b85be02baa..620841fda2 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Mon Sep 9 09:09:54 UTC 2024 - Martin Vidner + +- For CLI, use HTTP clients instead of D-Bus clients, + for Product (name and registration) (gh#openSUSE/agama#1548) + - added ProductHTTPClient + ------------------------------------------------------------------- Thu Sep 5 16:25:00 UTC 2024 - Lubos Kocman From 56335baf9e4ad8eeae78372c1a99ebe8696a6ec3 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Tue, 10 Sep 2024 16:23:13 +0200 Subject: [PATCH 12/14] call /api/manager/probe_sync with null body that is, "null". The server will ignore it anyway and it is shorter in the client code compared to the empty array. --- rust/agama-lib/src/manager/http_client.rs | 6 ++---- rust/agama-lib/src/product/store.rs | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/rust/agama-lib/src/manager/http_client.rs b/rust/agama-lib/src/manager/http_client.rs index 71e869989c..9255efd738 100644 --- a/rust/agama-lib/src/manager/http_client.rs +++ b/rust/agama-lib/src/manager/http_client.rs @@ -17,9 +17,7 @@ impl ManagerHTTPClient { pub async fn probe(&self) -> Result<(), ServiceError> { // BaseHTTPClient did not anticipate POST without request body - let dummy_body: Vec = vec![]; - self.client - .post_void("/manager/probe_sync", &dummy_body) - .await + // so we pass () which is rendered as `null` + self.client.post_void("/manager/probe_sync", &()).await } } diff --git a/rust/agama-lib/src/product/store.rs b/rust/agama-lib/src/product/store.rs index 9ebb23e61a..e5207ee0c3 100644 --- a/rust/agama-lib/src/product/store.rs +++ b/rust/agama-lib/src/product/store.rs @@ -160,7 +160,7 @@ mod test { when.method(POST) .path("/api/manager/probe_sync") .header("content-type", "application/json") - .body(r#"[]"#); + .body("null"); then.status(200); }); let url = server.url("/api"); From 0e5fcdb237512ef8811191d94675efbf67732ac5 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Tue, 10 Sep 2024 16:25:36 +0200 Subject: [PATCH 13/14] call ProductHTTPClient::get_registration directly to save 1 HTTP call --- rust/agama-lib/src/product/http_client.rs | 12 ------------ rust/agama-lib/src/product/store.rs | 9 ++++----- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/rust/agama-lib/src/product/http_client.rs b/rust/agama-lib/src/product/http_client.rs index 04d78473fe..cc8e483105 100644 --- a/rust/agama-lib/src/product/http_client.rs +++ b/rust/agama-lib/src/product/http_client.rs @@ -50,18 +50,6 @@ impl ProductHTTPClient { self.client.get("/software/registration").await } - /// registration code used to register product - pub async fn registration_code(&self) -> Result { - let ri = self.get_registration().await?; - Ok(ri.key) - } - - /// email used to register product - pub async fn email(&self) -> Result { - let ri = self.get_registration().await?; - Ok(ri.email) - } - /// register product pub async fn register(&self, code: &str, email: &str) -> Result<(u32, String), ServiceError> { // note RegistrationParams != RegistrationInfo, fun! diff --git a/rust/agama-lib/src/product/store.rs b/rust/agama-lib/src/product/store.rs index e5207ee0c3..1c3609e27d 100644 --- a/rust/agama-lib/src/product/store.rs +++ b/rust/agama-lib/src/product/store.rs @@ -27,13 +27,12 @@ impl ProductStore { pub async fn load(&self) -> Result { let product = self.product_client.product().await?; - let registration_code = self.product_client.registration_code().await?; - let email = self.product_client.email().await?; + let registration_info = self.product_client.get_registration().await?; Ok(ProductSettings { id: Some(product), - registration_code: Self::non_empty_string(registration_code), - registration_email: Self::non_empty_string(email), + registration_code: Self::non_empty_string(registration_info.key), + registration_email: Self::non_empty_string(registration_info.email), }) } @@ -130,7 +129,7 @@ mod test { // Ensure the specified mock was called exactly one time (or fail with a detailed error description). software_mock.assert(); - registration_mock.assert_hits(2); + registration_mock.assert(); Ok(()) } From 11c4c5e5da391457dda435e34b8e69446765ad2c Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Tue, 10 Sep 2024 16:25:55 +0200 Subject: [PATCH 14/14] ProductHTTPClient: better naming --- rust/agama-lib/src/product/http_client.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/rust/agama-lib/src/product/http_client.rs b/rust/agama-lib/src/product/http_client.rs index cc8e483105..8c6f01054b 100644 --- a/rust/agama-lib/src/product/http_client.rs +++ b/rust/agama-lib/src/product/http_client.rs @@ -18,18 +18,17 @@ impl ProductHTTPClient { Self { client: base } } - // FIXME get_software_config ? - pub async fn get_config(&self) -> Result { + pub async fn get_software(&self) -> Result { self.client.get("/software/config").await } - pub async fn set_config(&self, config: &SoftwareConfig) -> Result<(), ServiceError> { + pub async fn set_software(&self, config: &SoftwareConfig) -> Result<(), ServiceError> { self.client.put_void("/software/config", config).await } /// Returns the id of the selected product to install pub async fn product(&self) -> Result { - let config = self.get_config().await?; + let config = self.get_software().await?; if let Some(product) = config.product { Ok(product) } else { @@ -43,7 +42,7 @@ impl ProductHTTPClient { product: Some(product_id.to_owned()), patterns: None, }; - self.set_config(&config).await + self.set_software(&config).await } pub async fn get_registration(&self) -> Result { @@ -51,10 +50,10 @@ impl ProductHTTPClient { } /// register product - pub async fn register(&self, code: &str, email: &str) -> Result<(u32, String), ServiceError> { + pub async fn register(&self, key: &str, email: &str) -> Result<(u32, String), ServiceError> { // note RegistrationParams != RegistrationInfo, fun! let params = RegistrationParams { - key: code.to_owned(), + key: key.to_owned(), email: email.to_owned(), };