From 6abef5d0516f0e376ddedb62d552b15052e141d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 3 Dec 2024 22:15:14 +0000 Subject: [PATCH 01/47] feat(products): add a SLES 15.6 definition * Only for testing purposes. --- products.d/agama-products.spec | 1 + products.d/sles_156.yaml | 162 +++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 products.d/sles_156.yaml diff --git a/products.d/agama-products.spec b/products.d/agama-products.spec index 10e6448cc2..4db27c60a9 100644 --- a/products.d/agama-products.spec +++ b/products.d/agama-products.spec @@ -65,6 +65,7 @@ Definition of SLE-based products (e.g., SUSE Linux Enterprise Server) for the Ag %license LICENSE %dir %{_datadir}/agama %dir %{_datadir}/agama/products.d +%{_datadir}/agama/products.d/sles_156.yaml %{_datadir}/agama/products.d/sles_160.yaml %{_datadir}/agama/products.d/sles_sap_160.yaml diff --git a/products.d/sles_156.yaml b/products.d/sles_156.yaml new file mode 100644 index 0000000000..cf242e68a0 --- /dev/null +++ b/products.d/sles_156.yaml @@ -0,0 +1,162 @@ +id: SLES +name: SUSE Linux Enterprise Server 15.6 +# ------------------------------------------------------------------------------ +# WARNING: When changing the product description delete the translations located +# at the at translations/description key below to avoid using obsolete +# translations!! +# ------------------------------------------------------------------------------ +description: "An open, reliable, compliant, and future-proof Linux Server choice + that ensures the enterprise's business continuity. It is the secure and + adaptable OS for long-term supported, innovation-ready infrastructure running + business-critical workloads on-premises, in the cloud, and at the edge." +icon: SUSE.svg +# Do not manually change any translations! See README.md for more details. +translations: + description: + ca: Una opció de servidor de Linux oberta, fiable, compatible i a prova del + futur que garanteix la continuïtat del negoci de l'empresa. És el sistema + operatiu segur i adaptable per a una infraestructura amb suport a llarg + termini i preparada per a la innovació que executa càrregues de treball + crítiques per a l'empresa a les instal·lacions, al núvol i a l'última. + cs: + Otevřená, spolehlivá, kompatibilní a perspektivní volba linuxového serveru, + která zajišťuje kontinuitu podnikání podniku. Je to bezpečný a + přizpůsobivý operační systém pro dlouhodobě podporovanou infrastrukturu + připravenou na inovace, na které běží kritické podnikové úlohy v lokálním + prostředí, v cloudu i na okraji sítě. + es: + Una opción de servidor Linux abierta, confiable, compatible y preparada para + el futuro que garantiza la continuidad del negocio de la empresa. Es el + sistema operativo seguro y adaptable para una infraestructura lista para + la innovación y con soporte a largo plazo que ejecuta cargas de trabajo + críticas para el negocio en las instalaciones, en la nube y en el borde. + ja: + オープンで信頼性が高く、各種の標準にも準拠し、将来性とビジネスの継続性を支援する Linux + サーバです。長期のサポートが提供されていることから、安全性と順応性に優れ、オンプレミスからクラウド、エッジ環境に至るまで、様々な場所で重要なビジネス処理をこなすことのできる革新性の高いインフラストラクチャです。 + pt_BR: + Uma escolha de servidor Linux aberta, confiável, compatível e à prova do + futuro que garante a continuidade dos negócios da empresa. É o SO seguro e + adaptável para infraestrutura com suporte de longo prazo e pronta para + inovação, executando cargas de trabalho críticas para os negócios no + local, na nuvem e na borda. + sv: Ett öppet, pålitligt, kompatibelt och framtidssäkert Linux-serverval som + säkerställer företagets affärskontinuitet. Det är det säkra och + anpassningsbara operativsystemet för långsiktigt stödd, innovationsfärdig + infrastruktur som kör affärskritiska arbetsbelastningar på plats, i molnet + och vid kanten. + tr: + İşletmenin iş sürekliliğini garanti eden açık, güvenilir, uyumlu ve geleceğe + dönük bir Linux Sunucu seçeneği. Uzun vadeli desteklenen, inovasyona hazır + altyapı için güvenli ve uyarlanabilir işletim sistemidir. Şirket içinde, + bulutta ve uçta iş açısından kritik iş yüklerini çalıştırır. +software: + installation_repositories: [] + mandatory_patterns: + - base_traditional + optional_patterns: null # no optional pattern shared + user_patterns: [] + mandatory_packages: + - NetworkManager + optional_packages: null + base_product: SLES + version: "15-6" + +security: + lsm: selinux + available_lsms: + selinux: + patterns: + - selinux + policy: enforcing + none: + patterns: null + +storage: + space_policy: delete + volumes: + - "/" + - "swap" + volume_templates: + - mount_path: "/" + filesystem: btrfs + btrfs: + snapshots: true + read_only: false + default_subvolume: "@" + subvolumes: + - path: home + - path: opt + - path: root + - path: srv + - path: usr/local + # Unified var subvolume - https://lists.opensuse.org/opensuse-packaging/2017-11/msg00017.html + - path: var + copy_on_write: false + # Architecture specific subvolumes + - path: boot/grub2/arm64-efi + archs: aarch64 + - path: boot/grub2/arm-efi + archs: arm + - path: boot/grub2/i386-pc + archs: x86_64 + - path: boot/grub2/powerpc-ieee1275 + archs: ppc,!board_powernv + - path: boot/grub2/s390x-emu + archs: s390 + - path: boot/grub2/x86_64-efi + archs: x86_64 + - path: boot/grub2/riscv64-efi + archs: riscv64 + size: + auto: true + outline: + required: true + filesystems: + - btrfs + - ext2 + - ext3 + - ext4 + - xfs + auto_size: + base_min: 5 GiB + base_max: 15 GiB + snapshots_increment: 250% + max_fallback_for: + - "/home" + snapshots_configurable: true + - mount_path: "swap" + filesystem: swap + size: + min: 1 GiB + max: 2 GiB + outline: + required: false + filesystems: + - swap + - mount_path: "/home" + filesystem: xfs + size: + auto: false + min: 10 GiB + max: unlimited + outline: + required: false + filesystems: + - btrfs + - ext2 + - ext3 + - ext4 + - xfs + - filesystem: xfs + size: + auto: false + min: 1 GiB + outline: + required: false + filesystems: + - btrfs + - ext2 + - ext3 + - ext4 + - xfs + - vfat From c593d0fe1a891cefa01564a5cd4f326a3be6d16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 3 Dec 2024 22:16:08 +0000 Subject: [PATCH 02/47] fix(service): fix wrong method name --- service/lib/agama/software/manager.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb index 63f4675af5..f76046ea7a 100644 --- a/service/lib/agama/software/manager.rb +++ b/service/lib/agama/software/manager.rb @@ -339,7 +339,7 @@ def registration # rubocop:disable Metrics/AbcSize def add_service(service) # init repos, so we are sure we operate on "/" and have GPG imported - initialize_target_repos + initialize_target # save repositories before refreshing added services (otherwise # pkg-bindings will treat them as removed by the service refresh and # unload them) From fc4da539cf951d014524ec91d37520a138967864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 4 Dec 2024 11:36:02 +0000 Subject: [PATCH 03/47] feat(service): include registration requirement in the product --- service/lib/agama/dbus/software/product.rb | 5 +++-- service/lib/agama/software/product.rb | 9 +++++++++ service/lib/agama/software/product_builder.rb | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb index 53209ff0a3..db2838a944 100644 --- a/service/lib/agama/dbus/software/product.rb +++ b/service/lib/agama/dbus/software/product.rb @@ -58,8 +58,9 @@ def available_products product.id, product.display_name, { - "description" => product.localized_description, - "icon" => product.icon + "description" => product.localized_description, + "icon" => product.icon, + "registration" => product.registration } ] end diff --git a/service/lib/agama/software/product.rb b/service/lib/agama/software/product.rb index 3fe27489a7..ff18bdc9f0 100644 --- a/service/lib/agama/software/product.rb +++ b/service/lib/agama/software/product.rb @@ -19,6 +19,8 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. +require "agama/registration" + module Agama module Software # Represents a product that Agama can install. @@ -90,6 +92,12 @@ class Product # @return [Array] attr_accessor :user_patterns + # Determines if the product should be registered. + # + # @see Agama::Registration::Requirement + # @return [String] + attr_accessor :registration + # Product translations. # # @example @@ -115,6 +123,7 @@ def initialize(id) @optional_patterns = [] # nil = display all visible patterns, [] = display no patterns @user_patterns = nil + @registration = Agama::Registration::Requirement::NOT_REQUIRED @translations = {} end diff --git a/service/lib/agama/software/product_builder.rb b/service/lib/agama/software/product_builder.rb index a9ff1d655b..1d5234682b 100644 --- a/service/lib/agama/software/product_builder.rb +++ b/service/lib/agama/software/product_builder.rb @@ -65,6 +65,7 @@ def initialize_product(id, data, attrs) product.name = data[:name] product.version = data[:version] product.icon = attrs["icon"] if attrs["icon"] + product.registration = attrs["registration"] if attrs["registration"] end end From 68dad4ceba1ffc2f72665c53742cf7fa6a1909cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 4 Dec 2024 11:41:48 +0000 Subject: [PATCH 04/47] feat(rust): include registration requirement in the product --- rust/agama-lib/src/product/client.rs | 17 +++++++------ rust/agama-lib/src/software/model.rs | 35 +++++++++------------------ rust/agama-server/src/software/web.rs | 26 -------------------- 3 files changed, 22 insertions(+), 56 deletions(-) diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index 8517ddb9e9..4e6080b4f0 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -19,7 +19,9 @@ // find current contact information at www.suse.com. use std::collections::HashMap; +use std::str::FromStr; +use crate::dbus::get_property; use crate::error::ServiceError; use crate::software::model::RegistrationRequirement; use crate::software::proxies::SoftwareProductProxy; @@ -39,6 +41,8 @@ pub struct Product { pub description: String, /// Product icon (e.g., "default.svg") pub icon: String, + /// Registration requirement + pub registration: RegistrationRequirement, } /// D-Bus client for the software service @@ -72,11 +76,17 @@ impl<'a> ProductClient<'a> { Some(value) => value.try_into().unwrap(), None => "default.svg", }; + + let registration = get_property::(&data, "registration") + .map(|r| RegistrationRequirement::from_str(&r).unwrap_or_default()) + .unwrap_or_default(); + Product { id, name, description: description.to_string(), icon: icon.to_string(), + registration, } }) .collect(); @@ -114,13 +124,6 @@ impl<'a> ProductClient<'a> { Ok(self.registration_proxy.email().await?) } - pub async fn registration_requirement(&self) -> Result { - let requirement = self.registration_proxy.requirement().await?; - // unknown number can happen only if we do programmer mistake - let result: RegistrationRequirement = requirement.try_into().unwrap(); - Ok(result) - } - /// register product pub async fn register(&self, code: &str, email: &str) -> Result<(u32, String), ServiceError> { let mut options: HashMap<&str, &zbus::zvariant::Value> = HashMap::new(); diff --git a/rust/agama-lib/src/software/model.rs b/rust/agama-lib/src/software/model.rs index 31f81fbcaf..619b533a8f 100644 --- a/rust/agama-lib/src/software/model.rs +++ b/rust/agama-lib/src/software/model.rs @@ -47,14 +47,22 @@ pub struct RegistrationInfo { 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)] +#[derive( + Clone, + Default, + Debug, + Serialize, + Deserialize, + strum::Display, + strum::EnumString, + utoipa::ToSchema, +)] +#[strum(serialize_all = "camelCase")] pub enum RegistrationRequirement { /// Product does not require registration + #[default] NotRequired = 0, /// Product has optional registration Optional = 1, @@ -62,25 +70,6 @@ pub enum RegistrationRequirement { 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(()), - } - } -} - /// Software resolvable type (package or pattern). #[derive(Deserialize, Serialize, strum::Display, utoipa::ToSchema)] #[strum(serialize_all = "camelCase")] diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index f4a0532349..f800c75cab 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -74,10 +74,6 @@ pub async fn software_streams(dbus: zbus::Connection) -> Result Result, Error> { - // TODO: move registration requirement to product in dbus and so just one event will be needed. - let proxy = RegistrationProxy::new(&dbus).await?; - let stream = proxy - .receive_requirement_changed() - .await - .then(|change| async move { - if let Ok(id) = change.get().await { - // unwrap is safe as possible numbers is send by our controlled dbus - return Some(Event::RegistrationRequirementChanged { - requirement: id.try_into().unwrap(), - }); - } - None - }) - .filter_map(|e| e); - Ok(stream) -} - async fn registration_email_changed_stream( dbus: zbus::Connection, ) -> Result, Error> { @@ -269,7 +244,6 @@ async fn get_registration( let result = RegistrationInfo { key: state.product.registration_code().await?, email: state.product.email().await?, - requirement: state.product.registration_requirement().await?, }; Ok(Json(result)) } From fbbb7a74b522275d4612095de69206a1ab5db2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 4 Dec 2024 11:50:11 +0000 Subject: [PATCH 05/47] feat(products): add registration requirement to SLE 15 SP6 --- products.d/sles_156.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/products.d/sles_156.yaml b/products.d/sles_156.yaml index cf242e68a0..ea22e9266e 100644 --- a/products.d/sles_156.yaml +++ b/products.d/sles_156.yaml @@ -1,5 +1,6 @@ id: SLES name: SUSE Linux Enterprise Server 15.6 +registration: "mandatory" # ------------------------------------------------------------------------------ # WARNING: When changing the product description delete the translations located # at the at translations/description key below to avoid using obsolete From d0beb0b007cbd6931b49db871e9df2fccd0b3666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 4 Dec 2024 12:12:16 +0000 Subject: [PATCH 06/47] feat(service): move the "version" field to the top-level --- products.d/sles_156.yaml | 2 +- service/lib/agama/software/product_builder.rb | 2 +- .../agama/software/product_builder_test.rb | 22 +++++++++++-------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/products.d/sles_156.yaml b/products.d/sles_156.yaml index ea22e9266e..48c70ca4af 100644 --- a/products.d/sles_156.yaml +++ b/products.d/sles_156.yaml @@ -1,6 +1,7 @@ id: SLES name: SUSE Linux Enterprise Server 15.6 registration: "mandatory" +version: "15-6" # ------------------------------------------------------------------------------ # WARNING: When changing the product description delete the translations located # at the at translations/description key below to avoid using obsolete @@ -60,7 +61,6 @@ software: - NetworkManager optional_packages: null base_product: SLES - version: "15-6" security: lsm: selinux diff --git a/service/lib/agama/software/product_builder.rb b/service/lib/agama/software/product_builder.rb index 1d5234682b..a2507507bd 100644 --- a/service/lib/agama/software/product_builder.rb +++ b/service/lib/agama/software/product_builder.rb @@ -66,6 +66,7 @@ def initialize_product(id, data, attrs) product.version = data[:version] product.icon = attrs["icon"] if attrs["icon"] product.registration = attrs["registration"] if attrs["registration"] + product.version = attrs["version"] if attrs["version"] end end @@ -99,7 +100,6 @@ def set_translations(product, attrs) def product_data_from_config(id) { name: config.products.dig(id, "software", "base_product"), - version: config.products.dig(id, "software", "version"), icon: config.products.dig(id, "software", "icon"), labels: config.arch_elements_from( id, "software", "installation_labels", property: :label diff --git a/service/test/agama/software/product_builder_test.rb b/service/test/agama/software/product_builder_test.rb index a48bc87ceb..cf67c02b0f 100644 --- a/service/test/agama/software/product_builder_test.rb +++ b/service/test/agama/software/product_builder_test.rb @@ -41,6 +41,8 @@ "id" => "Test1", "name" => "Product Test 1", "description" => "This is a test product named Test 1", + "version" => "1.0", + "registration" => "mandatory", "translations" => { "description" => { "cs" => "Czech", @@ -84,19 +86,19 @@ "archs" => "aarch64" } ], - "base_product" => "Test1", - "version" => "1.0" + "base_product" => "Test1" } }, { - "id" => "Test2", - "name" => "Product Test 2", - "description" => "This is a test product named Test 2", - "archs" => "x86_64,aarch64", - "software" => { + "id" => "Test2", + "name" => "Product Test 2", + "description" => "This is a test product named Test 2", + "archs" => "x86_64,aarch64", + "version" => "2.0", + "registration" => "optional", + "software" => { "mandatory_patterns" => ["pattern2-1"], - "base_product" => "Test2", - "version" => "2.0" + "base_product" => "Test2" } }, { @@ -143,6 +145,7 @@ description: "This is a test product named Test 1", name: "Test1", version: "1.0", + registration: "mandatory", repositories: ["https://repos/test1/x86_64/product/"], mandatory_patterns: ["pattern1-1", "pattern1-2"], optional_patterns: ["pattern1-3"], @@ -200,6 +203,7 @@ description: "This is a test product named Test 2", name: "Test2", version: "2.0", + registration: "optional", repositories: [], mandatory_patterns: ["pattern2-1"], optional_patterns: [], From bf7d346da98964c6f7d61d88a151b6b6fc11b2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 4 Dec 2024 12:23:23 +0000 Subject: [PATCH 07/47] refactor(rust): change registration "not_required" to "no" --- rust/agama-lib/src/software/model.rs | 2 +- service/lib/agama/dbus/software/product.rb | 2 +- service/lib/agama/registration.rb | 6 +++--- service/lib/agama/software/product.rb | 2 +- service/test/agama/dbus/software/product_test.rb | 2 +- service/test/agama/registration_test.rb | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rust/agama-lib/src/software/model.rs b/rust/agama-lib/src/software/model.rs index 619b533a8f..1678334473 100644 --- a/rust/agama-lib/src/software/model.rs +++ b/rust/agama-lib/src/software/model.rs @@ -63,7 +63,7 @@ pub struct RegistrationInfo { pub enum RegistrationRequirement { /// Product does not require registration #[default] - NotRequired = 0, + No = 0, /// Product has optional registration Optional = 1, /// It is mandatory to register the product diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb index db2838a944..40b7d2b1fd 100644 --- a/service/lib/agama/dbus/software/product.rb +++ b/service/lib/agama/dbus/software/product.rb @@ -178,7 +178,7 @@ def register(reg_code, email: nil) [1, "Product not selected yet"] elsif backend.registration.reg_code [2, "Product already registered"] - elsif backend.registration.requirement == Agama::Registration::Requirement::NOT_REQUIRED + elsif backend.registration.requirement == Agama::Registration::Requirement::NO [3, "Product does not require registration"] else connect_result(first_error_code: 4) do diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb index 3466147215..bc180f1ccd 100644 --- a/service/lib/agama/registration.rb +++ b/service/lib/agama/registration.rb @@ -41,7 +41,7 @@ class Registration attr_reader :email module Requirement - NOT_REQUIRED = :not_required + NO = :no OPTIONAL = :optional MANDATORY = :mandatory end @@ -142,10 +142,10 @@ def finish # # @return [Symbol] See {Requirement}. def requirement - return Requirement::NOT_REQUIRED unless product + return Requirement::NO unless product return Requirement::MANDATORY if product.repositories.none? - Requirement::NOT_REQUIRED + Requirement::NO end # Callbacks to be called when registration changes (e.g., a different product is selected). diff --git a/service/lib/agama/software/product.rb b/service/lib/agama/software/product.rb index ff18bdc9f0..01b219bf09 100644 --- a/service/lib/agama/software/product.rb +++ b/service/lib/agama/software/product.rb @@ -123,7 +123,7 @@ def initialize(id) @optional_patterns = [] # nil = display all visible patterns, [] = display no patterns @user_patterns = nil - @registration = Agama::Registration::Requirement::NOT_REQUIRED + @registration = Agama::Registration::Requirement::NO @translations = {} end diff --git a/service/test/agama/dbus/software/product_test.rb b/service/test/agama/dbus/software/product_test.rb index 8ca3d45787..cadcdf8dae 100644 --- a/service/test/agama/dbus/software/product_test.rb +++ b/service/test/agama/dbus/software/product_test.rb @@ -158,7 +158,7 @@ end context "if the registration is not required" do - let(:requirement) { Agama::Registration::Requirement::NOT_REQUIRED } + let(:requirement) { Agama::Registration::Requirement::NO } it "returns 0" do expect(subject.requirement).to eq(0) diff --git a/service/test/agama/registration_test.rb b/service/test/agama/registration_test.rb index a2dd6731b8..05b7757fef 100644 --- a/service/test/agama/registration_test.rb +++ b/service/test/agama/registration_test.rb @@ -346,7 +346,7 @@ let(:product) { nil } it "returns not required" do - expect(subject.requirement).to eq(Agama::Registration::Requirement::NOT_REQUIRED) + expect(subject.requirement).to eq(Agama::Registration::Requirement::NO) end end @@ -359,7 +359,7 @@ let(:repositories) { ["https://repo"] } it "returns not required" do - expect(subject.requirement).to eq(Agama::Registration::Requirement::NOT_REQUIRED) + expect(subject.requirement).to eq(Agama::Registration::Requirement::NO) end end From 9aac42681f219db7e5ca7f4b1c9e01694c1a6086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 4 Dec 2024 13:35:02 +0000 Subject: [PATCH 08/47] chore(products): temporarily move SLE 15 definition to openSUSE package --- products.d/agama-products.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products.d/agama-products.spec b/products.d/agama-products.spec index 4db27c60a9..d252bd9b75 100644 --- a/products.d/agama-products.spec +++ b/products.d/agama-products.spec @@ -52,6 +52,7 @@ Definition of openSUSE products (Tumbleweed, Leap, MicroOS and Slowroll) for the %{_datadir}/agama/products.d/tumbleweed.yaml %{_datadir}/agama/products.d/leap_160.yaml %{_datadir}/agama/products.d/slowroll.yaml +%{_datadir}/agama/products.d/sles_160.yaml %package sle Summary: Definition of SLE products for the Agama installer. @@ -66,7 +67,6 @@ Definition of SLE-based products (e.g., SUSE Linux Enterprise Server) for the Ag %dir %{_datadir}/agama %dir %{_datadir}/agama/products.d %{_datadir}/agama/products.d/sles_156.yaml -%{_datadir}/agama/products.d/sles_160.yaml %{_datadir}/agama/products.d/sles_sap_160.yaml %changelog From 9906fc03ab61bc6e0dade90c0a9670bc835108c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 4 Dec 2024 14:00:57 +0000 Subject: [PATCH 09/47] chore(products): swap SLE 15 and SLE 16 definitions --- products.d/agama-products.spec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/products.d/agama-products.spec b/products.d/agama-products.spec index d252bd9b75..41aba34169 100644 --- a/products.d/agama-products.spec +++ b/products.d/agama-products.spec @@ -52,7 +52,7 @@ Definition of openSUSE products (Tumbleweed, Leap, MicroOS and Slowroll) for the %{_datadir}/agama/products.d/tumbleweed.yaml %{_datadir}/agama/products.d/leap_160.yaml %{_datadir}/agama/products.d/slowroll.yaml -%{_datadir}/agama/products.d/sles_160.yaml +%{_datadir}/agama/products.d/sles_156.yaml %package sle Summary: Definition of SLE products for the Agama installer. @@ -66,7 +66,7 @@ Definition of SLE-based products (e.g., SUSE Linux Enterprise Server) for the Ag %license LICENSE %dir %{_datadir}/agama %dir %{_datadir}/agama/products.d -%{_datadir}/agama/products.d/sles_156.yaml +%{_datadir}/agama/products.d/sles_160.yaml %{_datadir}/agama/products.d/sles_sap_160.yaml %changelog From 22b734dc66cfcb4c4ba590c38256bc7bb6252ce4 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 4 Dec 2024 17:34:07 +0100 Subject: [PATCH 10/47] quick POC to fix registration: --- service/lib/agama/registration.rb | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb index bc180f1ccd..7d50e9c401 100644 --- a/service/lib/agama/registration.rb +++ b/service/lib/agama/registration.rb @@ -30,6 +30,15 @@ module Agama # Handles everything related to registration of system to SCC, RMT or similar. class Registration + + # Note: identical and keep in sync with Software::Manager::TARGET_DIR + TARGET_DIR = "/run/agama/zypp" + private_constant :TARGET_DIR + + GLOBAL_CREDENTIALS_PATH = File.join(TARGET_DIR, + SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE) + private_constant :GLOBAL_CREDENTIALS_PATH + # Code used for registering the product. # # @return [String, nil] nil if the product is not registered yet. @@ -74,7 +83,7 @@ def register(code, email: "") login, password = SUSE::Connect::YaST.announce_system(connect_params, target_distro) # write the global credentials # TODO: check if we can do it in memory for libzypp - SUSE::Connect::YaST.create_credentials_file(login, password) + SUSE::Connect::YaST.create_credentials_file(login, password, GLOBAL_CREDENTIALS_PATH) target_product = OpenStruct.new( arch: Yast::Arch.rpm_arch, @@ -86,7 +95,7 @@ def register(code, email: "") # if service require specific credentials file, store it @credentials_file = credentials_from_url(@service.url) if @credentials_file - SUSE::Connect::YaST.create_credentials_file(login, password, @credentials_file) + SUSE::Connect::YaST.create_credentials_file(login, password, File.join(TARGET_DIR, @credentials_file)) end Y2Packager::NewRepositorySetup.instance.add_service(@service.name) @software.add_service(@service) @@ -116,9 +125,9 @@ def deregister email: email } SUSE::Connect::YaST.deactivate_system(connect_params) - FileUtils.rm(SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE) # connect does not remove it itself + FileUtils.rm(GLOBAL_CREDENTIALS_PATH) # connect does not remove it itself if @credentials_file - FileUtils.rm(credentials_path(@credentials_file)) + FileUtils.rm(credentials_path(File.join(TARGET_DIR, @credentials_file))) @credentials_file = nil end @@ -131,7 +140,7 @@ def deregister def finish return unless reg_code - files = [credentials_path(@credentials_file), SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE] + files = [credentials_path(@credentials_file), SUSE::Connect::YaST::GLOBAL_CREDENTIALS_PATH] files.each do |file| dest = File.join(Yast::Installation.destdir, file) FileUtils.cp(file, dest) From b9c01551b418e06af6eea7a8d0a37c2c4d6565bb Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 4 Dec 2024 17:41:35 +0100 Subject: [PATCH 11/47] make rubocop happy and fix tests --- service/lib/agama/registration.rb | 6 +++--- service/test/agama/registration_test.rb | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb index 7d50e9c401..c3618c70c5 100644 --- a/service/lib/agama/registration.rb +++ b/service/lib/agama/registration.rb @@ -30,8 +30,7 @@ module Agama # Handles everything related to registration of system to SCC, RMT or similar. class Registration - - # Note: identical and keep in sync with Software::Manager::TARGET_DIR + # NOTE: identical and keep in sync with Software::Manager::TARGET_DIR TARGET_DIR = "/run/agama/zypp" private_constant :TARGET_DIR @@ -95,7 +94,8 @@ def register(code, email: "") # if service require specific credentials file, store it @credentials_file = credentials_from_url(@service.url) if @credentials_file - SUSE::Connect::YaST.create_credentials_file(login, password, File.join(TARGET_DIR, @credentials_file)) + SUSE::Connect::YaST.create_credentials_file(login, password, + File.join(TARGET_DIR, @credentials_file)) end Y2Packager::NewRepositorySetup.instance.add_service(@service.name) @software.add_service(@service) diff --git a/service/test/agama/registration_test.rb b/service/test/agama/registration_test.rb index 05b7757fef..72b79d2323 100644 --- a/service/test/agama/registration_test.rb +++ b/service/test/agama/registration_test.rb @@ -90,7 +90,7 @@ it "creates credentials file" do expect(SUSE::Connect::YaST).to receive(:create_credentials_file) - .with("test-user", "12345") + .with("test-user", "12345", "/run/agama/zypp/etc/zypp/credentials.d/SCCcredentials") subject.register("11112222", email: "test@test.com") end @@ -117,13 +117,14 @@ before do allow(subject).to receive(:credentials_from_url) - .with("https://credentials/file").and_return("credentials") + .with("https://credentials/file") + .and_return("/etc/zypp/credentials.d/product") end it "creates the credentials file" do expect(SUSE::Connect::YaST).to receive(:create_credentials_file) expect(SUSE::Connect::YaST).to receive(:create_credentials_file) - .with("test-user", "12345", "credentials") + .with("test-user", "12345", "/run/agama/zypp/etc/zypp/credentials.d/product") subject.register("11112222", email: "test@test.com") end From 3e3708c8c8f7bdce297a37ccb747a4ce98e03848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Wed, 4 Dec 2024 22:00:47 +0000 Subject: [PATCH 12/47] fix(web): add registration to Product type --- web/src/App.test.tsx | 4 ++-- web/src/components/core/ChangeProductLink.test.tsx | 2 ++ web/src/components/layout/Header.test.tsx | 7 +++++-- web/src/components/product/ProductSelectionPage.test.tsx | 2 ++ web/src/types/software.ts | 2 ++ 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index 1f806f0989..abe0834187 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -40,8 +40,8 @@ jest.mock("~/api/l10n", () => ({ updateConfig: jest.fn(), })); -const tumbleweed: Product = { id: "openSUSE", name: "openSUSE Tumbleweed" }; -const microos: Product = { id: "Leap Micro", name: "openSUSE Micro" }; +const tumbleweed: Product = { id: "openSUSE", name: "openSUSE Tumbleweed", registration: "No" }; +const microos: Product = { id: "Leap Micro", name: "openSUSE Micro", registration: "No" }; // list of available products let mockProducts: Product[]; diff --git a/web/src/components/core/ChangeProductLink.test.tsx b/web/src/components/core/ChangeProductLink.test.tsx index ddbdd369d1..bc71c1be9b 100644 --- a/web/src/components/core/ChangeProductLink.test.tsx +++ b/web/src/components/core/ChangeProductLink.test.tsx @@ -32,12 +32,14 @@ const tumbleweed: Product = { name: "openSUSE Tumbleweed", icon: "tumbleweed.svg", description: "Tumbleweed description...", + registration: "No", }; const microos: Product = { id: "MicroOS", name: "openSUSE MicroOS", icon: "MicroOS.svg", description: "MicroOS description", + registration: "No", }; let mockUseProduct: { products: Product[]; selectedProduct?: Product }; diff --git a/web/src/components/layout/Header.test.tsx b/web/src/components/layout/Header.test.tsx index 1ae19803fc..882a11e904 100644 --- a/web/src/components/layout/Header.test.tsx +++ b/web/src/components/layout/Header.test.tsx @@ -25,17 +25,20 @@ import { screen, within } from "@testing-library/react"; import { installerRender, mockRoutes } from "~/test-utils"; import Header from "./Header"; import { InstallationPhase } from "~/types/status"; +import { Product } from "~/types/software"; -const tumbleweed = { +const tumbleweed: Product = { id: "Tumbleweed", name: "openSUSE Tumbleweed", description: "Tumbleweed description...", + registration: "No", }; -const microos = { +const microos: Product = { id: "MicroOS", name: "openSUSE MicroOS", description: "MicroOS description", + registration: "No", }; let phase: InstallationPhase; diff --git a/web/src/components/product/ProductSelectionPage.test.tsx b/web/src/components/product/ProductSelectionPage.test.tsx index 4209e2da58..bb096ef626 100644 --- a/web/src/components/product/ProductSelectionPage.test.tsx +++ b/web/src/components/product/ProductSelectionPage.test.tsx @@ -33,6 +33,7 @@ const tumbleweed: Product = { name: "openSUSE Tumbleweed", icon: "tumbleweed.svg", description: "Tumbleweed description...", + registration: "No", }; const microOs: Product = { @@ -40,6 +41,7 @@ const microOs: Product = { name: "openSUSE MicroOS", icon: "microos.svg", description: "MicroOS description", + registration: "No", }; jest.mock("~/queries/software", () => ({ diff --git a/web/src/types/software.ts b/web/src/types/software.ts index 822d60855a..6a54c0b429 100644 --- a/web/src/types/software.ts +++ b/web/src/types/software.ts @@ -41,6 +41,8 @@ type Product = { description?: string; /** Product icon (e.g., "default.svg") */ icon?: string; + /** If product is registrable or not */ + registration: string; }; type PatternsSelection = { [key: string]: SelectedBy }; From 56fa368c6fab9dc8e61652d45290dcf12c4d6f70 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 4 Dec 2024 23:02:59 +0100 Subject: [PATCH 13/47] fix creating credentials --- service/lib/agama/registration.rb | 4 ++-- service/test/agama/registration_test.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb index c3618c70c5..208c8290ed 100644 --- a/service/lib/agama/registration.rb +++ b/service/lib/agama/registration.rb @@ -95,7 +95,7 @@ def register(code, email: "") @credentials_file = credentials_from_url(@service.url) if @credentials_file SUSE::Connect::YaST.create_credentials_file(login, password, - File.join(TARGET_DIR, @credentials_file)) + File.join(TARGET_DIR, credentials_path(@credentials_file))) end Y2Packager::NewRepositorySetup.instance.add_service(@service.name) @software.add_service(@service) @@ -127,7 +127,7 @@ def deregister SUSE::Connect::YaST.deactivate_system(connect_params) FileUtils.rm(GLOBAL_CREDENTIALS_PATH) # connect does not remove it itself if @credentials_file - FileUtils.rm(credentials_path(File.join(TARGET_DIR, @credentials_file))) + FileUtils.rm(File.join(TARGET_DIR, credentials_path(@credentials_file))) @credentials_file = nil end diff --git a/service/test/agama/registration_test.rb b/service/test/agama/registration_test.rb index 72b79d2323..d34255c2ba 100644 --- a/service/test/agama/registration_test.rb +++ b/service/test/agama/registration_test.rb @@ -118,13 +118,13 @@ before do allow(subject).to receive(:credentials_from_url) .with("https://credentials/file") - .and_return("/etc/zypp/credentials.d/product") + .and_return("productA") end it "creates the credentials file" do expect(SUSE::Connect::YaST).to receive(:create_credentials_file) expect(SUSE::Connect::YaST).to receive(:create_credentials_file) - .with("test-user", "12345", "/run/agama/zypp/etc/zypp/credentials.d/product") + .with("test-user", "12345", "/run/agama/zypp/etc/zypp/credentials.d/productA") subject.register("11112222", email: "test@test.com") end From 916d41e99dbacd3b3393d4b0730e638d2960fbca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Wed, 4 Dec 2024 22:03:47 +0000 Subject: [PATCH 14/47] feat(web): allow setting a route only for registrable products Actually, at this moment the flag is only used for including a link to the route in the main navigation or not depending on the Product#registration value. But the idea is to limite the navigation too in future iterations. --- web/src/components/layout/Sidebar.test.tsx | 66 +++++++++++++++++++--- web/src/components/layout/Sidebar.tsx | 4 ++ web/src/types/routes.ts | 2 + 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/web/src/components/layout/Sidebar.test.tsx b/web/src/components/layout/Sidebar.test.tsx index fa3d3c7495..346bf95317 100644 --- a/web/src/components/layout/Sidebar.test.tsx +++ b/web/src/components/layout/Sidebar.test.tsx @@ -24,25 +24,77 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import Sidebar from "./Sidebar"; +import { Product } from "~/types/software"; +import { useProduct } from "~/queries/software"; jest.mock("~/components/core/ChangeProductLink", () => () =>
ChangeProductLink Mock
); +const tw: Product = { + id: "Tumbleweed", + name: "openSUSE Tumbleweed", + registration: "No", +}; + +const sle: Product = { + id: "sle", + name: "SLE", + registration: "Mandatory", +}; + +let selectedProduct: Product; + +jest.mock("~/queries/software", () => ({ + ...jest.requireActual("~/queries/software"), + useProduct: (): ReturnType => { + return { + products: [tw, sle], + selectedProduct, + }; + }, +})); + jest.mock("~/router", () => ({ rootRoutes: () => [ { path: "/", handle: { name: "Main" } }, { path: "/l10n", handle: { name: "L10n" } }, { path: "/hidden" }, + { + path: "/registration", + handle: { name: "Registration", needsRegistrableProduct: true }, + }, ], })); describe("Sidebar", () => { - it("renders a navigation on top of root routes with handle object", () => { - installerRender(); - const mainNavigation = screen.getByRole("navigation"); - const mainNavigationLinks = within(mainNavigation).getAllByRole("link"); - expect(mainNavigationLinks.length).toBe(2); - screen.getByRole("link", { name: "Main" }); - screen.getByRole("link", { name: "L10n" }); + describe("when product is registrable", () => { + beforeEach(() => { + selectedProduct = sle; + }); + + it("renders a navigation including all root routes with handle object", () => { + installerRender(); + const mainNavigation = screen.getByRole("navigation"); + const mainNavigationLinks = within(mainNavigation).getAllByRole("link"); + expect(mainNavigationLinks.length).toBe(3); + screen.getByRole("link", { name: "Main" }); + screen.getByRole("link", { name: "L10n" }); + screen.getByRole("link", { name: "Registration" }); + }); + }); + + describe("when product is not registrable", () => { + beforeEach(() => { + selectedProduct = tw; + }); + + it("renders a navigation including all root routes with handle object, except ones set as needsRegistrableProduct", () => { + installerRender(); + const mainNavigation = screen.getByRole("navigation"); + const mainNavigationLinks = within(mainNavigation).getAllByRole("link"); + expect(mainNavigationLinks.length).toBe(2); + screen.getByRole("link", { name: "Main" }); + screen.getByRole("link", { name: "L10n" }); + }); }); it("mounts core/ChangeProductLink component", () => { diff --git a/web/src/components/layout/Sidebar.tsx b/web/src/components/layout/Sidebar.tsx index aa768d0218..2b1789c21d 100644 --- a/web/src/components/layout/Sidebar.tsx +++ b/web/src/components/layout/Sidebar.tsx @@ -27,10 +27,14 @@ import { Icon } from "~/components/layout"; import { ChangeProductLink } from "~/components/core"; import { rootRoutes } from "~/router"; import { _ } from "~/i18n"; +import { useProduct } from "~/queries/software"; const MainNavigation = (): React.ReactNode => { + const { selectedProduct: product } = useProduct(); + const links = rootRoutes().map((r) => { if (!r.handle) return null; + if (r.handle.needsRegistrableProduct && product.registration === "No") return null; // eslint-disable-next-line agama-i18n/string-literals const name = _(r.handle.name); diff --git a/web/src/types/routes.ts b/web/src/types/routes.ts index 11f2135547..fe5b85ea71 100644 --- a/web/src/types/routes.ts +++ b/web/src/types/routes.ts @@ -29,6 +29,8 @@ type RouteHandle = { title?: string; /** Icon for representing the route in some places, like a menu entry */ icon?: string; + /** Whether the route link will be rendered for registrable products only */ + needsRegistrableProduct?: boolean; }; type Route = RouteObject & { handle?: RouteHandle }; From 34d311db505410ecf082e658c5d19aa1828ca653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Wed, 4 Dec 2024 22:15:51 +0000 Subject: [PATCH 15/47] feat(web): add product registration interface An initial version that basically consists on - Main menu entry for registration - Global alert visible on all pages except the registration page and those mounted at "supportive paths" (temporary name) - Section with form for entering registration code and email - Section to display registration information when available Only basic workflow has been manually tested; unit tests will be added in upcoming commits. --- web/src/api/software.ts | 37 +++- web/src/components/core/IssuesDrawer.tsx | 7 +- web/src/components/core/Page.tsx | 10 +- web/src/components/layout/Icon.tsx | 2 + .../product/ProductRegistrationAlert.tsx | 58 ++++++ .../product/ProductRegistrationPage.tsx | 184 ++++++++++++++++++ web/src/components/product/index.ts | 2 + web/src/queries/software.ts | 61 ++++++ web/src/router.js | 2 + web/src/routes/paths.ts | 15 +- web/src/routes/registration.tsx | 40 ++++ web/src/types/software.ts | 14 +- web/src/utils.js | 6 + 13 files changed, 429 insertions(+), 9 deletions(-) create mode 100644 web/src/components/product/ProductRegistrationAlert.tsx create mode 100644 web/src/components/product/ProductRegistrationPage.tsx create mode 100644 web/src/routes/registration.tsx diff --git a/web/src/api/software.ts b/web/src/api/software.ts index 3be92e58ff..82f8650dc5 100644 --- a/web/src/api/software.ts +++ b/web/src/api/software.ts @@ -20,8 +20,14 @@ * find current contact information at www.suse.com. */ -import { Pattern, Product, SoftwareConfig, SoftwareProposal } from "~/types/software"; -import { get, put } from "~/api/http"; +import { + Pattern, + Product, + SoftwareConfig, + RegistrationInfo, + SoftwareProposal, +} from "~/types/software"; +import { del, get, post, put } from "~/api/http"; /** * Returns the software configuration @@ -38,6 +44,11 @@ const fetchProposal = (): Promise => get("/api/software/propos */ const fetchProducts = (): Promise => get("/api/software/products"); +/** + * Returns an object with the registration info + */ +const fetchRegistration = (): Promise => get("/api/software/registration"); + /** * Returns the list of patterns for the selected product */ @@ -50,4 +61,24 @@ const fetchPatterns = (): Promise => get("/api/software/patterns"); */ const updateConfig = (config: SoftwareConfig) => put("/api/software/config", config); -export { fetchConfig, fetchPatterns, fetchProposal, fetchProducts, updateConfig }; +/** + * Request registration of selected product with given key + */ +const register = ({ key, email }: { key: string; email?: string }) => + post("/api/software/registration", { key, email }); + +/** + * Request deregistering selected product + */ +const deregister = () => del("/api/software/registration"); + +export { + fetchConfig, + fetchPatterns, + fetchProposal, + fetchProducts, + fetchRegistration, + updateConfig, + register, + deregister, +}; diff --git a/web/src/components/core/IssuesDrawer.tsx b/web/src/components/core/IssuesDrawer.tsx index 4a0ecfa7fe..67a8fdd94d 100644 --- a/web/src/components/core/IssuesDrawer.tsx +++ b/web/src/components/core/IssuesDrawer.tsx @@ -49,6 +49,8 @@ const IssuesDrawer = forwardRef(({ onClose }: { onClose: () => void }, ref) => { users: _("Users"), storage: _("Storage"), software: _("Software"), + // FIXME: Registration or Product? + product: _("Registration"), }; if (issues.isEmpty || phase === InstallationPhase.Install) return; @@ -65,13 +67,16 @@ const IssuesDrawer = forwardRef(({ onClose }: { onClose: () => void }, ref) => {

{Object.entries(issuesByScope).map(([scope, issues], idx) => { if (issues.length === 0) return null; + // FIXME: address this better or use the /product(s)? namespace instead of + // /registration. + const section = scope === "product" ? "registration" : scope; const ariaLabelId = `${scope}-issues-section`; return (

- + {scopeHeaders[scope]}

diff --git a/web/src/components/core/Page.tsx b/web/src/components/core/Page.tsx index 799f2413eb..5ddf83e5fa 100644 --- a/web/src/components/core/Page.tsx +++ b/web/src/components/core/Page.tsx @@ -40,6 +40,7 @@ import { TitleProps, } from "@patternfly/react-core"; import { Flex } from "~/components/layout"; +import { ProductRegistrationAlert } from "~/components/product"; import { _ } from "~/i18n"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import flexStyles from "@patternfly/react-styles/css/utilities/Flex/flex"; @@ -279,9 +280,12 @@ const Submit = ({ children, ...props }: SubmitActionProps) => { * @see [Patternfly Page/PageSection](https://www.patternfly.org/components/page#pagesection) */ const Content = ({ children, ...pageSectionProps }: React.PropsWithChildren) => ( - - {children} - + <> + + + {children} + + ); /** diff --git a/web/src/components/layout/Icon.tsx b/web/src/components/layout/Icon.tsx index cbdc3ce110..09ab2d872b 100644 --- a/web/src/components/layout/Icon.tsx +++ b/web/src/components/layout/Icon.tsx @@ -26,6 +26,7 @@ import React from "react"; // icons location. Check the tsconfig.json file to see its value. import AddAPhoto from "@icons/add_a_photo.svg?component"; import Apps from "@icons/apps.svg?component"; +import AppRegistration from "@icons/app_registration.svg?component"; import Badge from "@icons/badge.svg?component"; import Backspace from "@icons/backspace.svg?component"; import CheckCircle from "@icons/check_circle.svg?component"; @@ -91,6 +92,7 @@ import { SiLinux } from "@icons-pack/react-simple-icons"; const icons = { add_a_photo: AddAPhoto, apps: Apps, + app_registration: AppRegistration, badge: Badge, backspace: Backspace, check_circle: CheckCircle, diff --git a/web/src/components/product/ProductRegistrationAlert.tsx b/web/src/components/product/ProductRegistrationAlert.tsx new file mode 100644 index 0000000000..1681daaf92 --- /dev/null +++ b/web/src/components/product/ProductRegistrationAlert.tsx @@ -0,0 +1,58 @@ +/* + * Copyright (c) [2023-2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { Alert, Flex } from "@patternfly/react-core"; +import { useLocation } from "react-router-dom"; +import { Link } from "~/components/core"; +import { useProduct, useRegistration } from "~/queries/software"; +import { REGISTRATION, SUPPORTIVE_PATHS } from "~/routes/paths"; +import { isEmpty } from "~/utils"; +import { _ } from "~/i18n"; +import { sprintf } from "sprintf-js"; + +export default function ProductRegistrationAlert() { + const location = useLocation(); + const { selectedProduct: product } = useProduct(); + const registration = useRegistration(); + + if ([...SUPPORTIVE_PATHS, REGISTRATION.root].includes(location.pathname)) return; + if (product.registration === "No" || !isEmpty(registration.key)) return; + + return ( + + +

+ {sprintf( + _( + "%s has to be registered because ", + ), + product.name, + )} +

+ + {_("Register it now")} + +
+
+ ); +} diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx new file mode 100644 index 0000000000..ca6ea4ed94 --- /dev/null +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -0,0 +1,184 @@ +/* + * Copyright (c) [2023-2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React, { useState } from "react"; +import { + ActionGroup, + Alert, + Button, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Flex, + Form, + FormGroup, + Grid, + Stack, + TextInput, +} from "@patternfly/react-core"; +import { Page, PasswordInput } from "~/components/core"; +import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; +import spacingStyles from "@patternfly/react-styles/css/utilities/Spacing/spacing"; +import { + useProduct, + useRegistration, + useRegisterMutation, + useDeregisterMutation, +} from "~/queries/software"; +import { isEmpty, mask } from "~/utils"; +import { _ } from "~/i18n"; +import { sprintf } from "sprintf-js"; + +const FORM_ID = "productRegistration"; +const KEY_LABEL = _("Registration code"); +const EMAIL_LABEL = "Email"; + +const RegisteredProductSection = () => { + const { selectedProduct: product } = useProduct(); + const { mutate: deregister } = useDeregisterMutation(); + const registration = useRegistration(); + const [showCode, setShowCode] = useState(false); + const toggleCodeVisibility = () => setShowCode(!showCode); + + const footer = _("For using a different registration code, please %s the product first."); + const deregisterButtonLabel = _("deregister"); + const [footerStart, footerEnd] = footer.split("%s"); + + return ( + + {footerStart}{" "} + {" "} + {footerEnd} +

+ } + > + + + {KEY_LABEL} + + + {showCode ? registration.key : mask(registration.key)} + + + + {!isEmpty(registration.email) && ( + <> + {EMAIL_LABEL} + {registration.email} + + )} + + +
+ ); +}; + +const RegistrationFormSection = () => { + const { mutate: register } = useRegisterMutation(); + const [key, setKey] = useState(""); + const [email, setEmail] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + // FIXME: use the right type for AxiosResponse + const onRegisterError = ({ response }) => { + const originalMessage = response.data.message; + const from = originalMessage.indexOf(":") + 1; + setError(originalMessage.slice(from).trim()); + }; + + const submit = async (e: React.SyntheticEvent) => { + e.preventDefault(); + setError(null); + setLoading(true); + register({ key, email }, { onError: onRegisterError, onSettled: () => setLoading(false) }); + }; + + // TODO: adjust texts based of registration "type", mandatory or optional + + return ( + +
+ {error && } + + setKey(v)} /> + + + {EMAIL_LABEL} {_("(optional)")} + + } + > + setEmail(v)} /> + + + + + + +
+ ); +}; + +export default function ProductRegistrationPage() { + const { selectedProduct: product } = useProduct(); + const registration = useRegistration(); + + // TODO: render something meaningful instead? "Product not registrable"? + if (product.registration === "No") return; + + return ( + + +

{_("Registration")}

+
+ + + + + {isEmpty(registration.key) ? : } + + + +
+ ); +} diff --git a/web/src/components/product/index.ts b/web/src/components/product/index.ts index 1340569f37..fc63c951d0 100644 --- a/web/src/components/product/index.ts +++ b/web/src/components/product/index.ts @@ -22,3 +22,5 @@ export { default as ProductSelectionPage } from "./ProductSelectionPage"; export { default as ProductSelectionProgress } from "./ProductSelectionProgress"; +export { default as ProductRegistrationPage } from "./ProductRegistrationPage"; +export { default as ProductRegistrationAlert } from "./ProductRegistrationAlert"; diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts index 6030489bd7..287154283d 100644 --- a/web/src/queries/software.ts +++ b/web/src/queries/software.ts @@ -33,15 +33,19 @@ import { Pattern, PatternsSelection, Product, + RegistrationInfo, SelectedBy, SoftwareConfig, SoftwareProposal, } from "~/types/software"; import { + deregister, fetchConfig, fetchPatterns, fetchProducts, fetchProposal, + fetchRegistration, + register, updateConfig, } from "~/api/software"; import { QueryHookOptions } from "~/types/queries"; @@ -80,6 +84,14 @@ const selectedProductQuery = () => ({ queryFn: () => fetchConfig().then(({ product }) => product), }); +/** + * Query to retrieve registration info + */ +const registrationQuery = () => ({ + queryKey: ["software/registration"], + queryFn: fetchRegistration, +}); + /** * Query to retrieve available patterns */ @@ -111,6 +123,44 @@ const useConfigMutation = () => { return useMutation(query); }; +/** + * Hook that builds a mutation for registering a product + * + * @note it would trigger a general probing as a side-effect when mutation + * includes a product. + */ +const useRegisterMutation = () => { + const queryClient = useQueryClient(); + + const query = { + mutationFn: register, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["software/registration"] }); + startProbing(); + }, + }; + return useMutation(query); +}; + +/** + * Hook that builds a mutation for deregistering a product + * + * @note it would trigger a general probing as a side-effect when mutation + * includes a product. + */ +const useDeregisterMutation = () => { + const queryClient = useQueryClient(); + + const query = { + mutationFn: deregister, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["software/registration"] }); + startProbing(); + }, + }; + return useMutation(query); +}; + /** * Returns available products and selected one, if any */ @@ -172,6 +222,14 @@ const useProposal = (): SoftwareProposal => { return proposal; }; +/** + * Returns registration info + */ +const useRegistration = (): RegistrationInfo => { + const { data: registration } = useSuspenseQuery(registrationQuery()); + return registration; +}; + /** * Hook that returns a useEffect to listen for software proposal events * @@ -226,4 +284,7 @@ export { useProductChanges, useProposal, useProposalChanges, + useRegistration, + useRegisterMutation, + useDeregisterMutation, }; diff --git a/web/src/router.js b/web/src/router.js index f949a76652..314ef63116 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -30,6 +30,7 @@ import { OverviewPage } from "~/components/overview"; import l10nRoutes from "~/routes/l10n"; import networkRoutes from "~/routes/network"; import productsRoutes from "~/routes/products"; +import registrationRoutes from "~/routes/registration"; import storageRoutes from "~/routes/storage"; import softwareRoutes from "~/routes/software"; import usersRoutes from "~/routes/users"; @@ -43,6 +44,7 @@ const rootRoutes = () => [ element: , handle: { name: N_("Overview"), icon: "list_alt" }, }, + registrationRoutes(), l10nRoutes(), networkRoutes(), storageRoutes(), diff --git a/web/src/routes/paths.ts b/web/src/routes/paths.ts index 22cdba06e2..efe67522a4 100644 --- a/web/src/routes/paths.ts +++ b/web/src/routes/paths.ts @@ -39,6 +39,10 @@ const PRODUCT = { progress: "/products/progress", }; +const REGISTRATION = { + root: "/registration", +}; + const ROOT = { root: "/", login: "/login", @@ -78,4 +82,13 @@ const STORAGE = { }, }; -export { L10N, NETWORK, PRODUCT, ROOT, SOFTWARE, STORAGE, USER }; +const SUPPORTIVE_PATHS = [ + ROOT.login, + PRODUCT.changeProduct, + PRODUCT.progress, + ROOT.installationProgress, + ROOT.installationFinished, + USER.rootUser.edit, +]; + +export { L10N, NETWORK, PRODUCT, REGISTRATION, ROOT, SOFTWARE, STORAGE, USER, SUPPORTIVE_PATHS }; diff --git a/web/src/routes/registration.tsx b/web/src/routes/registration.tsx new file mode 100644 index 0000000000..db27ad5037 --- /dev/null +++ b/web/src/routes/registration.tsx @@ -0,0 +1,40 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { ProductRegistrationPage } from "~/components/product"; +import { Route } from "~/types/routes"; +import { REGISTRATION as PATHS } from "~/routes/paths"; +import { N_ } from "~/i18n"; + +const routes = (): Route => ({ + path: PATHS.root, + handle: { name: N_("Registration"), icon: "app_registration", needsRegistrableProduct: true }, + children: [ + { + index: true, + element: , + }, + ], +}); + +export default routes; diff --git a/web/src/types/software.ts b/web/src/types/software.ts index 6a54c0b429..59f5dcaeef 100644 --- a/web/src/types/software.ts +++ b/web/src/types/software.ts @@ -78,5 +78,17 @@ type Pattern = { selectedBy?: SelectedBy; }; +type RegistrationInfo = { + key: string; + email?: string; +}; + export { SelectedBy }; -export type { Pattern, PatternsSelection, Product, SoftwareConfig, SoftwareProposal }; +export type { + Pattern, + PatternsSelection, + Product, + SoftwareConfig, + RegistrationInfo, + SoftwareProposal, +}; diff --git a/web/src/utils.js b/web/src/utils.js index 50feed2b79..418de947a1 100644 --- a/web/src/utils.js +++ b/web/src/utils.js @@ -420,6 +420,11 @@ const timezoneTime = (timezone, { date = new Date() }) => { } }; +const mask = (value, visible = 4, character = "*") => { + const regex = new RegExp(`.(?=(.{${visible}}))`, "g"); + return value.replace(regex, character); +}; + export { noop, identity, @@ -441,4 +446,5 @@ export { remoteConnection, slugify, timezoneTime, + mask, }; From 0063a08dc465e603e3d9aa2f0fc6ce876af86f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Wed, 4 Dec 2024 22:30:49 +0000 Subject: [PATCH 16/47] fix(web): use "supportive paths" for InstallButton Certain elements must not be rendered on paths that do not use the "FullLayout". Other paths, considered "accessory" paths, have been temporarily grouped under the "SUPPORTIVE_PATHS" constant to reduce code duplication when excluding elements from them. Although the approach needs improvement, starting with the naming, let's use it where needed meanwhile. --- web/src/components/core/InstallButton.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/web/src/components/core/InstallButton.tsx b/web/src/components/core/InstallButton.tsx index 5a6d44333d..93826fe188 100644 --- a/web/src/components/core/InstallButton.tsx +++ b/web/src/components/core/InstallButton.tsx @@ -26,7 +26,7 @@ import { Popup } from "~/components/core"; import { startInstallation } from "~/api/manager"; import { useAllIssues } from "~/queries/issues"; import { useLocation } from "react-router-dom"; -import { PRODUCT, ROOT, USER } from "~/routes/paths"; +import { SUPPORTIVE_PATHS } from "~/routes/paths"; import { _ } from "~/i18n"; import { Icon } from "../layout"; @@ -38,14 +38,6 @@ import { Icon } from "../layout"; * defining the root authentication for the fisrt time, nor when the installer * is setting the chosen product. * */ -const EXCLUDED_FROM = [ - ROOT.login, - PRODUCT.changeProduct, - PRODUCT.progress, - ROOT.installationProgress, - ROOT.installationFinished, - USER.rootUser.edit, -]; const InstallConfirmationPopup = ({ onAccept, onClose }) => { return ( @@ -90,7 +82,7 @@ const InstallButton = ( const location = useLocation(); const hasIssues = !issues.isEmpty; - if (EXCLUDED_FROM.includes(location.pathname)) return; + if (SUPPORTIVE_PATHS.includes(location.pathname)) return; const { onClickWithIssues, ...buttonProps } = props; const open = async () => setIsOpen(true); From 64d27a343b712fb8bf728b91cf36be212059718d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 4 Dec 2024 22:52:00 +0000 Subject: [PATCH 17/47] fix(service): do not initialize the target in add_service --- service/lib/agama/software/manager.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb index f76046ea7a..3312d5138f 100644 --- a/service/lib/agama/software/manager.rb +++ b/service/lib/agama/software/manager.rb @@ -338,8 +338,6 @@ def registration # code is based on https://github.com/yast/yast-registration/blob/master/src/lib/registration/sw_mgmt.rb#L365 # rubocop:disable Metrics/AbcSize def add_service(service) - # init repos, so we are sure we operate on "/" and have GPG imported - initialize_target # save repositories before refreshing added services (otherwise # pkg-bindings will treat them as removed by the service refresh and # unload them) From 656091ee556e2b468211d75215c0156a5865e3f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 4 Dec 2024 23:06:06 +0000 Subject: [PATCH 18/47] fix(web): temporarily disable a TS problem --- web/src/components/product/ProductRegistrationPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index ca6ea4ed94..463ba123e8 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -120,6 +120,7 @@ const RegistrationFormSection = () => { e.preventDefault(); setError(null); setLoading(true); + // @ts-ignore register({ key, email }, { onError: onRegisterError, onSettled: () => setLoading(false) }); }; From 945b00ff3757bdfc6002fcc60cfcbfa4320054f7 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 5 Dec 2024 10:17:36 +0100 Subject: [PATCH 19/47] fix reading global credentials file --- service/lib/agama/registration.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb index 208c8290ed..655dc96c1d 100644 --- a/service/lib/agama/registration.rb +++ b/service/lib/agama/registration.rb @@ -34,7 +34,9 @@ class Registration TARGET_DIR = "/run/agama/zypp" private_constant :TARGET_DIR - GLOBAL_CREDENTIALS_PATH = File.join(TARGET_DIR, + # FIXME: it should use TARGET_DIR instead of "/", but connect failed to read it even + # if fs_root passed as client params. Check with SCC guys why. + GLOBAL_CREDENTIALS_PATH = File.join("/", SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE) private_constant :GLOBAL_CREDENTIALS_PATH From 35f303f7dc977165eaf7415bbed1996a508f48c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 5 Dec 2024 09:49:12 +0000 Subject: [PATCH 20/47] fix(web): mount registration alert only in valid paths Otherwise, it will trigger the useProduct hook which might not be available yet like in the case of the login path, in which the QueryClient has not been initialized yet. --- web/src/components/core/Page.tsx | 26 ++++++++++++------- .../product/ProductRegistrationAlert.tsx | 2 ++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/web/src/components/core/Page.tsx b/web/src/components/core/Page.tsx index 5ddf83e5fa..0e9ad30de9 100644 --- a/web/src/components/core/Page.tsx +++ b/web/src/components/core/Page.tsx @@ -44,8 +44,9 @@ import { ProductRegistrationAlert } from "~/components/product"; import { _ } from "~/i18n"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import flexStyles from "@patternfly/react-styles/css/utilities/Flex/flex"; -import { To, useNavigate } from "react-router-dom"; +import { To, useLocation, useNavigate } from "react-router-dom"; import { isEmpty, isObject } from "~/utils"; +import { REGISTRATION, SUPPORTIVE_PATHS } from "~/routes/paths"; /** * Props accepted by Page.Section @@ -279,14 +280,21 @@ const Submit = ({ children, ...props }: SubmitActionProps) => { * * @see [Patternfly Page/PageSection](https://www.patternfly.org/components/page#pagesection) */ -const Content = ({ children, ...pageSectionProps }: React.PropsWithChildren) => ( - <> - - - {children} - - -); +const Content = ({ children, ...pageSectionProps }: React.PropsWithChildren) => { + const location = useLocation(); + const mountRegistrationAlert = ![...SUPPORTIVE_PATHS, REGISTRATION.root].includes( + location.pathname, + ); + + return ( + <> + {mountRegistrationAlert && } + + {children} + + + ); +}; /** * Component for structuring an Agama page, built on top of PF/Page/PageGroup. diff --git a/web/src/components/product/ProductRegistrationAlert.tsx b/web/src/components/product/ProductRegistrationAlert.tsx index 1681daaf92..241ccaf6b6 100644 --- a/web/src/components/product/ProductRegistrationAlert.tsx +++ b/web/src/components/product/ProductRegistrationAlert.tsx @@ -35,6 +35,8 @@ export default function ProductRegistrationAlert() { const { selectedProduct: product } = useProduct(); const registration = useRegistration(); + // NOTE: it shouldn't be mounted in these paths, but let's prevent rendering + // if so just in case. if ([...SUPPORTIVE_PATHS, REGISTRATION.root].includes(location.pathname)) return; if (product.registration === "No" || !isEmpty(registration.key)) return; From f385dda1e694836ee58ced5676c9b0bfcadc260b Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 5 Dec 2024 11:43:10 +0100 Subject: [PATCH 21/47] adapt tests for workaround --- service/test/agama/registration_test.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/service/test/agama/registration_test.rb b/service/test/agama/registration_test.rb index d34255c2ba..c62e258a61 100644 --- a/service/test/agama/registration_test.rb +++ b/service/test/agama/registration_test.rb @@ -90,7 +90,9 @@ it "creates credentials file" do expect(SUSE::Connect::YaST).to receive(:create_credentials_file) - .with("test-user", "12345", "/run/agama/zypp/etc/zypp/credentials.d/SCCcredentials") + .with("test-user", "12345", "/etc/zypp/credentials.d/SCCcredentials") + # TODO: when fixing suse-connect read of fsroot + # .with("test-user", "12345", "/run/agama/zypp/etc/zypp/credentials.d/SCCcredentials") subject.register("11112222", email: "test@test.com") end From f532b37d1c2dbb59c15d3e7b62cb811629d638a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 5 Dec 2024 12:11:36 +0000 Subject: [PATCH 22/47] fix: use registration requirement capitalization --- rust/agama-lib/src/software/model.rs | 1 + web/src/components/layout/Sidebar.test.tsx | 2 +- web/src/types/registration.ts | 2 +- web/src/types/software.ts | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/rust/agama-lib/src/software/model.rs b/rust/agama-lib/src/software/model.rs index 1678334473..32618542f1 100644 --- a/rust/agama-lib/src/software/model.rs +++ b/rust/agama-lib/src/software/model.rs @@ -60,6 +60,7 @@ pub struct RegistrationInfo { utoipa::ToSchema, )] #[strum(serialize_all = "camelCase")] +#[serde(rename_all = "camelCase")] pub enum RegistrationRequirement { /// Product does not require registration #[default] diff --git a/web/src/components/layout/Sidebar.test.tsx b/web/src/components/layout/Sidebar.test.tsx index 346bf95317..fb47f7c809 100644 --- a/web/src/components/layout/Sidebar.test.tsx +++ b/web/src/components/layout/Sidebar.test.tsx @@ -38,7 +38,7 @@ const tw: Product = { const sle: Product = { id: "sle", name: "SLE", - registration: "Mandatory", + registration: "mandatory", }; let selectedProduct: Product; diff --git a/web/src/types/registration.ts b/web/src/types/registration.ts index 4e70d22974..bb31e80435 100644 --- a/web/src/types/registration.ts +++ b/web/src/types/registration.ts @@ -22,7 +22,7 @@ type Registration = { /** Registration requirement (i.e., "not-required", "optional", "mandatory") */ - requirement: string; + requirement: "no" | "optional" | "mandatory"; /** Registration code, if any */ code?: string; /** Registration email, if any */ diff --git a/web/src/types/software.ts b/web/src/types/software.ts index 59f5dcaeef..f35a9a609e 100644 --- a/web/src/types/software.ts +++ b/web/src/types/software.ts @@ -42,7 +42,7 @@ type Product = { /** Product icon (e.g., "default.svg") */ icon?: string; /** If product is registrable or not */ - registration: string; + registration: "no" | "optional" | "mandatory"; }; type PatternsSelection = { [key: string]: SelectedBy }; From 1d6c5a933ecd806790088f1f2d669e13110864e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 5 Dec 2024 12:32:25 +0000 Subject: [PATCH 23/47] fix(web): adapt the registration alert condition --- web/src/components/product/ProductRegistrationAlert.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/product/ProductRegistrationAlert.tsx b/web/src/components/product/ProductRegistrationAlert.tsx index 241ccaf6b6..7a119a6ebe 100644 --- a/web/src/components/product/ProductRegistrationAlert.tsx +++ b/web/src/components/product/ProductRegistrationAlert.tsx @@ -38,7 +38,7 @@ export default function ProductRegistrationAlert() { // NOTE: it shouldn't be mounted in these paths, but let's prevent rendering // if so just in case. if ([...SUPPORTIVE_PATHS, REGISTRATION.root].includes(location.pathname)) return; - if (product.registration === "No" || !isEmpty(registration.key)) return; + if (product.registration === "no" || !isEmpty(registration.key)) return; return ( From 0bb69a75300624bca26cc0cb26ef13b4a2d981cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 5 Dec 2024 12:40:10 +0000 Subject: [PATCH 24/47] fix(web): show registration alert in all sections But not rendering the link to the registration section when the user is already there. This commit also add the missing unit tests, covering all scenarios implemented until now. --- web/src/components/core/Page.tsx | 6 +- .../product/ProductRegistrationAlert.test.tsx | 134 ++++++++++++++++++ .../product/ProductRegistrationAlert.tsx | 32 ++--- 3 files changed, 152 insertions(+), 20 deletions(-) create mode 100644 web/src/components/product/ProductRegistrationAlert.test.tsx diff --git a/web/src/components/core/Page.tsx b/web/src/components/core/Page.tsx index 0e9ad30de9..f72d6ba171 100644 --- a/web/src/components/core/Page.tsx +++ b/web/src/components/core/Page.tsx @@ -46,7 +46,7 @@ import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import flexStyles from "@patternfly/react-styles/css/utilities/Flex/flex"; import { To, useLocation, useNavigate } from "react-router-dom"; import { isEmpty, isObject } from "~/utils"; -import { REGISTRATION, SUPPORTIVE_PATHS } from "~/routes/paths"; +import { SUPPORTIVE_PATHS } from "~/routes/paths"; /** * Props accepted by Page.Section @@ -282,9 +282,7 @@ const Submit = ({ children, ...props }: SubmitActionProps) => { */ const Content = ({ children, ...pageSectionProps }: React.PropsWithChildren) => { const location = useLocation(); - const mountRegistrationAlert = ![...SUPPORTIVE_PATHS, REGISTRATION.root].includes( - location.pathname, - ); + const mountRegistrationAlert = !SUPPORTIVE_PATHS.includes(location.pathname); return ( <> diff --git a/web/src/components/product/ProductRegistrationAlert.test.tsx b/web/src/components/product/ProductRegistrationAlert.test.tsx new file mode 100644 index 0000000000..856e47737a --- /dev/null +++ b/web/src/components/product/ProductRegistrationAlert.test.tsx @@ -0,0 +1,134 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { installerRender, mockRoutes } from "~/test-utils"; +import ProductRegistrationAlert from "./ProductRegistrationAlert"; +import { Product, RegistrationInfo } from "~/types/software"; +import { useProduct, useRegistration } from "~/queries/software"; +import { PRODUCT, REGISTRATION, ROOT, USER } from "~/routes/paths"; + +jest.mock("~/components/core/ChangeProductLink", () => () =>
ChangeProductLink Mock
); + +const tw: Product = { + id: "Tumbleweed", + name: "openSUSE Tumbleweed", + registration: "no", +}; + +const sle: Product = { + id: "sle", + name: "SLE", + registration: "mandatory", +}; + +let selectedProduct: Product; +let registrationInfoMock: RegistrationInfo; + +jest.mock("~/queries/software", () => ({ + ...jest.requireActual("~/queries/software"), + useRegistration: (): ReturnType => registrationInfoMock, + useProduct: (): ReturnType => { + return { + products: [tw, sle], + selectedProduct, + }; + }, +})); + +const rendersNothingInSomePaths = () => { + describe.each([ + ["login", ROOT.login], + ["product selection", PRODUCT.changeProduct], + ["product selection progress", PRODUCT.progress], + ["installation progress", ROOT.installationProgress], + ["installation finished", ROOT.installationFinished], + ["root authentication", USER.rootUser.edit], + ])(`but at %s path`, (_, path) => { + beforeEach(() => { + mockRoutes(path); + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); +}; + +describe("ProductRegistrationAlert", () => { + describe("when product is registrable and registration code is not set", () => { + beforeEach(() => { + selectedProduct = sle; + registrationInfoMock = { key: "", email: "" }; + }); + + rendersNothingInSomePaths(); + + it("renders an alert warning about registration required", () => { + installerRender(); + screen.getByRole("heading", { + name: /Warning alert:.*must be registered/, + }); + const link = screen.getByRole("link", { name: "Register it now" }); + expect(link).toHaveAttribute("href", REGISTRATION.root); + }); + + describe("but at registration path already", () => { + beforeEach(() => { + mockRoutes(REGISTRATION.root); + }); + + it("does not render the link to registration", () => { + installerRender(); + screen.getByRole("heading", { + name: /Warning alert:.*must be registered/, + }); + expect(screen.queryAllByRole("link")).toEqual([]); + }); + }); + }); + + describe("when product is registrable and registration code is already set", () => { + beforeEach(() => { + selectedProduct = sle; + registrationInfoMock = { key: "INTERNAL-USE-ONLY-1234-5678", email: "" }; + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); + + describe("when product is not registrable", () => { + beforeEach(() => { + selectedProduct = tw; + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); +}); diff --git a/web/src/components/product/ProductRegistrationAlert.tsx b/web/src/components/product/ProductRegistrationAlert.tsx index 7a119a6ebe..4f09c30229 100644 --- a/web/src/components/product/ProductRegistrationAlert.tsx +++ b/web/src/components/product/ProductRegistrationAlert.tsx @@ -21,7 +21,7 @@ */ import React from "react"; -import { Alert, Flex } from "@patternfly/react-core"; +import { Alert } from "@patternfly/react-core"; import { useLocation } from "react-router-dom"; import { Link } from "~/components/core"; import { useProduct, useRegistration } from "~/queries/software"; @@ -30,6 +30,18 @@ import { isEmpty } from "~/utils"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; +const LinkToRegistration = () => { + const location = useLocation(); + + if (location.pathname === REGISTRATION.root) return; + + return ( + + {_("Register it now")} + + ); +}; + export default function ProductRegistrationAlert() { const location = useLocation(); const { selectedProduct: product } = useProduct(); @@ -37,24 +49,12 @@ export default function ProductRegistrationAlert() { // NOTE: it shouldn't be mounted in these paths, but let's prevent rendering // if so just in case. - if ([...SUPPORTIVE_PATHS, REGISTRATION.root].includes(location.pathname)) return; + if (SUPPORTIVE_PATHS.includes(location.pathname)) return; if (product.registration === "no" || !isEmpty(registration.key)) return; return ( - - -

- {sprintf( - _( - "%s has to be registered because ", - ), - product.name, - )} -

- - {_("Register it now")} - -
+ + ); } From 42ad4a225f6ad5a604c05f01e40b35312b9b2d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 5 Dec 2024 12:46:35 +0000 Subject: [PATCH 25/47] fix(web): adapt broken registration conditions To use the proper value according to the recently updated type. Basically using "no" instead of "No". --- web/src/components/layout/Sidebar.tsx | 2 +- web/src/components/product/ProductRegistrationPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/components/layout/Sidebar.tsx b/web/src/components/layout/Sidebar.tsx index 2b1789c21d..5d446130d9 100644 --- a/web/src/components/layout/Sidebar.tsx +++ b/web/src/components/layout/Sidebar.tsx @@ -34,7 +34,7 @@ const MainNavigation = (): React.ReactNode => { const links = rootRoutes().map((r) => { if (!r.handle) return null; - if (r.handle.needsRegistrableProduct && product.registration === "No") return null; + if (r.handle.needsRegistrableProduct && product.registration === "no") return null; // eslint-disable-next-line agama-i18n/string-literals const name = _(r.handle.name); diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index 463ba123e8..b776faecb2 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -165,7 +165,7 @@ export default function ProductRegistrationPage() { const registration = useRegistration(); // TODO: render something meaningful instead? "Product not registrable"? - if (product.registration === "No") return; + if (product.registration === "no") return; return ( From a923a262082f46522483ca60c7cbf4cde5d35326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 5 Dec 2024 12:56:46 +0000 Subject: [PATCH 26/47] fix(web): update tests for core/Page.Content To make them work again now that it is making use of useLocation ReactRouterDom hook and to ensure it does not try to mount ProductRegistrationAlert component at certain paths. --- web/src/components/core/Page.test.tsx | 32 +++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/web/src/components/core/Page.test.tsx b/web/src/components/core/Page.test.tsx index be2515d2a3..42a73fb767 100644 --- a/web/src/components/core/Page.test.tsx +++ b/web/src/components/core/Page.test.tsx @@ -22,12 +22,17 @@ import React from "react"; import { screen, within } from "@testing-library/react"; -import { plainRender, mockNavigateFn } from "~/test-utils"; +import { plainRender, mockNavigateFn, mockRoutes, installerRender } from "~/test-utils"; import { Page } from "~/components/core"; import { _ } from "~/i18n"; +import { PRODUCT, ROOT, USER } from "~/routes/paths"; let consoleErrorSpy: jest.SpyInstance; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlertMock
+)); + describe("Page", () => { beforeAll(() => { consoleErrorSpy = jest.spyOn(console, "error"); @@ -93,10 +98,33 @@ describe("Page", () => { describe("Page.Content", () => { it("renders a node that fills all the available space", () => { - plainRender({_("The Content")}); + installerRender({_("The Content")}); const content = screen.getByText("The Content"); expect(content.classList.contains("pf-m-fill")).toBe(true); }); + + it("mounts a ProductRegistrationAlert", () => { + installerRender(); + screen.getByText("ProductRegistrationAlertMock"); + }); + + describe.each([ + ["login", ROOT.login], + ["product selection", PRODUCT.changeProduct], + ["product selection progress", PRODUCT.progress], + ["installation progress", ROOT.installationProgress], + ["installation finished", ROOT.installationFinished], + ["root authentication", USER.rootUser.edit], + ])(`but at %s path`, (_, path) => { + beforeEach(() => { + mockRoutes(path); + }); + + it("does not mount ProductRegistrationAlert", () => { + const { container } = installerRender(); + expect(screen.queryByText("ProductRegistrationAlertMock")).toBeNull(); + }); + }); }); describe("Page.Cancel", () => { From 564d7cf9009346814d55e6286c7f9335fe8049e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 5 Dec 2024 15:11:50 +0000 Subject: [PATCH 27/47] fix(web): add basic tests for registration page --- .../product/ProductRegistrationPage.test.tsx | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 web/src/components/product/ProductRegistrationPage.test.tsx diff --git a/web/src/components/product/ProductRegistrationPage.test.tsx b/web/src/components/product/ProductRegistrationPage.test.tsx new file mode 100644 index 0000000000..8e8832b96d --- /dev/null +++ b/web/src/components/product/ProductRegistrationPage.test.tsx @@ -0,0 +1,175 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { installerRender } from "~/test-utils"; +import ProductRegistrationPage from "./ProductRegistrationPage"; +import { Product, RegistrationInfo } from "~/types/software"; +import { useProduct, useRegistration } from "~/queries/software"; + +const tw: Product = { + id: "Tumbleweed", + name: "openSUSE Tumbleweed", + registration: "no", +}; + +const sle: Product = { + id: "sle", + name: "SLE", + registration: "mandatory", +}; + +let selectedProduct: Product; +let registrationInfoMock: RegistrationInfo; +const registerMutationMock = jest.fn(); +const deregisterMutationMock = jest.fn(); + +jest.mock("~/queries/software", () => ({ + ...jest.requireActual("~/queries/software"), + useRegisterMutation: () => ({ mutate: registerMutationMock }), + useDeregisterMutation: () => ({ mutate: deregisterMutationMock }), + useRegistration: (): ReturnType => registrationInfoMock, + useProduct: (): ReturnType => { + return { + products: [tw, sle], + selectedProduct, + }; + }, +})); + +describe("ProductRegistrationPage", () => { + describe("when selected product is not registrable", () => { + beforeEach(() => { + selectedProduct = tw; + registrationInfoMock = { key: "", email: "" }; + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); + + describe("when selected product is registrable and registration code is not set", () => { + beforeEach(() => { + selectedProduct = sle; + registrationInfoMock = { key: "", email: "" }; + }); + + it("renders ProductRegistrationAlert component", () => { + installerRender(); + screen.getByText("Warning alert:"); + }); + + it("renders a form to allow user registering the product", async () => { + const { user } = installerRender(); + const registrationCodeInput = screen.getByLabelText("Registration code"); + const emailInput = screen.getByRole("textbox", { name: /Email/ }); + const submitButton = screen.getByRole("button", { name: "Register" }); + + await user.type(registrationCodeInput, "INTERNAL-USE-ONLY-1234-5678"); + await user.type(emailInput, "example@company.test"); + await user.click(submitButton); + + expect(registerMutationMock).toHaveBeenCalledWith( + { + email: "example@company.test", + key: "INTERNAL-USE-ONLY-1234-5678", + }, + expect.anything(), + ); + }); + + it.todo("handles and renders errors from server, if any"); + }); + + describe("when selected product is registrable and registration code is set", () => { + beforeEach(() => { + selectedProduct = sle; + registrationInfoMock = { key: "INTERNAL-USE-ONLY-1234-5678", email: "example@company.test" }; + }); + + it("does not render ProductRegistrationAlert component", () => { + installerRender(); + expect(screen.queryByText("Warning alert:")).toBeNull(); + }); + + it("renders registration information with code partially hidden", async () => { + const { user } = installerRender(); + const visibilityCodeToggler = screen.getByRole("button", { name: "Show" }); + screen.getByText(/\*?5678/); + expect(screen.queryByText("INTERNAL-USE-ONLY-1234-5678")).toBeNull(); + expect(screen.queryByText("INTERNAL-USE-ONLY-1234-5678")).toBeNull(); + screen.getByText("example@company.test"); + await user.click(visibilityCodeToggler); + screen.getByText("INTERNAL-USE-ONLY-1234-5678"); + await user.click(visibilityCodeToggler); + expect(screen.queryByText("INTERNAL-USE-ONLY-1234-5678")).toBeNull(); + screen.getByText(/\*?5678/); + }); + + it("allows deregistering the product", async () => { + const { user } = installerRender(); + const deregisterButton = screen.getByRole("button", { name: "deregister" }); + await user.click(deregisterButton); + expect(deregisterMutationMock).toHaveBeenCalled(); + }); + + // describe("but at registration path already", () => { + // beforeEach(() => { + // mockRoutes(REGISTRATION.root); + // }); + // + // it("does not render the link to registration", () => { + // installerRender(); + // screen.getByRole("heading", { + // name: /Warning alert:.*must be registered/, + // }); + // expect(screen.queryAllByRole("link")).toEqual([]); + // }); + // }); + // }); + // + // describe("when product is registrable and registration code is already set", () => { + // beforeEach(() => { + // selectedProduct = sle; + // registrationInfoMock = { key: "INTERNAL-USE-ONLY-1234-5678", email: "" }; + // }); + // + // it("renders nothing", () => { + // const { container } = installerRender(); + // expect(container).toBeEmptyDOMElement(); + // }); + // }); + // + // describe("when product is not registrable", () => { + // beforeEach(() => { + // selectedProduct = tw; + // }); + // + // it("renders nothing", () => { + // const { container } = installerRender(); + // expect(container).toBeEmptyDOMElement(); + // }); + }); +}); From 12cbb83517382c458174bc8085fcbae8e79a4c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 5 Dec 2024 15:19:00 +0000 Subject: [PATCH 28/47] fix(web) minor changes for overview page * Promote title * Remove no longer needed hint --- .../components/overview/OverviewPage.test.tsx | 7 ++- web/src/components/overview/OverviewPage.tsx | 43 ++++++++----------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/web/src/components/overview/OverviewPage.test.tsx b/web/src/components/overview/OverviewPage.test.tsx index 2cef9ae6b6..d37d118670 100644 --- a/web/src/components/overview/OverviewPage.test.tsx +++ b/web/src/components/overview/OverviewPage.test.tsx @@ -22,16 +22,19 @@ import React from "react"; import { screen } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; +import { installerRender } from "~/test-utils"; import { OverviewPage } from "~/components/overview"; jest.mock("~/components/overview/L10nSection", () => () =>
Localization Section
); jest.mock("~/components/overview/StorageSection", () => () =>
Storage Section
); jest.mock("~/components/overview/SoftwareSection", () => () =>
Software Section
); +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert
+)); describe("when a product is selected", () => { it("renders the overview page content", async () => { - plainRender(); + installerRender(); await screen.findByText("Localization Section"); await screen.findByText("Storage Section"); await screen.findByText("Software Section"); diff --git a/web/src/components/overview/OverviewPage.tsx b/web/src/components/overview/OverviewPage.tsx index ecbe0c5951..17f0efadc9 100644 --- a/web/src/components/overview/OverviewPage.tsx +++ b/web/src/components/overview/OverviewPage.tsx @@ -21,44 +21,35 @@ */ import React from "react"; -import { Grid, GridItem, Hint, HintBody, Stack } from "@patternfly/react-core"; +import { Grid, GridItem, Stack } from "@patternfly/react-core"; import { Page } from "~/components/core"; import L10nSection from "./L10nSection"; import StorageSection from "./StorageSection"; import SoftwareSection from "./SoftwareSection"; import { _ } from "~/i18n"; -const OverviewSection = () => ( - - - - - - - -); - export default function OverviewPage() { return ( + +

{_("Overview")}

+
+ - - - {_( - "Take your time to check your configuration before starting the installation process.", - )} - - - - - + + + + + + + From 5cd947b7838e66f8eda565b0c89ff3e4465d7ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 5 Dec 2024 15:54:03 +0000 Subject: [PATCH 29/47] fix(web) make test suite work again By using installerRender instead of plainRender when needed and pleasing TypeScript and linters. --- web/src/components/core/Page.test.tsx | 2 +- web/src/components/layout/Sidebar.test.tsx | 2 +- web/src/components/storage/ProposalPage.test.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/src/components/core/Page.test.tsx b/web/src/components/core/Page.test.tsx index 42a73fb767..242e22a875 100644 --- a/web/src/components/core/Page.test.tsx +++ b/web/src/components/core/Page.test.tsx @@ -121,7 +121,7 @@ describe("Page", () => { }); it("does not mount ProductRegistrationAlert", () => { - const { container } = installerRender(); + installerRender(); expect(screen.queryByText("ProductRegistrationAlertMock")).toBeNull(); }); }); diff --git a/web/src/components/layout/Sidebar.test.tsx b/web/src/components/layout/Sidebar.test.tsx index fb47f7c809..fa094548cc 100644 --- a/web/src/components/layout/Sidebar.test.tsx +++ b/web/src/components/layout/Sidebar.test.tsx @@ -32,7 +32,7 @@ jest.mock("~/components/core/ChangeProductLink", () => () =>
ChangeProductL const tw: Product = { id: "Tumbleweed", name: "openSUSE Tumbleweed", - registration: "No", + registration: "no", }; const sle: Product = { diff --git a/web/src/components/storage/ProposalPage.test.tsx b/web/src/components/storage/ProposalPage.test.tsx index 28081f88be..44f3c3d1d8 100644 --- a/web/src/components/storage/ProposalPage.test.tsx +++ b/web/src/components/storage/ProposalPage.test.tsx @@ -27,7 +27,7 @@ */ import React from "react"; import { screen } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; +import { installerRender } from "~/test-utils"; import { ProposalPage } from "~/components/storage"; import { ProposalResult, @@ -141,6 +141,6 @@ jest.mock("~/queries/storage", () => ({ })); it("renders the device, settings and result sections", () => { - plainRender(); + installerRender(); screen.findByText("Device"); }); From 7c3bd3bd9d7f21383d683028d6ec120f0d89c7c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 5 Dec 2024 16:46:48 +0000 Subject: [PATCH 30/47] fix(web): make test suite work again Part of previous commit 5cd947b7838e66f8eda565b0c89ff3e4465d7ac8 --- web/src/App.test.tsx | 4 ++-- .../core/FormValidationError.test.jsx | 14 +++++++++----- web/src/components/core/LoginPage.test.tsx | 14 +++++++++----- web/src/components/core/ServerError.test.tsx | 13 ++++++++----- .../components/l10n/KeyboardSelection.test.tsx | 8 ++++++-- web/src/components/l10n/L10nPage.test.jsx | 18 +++++++++++------- .../components/l10n/LocaleSelection.test.tsx | 8 ++++++-- .../components/l10n/TimezoneSelection.test.tsx | 10 +++++++--- .../components/network/NetworkPage.test.tsx | 4 ++++ .../product/ProductSelectionPage.test.tsx | 8 ++++++-- .../components/software/SoftwarePage.test.tsx | 4 ++++ .../SoftwarePatternsSelection.test.tsx | 4 ++++ .../users/RootAuthMethodsPage.test.tsx | 4 ++++ 13 files changed, 80 insertions(+), 33 deletions(-) diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index abe0834187..56fd4baffc 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -40,8 +40,8 @@ jest.mock("~/api/l10n", () => ({ updateConfig: jest.fn(), })); -const tumbleweed: Product = { id: "openSUSE", name: "openSUSE Tumbleweed", registration: "No" }; -const microos: Product = { id: "Leap Micro", name: "openSUSE Micro", registration: "No" }; +const tumbleweed: Product = { id: "openSUSE", name: "openSUSE Tumbleweed", registration: "no" }; +const microos: Product = { id: "Leap Micro", name: "openSUSE Micro", registration: "no" }; // list of available products let mockProducts: Product[]; diff --git a/web/src/components/core/FormValidationError.test.jsx b/web/src/components/core/FormValidationError.test.jsx index 3ee1651071..e1b6b7e3bd 100644 --- a/web/src/components/core/FormValidationError.test.jsx +++ b/web/src/components/core/FormValidationError.test.jsx @@ -22,26 +22,30 @@ import React from "react"; import { screen } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; +import { installerRender } from "~/test-utils"; import { FormValidationError } from "~/components/core"; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + it("renders nothing when message is null", () => { - const { container } = plainRender(); + const { container } = installerRender(); expect(container).toBeEmptyDOMElement(); }); it("renders nothing when message is empty", () => { - const { container } = plainRender(); + const { container } = installerRender(); expect(container).toBeEmptyDOMElement(); }); it("renders nothing when message is not defined", () => { - const { container } = plainRender(); + const { container } = installerRender(); expect(container).toBeEmptyDOMElement(); }); it("renders a PatternFly error with given message", () => { - plainRender(); + installerRender(); const node = screen.getByText("Invalid input"); expect(node.parentNode.classList.contains("pf-m-error")).toBe(true); }); diff --git a/web/src/components/core/LoginPage.test.tsx b/web/src/components/core/LoginPage.test.tsx index d03e595902..b6039bde5a 100644 --- a/web/src/components/core/LoginPage.test.tsx +++ b/web/src/components/core/LoginPage.test.tsx @@ -22,7 +22,7 @@ import React from "react"; import { screen, within } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; +import { installerRender } from "~/test-utils"; import { LoginPage } from "~/components/core"; import { AuthErrors } from "~/context/auth"; @@ -31,6 +31,10 @@ let mockIsAuthenticated: boolean; let mockLoginError; const mockLoginFn = jest.fn(); +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + jest.mock("~/context/auth", () => ({ ...jest.requireActual("~/context/auth"), useAuth: () => { @@ -56,12 +60,12 @@ describe("LoginPage", () => { describe("when user is not authenticated", () => { it("renders reference to root", () => { - plainRender(); + installerRender(); screen.getAllByText(/root/); }); it("allows entering a password", async () => { - const { user } = plainRender(); + const { user } = installerRender(); const form = screen.getByRole("form", { name: "Login form" }); const passwordInput = within(form).getByLabelText("Password input"); const loginButton = within(form).getByRole("button", { name: "Log in" }); @@ -79,7 +83,7 @@ describe("LoginPage", () => { }); it("renders an authentication error", async () => { - const { user } = plainRender(); + const { user } = installerRender(); const form = screen.getByRole("form", { name: "Login form" }); const passwordInput = within(form).getByLabelText("Password input"); const loginButton = within(form).getByRole("button", { name: "Log in" }); @@ -100,7 +104,7 @@ describe("LoginPage", () => { }); it("renders a server error text", async () => { - const { user } = plainRender(); + const { user } = installerRender(); const form = screen.getByRole("form", { name: "Login form" }); const passwordInput = within(form).getByLabelText("Password input"); const loginButton = within(form).getByRole("button", { name: "Log in" }); diff --git a/web/src/components/core/ServerError.test.tsx b/web/src/components/core/ServerError.test.tsx index 7f0a950333..e9922eb167 100644 --- a/web/src/components/core/ServerError.test.tsx +++ b/web/src/components/core/ServerError.test.tsx @@ -22,20 +22,23 @@ import React from "react"; import { screen } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; - -import * as utils from "~/utils"; +import { installerRender } from "~/test-utils"; import { ServerError } from "~/components/core"; +import * as utils from "~/utils"; + +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); describe("ServerError", () => { it("includes a generic server problem message", () => { - plainRender(); + installerRender(); screen.getByText(/Cannot connect to Agama server/i); }); it("calls location.reload when user clicks on 'Reload'", async () => { jest.spyOn(utils, "locationReload").mockImplementation(utils.noop); - const { user } = plainRender(); + const { user } = installerRender(); const reloadButton = await screen.findByRole("button", { name: /Reload/i }); await user.click(reloadButton); expect(utils.locationReload).toHaveBeenCalled(); diff --git a/web/src/components/l10n/KeyboardSelection.test.tsx b/web/src/components/l10n/KeyboardSelection.test.tsx index 10c85d6f8a..c22c3c5894 100644 --- a/web/src/components/l10n/KeyboardSelection.test.tsx +++ b/web/src/components/l10n/KeyboardSelection.test.tsx @@ -24,7 +24,7 @@ import React from "react"; import KeyboardSelection from "./KeyboardSelection"; import userEvent from "@testing-library/user-event"; import { screen } from "@testing-library/react"; -import { mockNavigateFn, plainRender } from "~/test-utils"; +import { mockNavigateFn, installerRender } from "~/test-utils"; const keymaps = [ { id: "us", name: "English" }, @@ -35,6 +35,10 @@ const mockConfigMutation = { mutate: jest.fn(), }; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + jest.mock("~/queries/l10n", () => ({ ...jest.requireActual("~/queries/l10n"), useConfigMutation: () => mockConfigMutation, @@ -47,7 +51,7 @@ jest.mock("react-router-dom", () => ({ })); it("allows changing the keyboard", async () => { - plainRender(); + installerRender(); const option = await screen.findByText("Spanish"); await userEvent.click(option); diff --git a/web/src/components/l10n/L10nPage.test.jsx b/web/src/components/l10n/L10nPage.test.jsx index 7229c51903..05ef150027 100644 --- a/web/src/components/l10n/L10nPage.test.jsx +++ b/web/src/components/l10n/L10nPage.test.jsx @@ -22,9 +22,13 @@ import React from "react"; import { screen, within } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; +import { installerRender } from "~/test-utils"; import L10nPage from "~/components/l10n/L10nPage"; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + let mockLoadedData; const locales = [ @@ -58,7 +62,7 @@ beforeEach(() => { }); it("renders a section for configuring the language", () => { - plainRender(); + installerRender(); const region = screen.getByRole("region", { name: "Language" }); within(region).getByText("English - United States"); within(region).getByText("Change"); @@ -70,7 +74,7 @@ describe("if there is no selected language", () => { }); it("renders a button for selecting a language", () => { - plainRender(); + installerRender(); const region = screen.getByRole("region", { name: "Language" }); within(region).getByText("Not selected yet"); within(region).getByText("Select"); @@ -78,7 +82,7 @@ describe("if there is no selected language", () => { }); it("renders a section for configuring the keyboard", () => { - plainRender(); + installerRender(); const region = screen.getByRole("region", { name: "Keyboard" }); within(region).getByText("English"); within(region).getByText("Change"); @@ -90,7 +94,7 @@ describe("if there is no selected keyboard", () => { }); it("renders a button for selecting a keyboard", () => { - plainRender(); + installerRender(); const region = screen.getByRole("region", { name: "Keyboard" }); within(region).getByText("Not selected yet"); within(region).getByText("Select"); @@ -98,7 +102,7 @@ describe("if there is no selected keyboard", () => { }); it("renders a section for configuring the time zone", () => { - plainRender(); + installerRender(); const region = screen.getByRole("region", { name: "Time zone" }); within(region).getByText("Europe - Berlin"); within(region).getByText("Change"); @@ -110,7 +114,7 @@ describe("if there is no selected time zone", () => { }); it("renders a button for selecting a time zone", () => { - plainRender(); + installerRender(); const region = screen.getByRole("region", { name: "Time zone" }); within(region).getByText("Not selected yet"); within(region).getByText("Select"); diff --git a/web/src/components/l10n/LocaleSelection.test.tsx b/web/src/components/l10n/LocaleSelection.test.tsx index d00cb87769..b2d4a98aa5 100644 --- a/web/src/components/l10n/LocaleSelection.test.tsx +++ b/web/src/components/l10n/LocaleSelection.test.tsx @@ -24,7 +24,7 @@ import React from "react"; import LocaleSelection from "./LocaleSelection"; import userEvent from "@testing-library/user-event"; import { screen } from "@testing-library/react"; -import { mockNavigateFn, plainRender } from "~/test-utils"; +import { mockNavigateFn, installerRender } from "~/test-utils"; const locales = [ { id: "en_US.UTF-8", name: "English", territory: "United States" }, @@ -35,6 +35,10 @@ const mockConfigMutation = { mutate: jest.fn(), }; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + jest.mock("~/queries/l10n", () => ({ ...jest.requireActual("~/queries/l10n"), useL10n: () => ({ locales, selectedLocale: locales[0] }), @@ -47,7 +51,7 @@ jest.mock("react-router-dom", () => ({ })); it("allows changing the keyboard", async () => { - plainRender(); + installerRender(); const option = await screen.findByText("Spanish"); await userEvent.click(option); diff --git a/web/src/components/l10n/TimezoneSelection.test.tsx b/web/src/components/l10n/TimezoneSelection.test.tsx index 201595f4e7..1077a34cef 100644 --- a/web/src/components/l10n/TimezoneSelection.test.tsx +++ b/web/src/components/l10n/TimezoneSelection.test.tsx @@ -24,7 +24,11 @@ import React from "react"; import TimezoneSelection from "./TimezoneSelection"; import userEvent from "@testing-library/user-event"; import { screen } from "@testing-library/react"; -import { mockNavigateFn, plainRender } from "~/test-utils"; +import { mockNavigateFn, installerRender } from "~/test-utils"; + +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); const timezones = [ { id: "Europe/Berlin", parts: ["Europe", "Berlin"], country: "Germany", utcOffset: 120 }, @@ -70,7 +74,7 @@ afterEach(() => { }); it("allows changing the timezone", async () => { - plainRender(); + installerRender(); const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); const option = await screen.findByText("Europe-Madrid"); @@ -82,7 +86,7 @@ it("allows changing the timezone", async () => { }); it("displays the UTC offset", () => { - plainRender(); + installerRender(); expect(screen.getByText("Australia/Adelaide UTC+9:30")).toBeInTheDocument(); expect(screen.getByText("Europe/Madrid UTC+2")).toBeInTheDocument(); diff --git a/web/src/components/network/NetworkPage.test.tsx b/web/src/components/network/NetworkPage.test.tsx index e6f0bfae52..841bb245cb 100644 --- a/web/src/components/network/NetworkPage.test.tsx +++ b/web/src/components/network/NetworkPage.test.tsx @@ -26,6 +26,10 @@ import { installerRender } from "~/test-utils"; import NetworkPage from "~/components/network/NetworkPage"; import { Connection, ConnectionMethod, ConnectionStatus, ConnectionType } from "~/types/network"; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + const wiredConnection = new Connection("eth0", { iface: "eth0", method4: ConnectionMethod.MANUAL, diff --git a/web/src/components/product/ProductSelectionPage.test.tsx b/web/src/components/product/ProductSelectionPage.test.tsx index bb096ef626..9e8607a1e5 100644 --- a/web/src/components/product/ProductSelectionPage.test.tsx +++ b/web/src/components/product/ProductSelectionPage.test.tsx @@ -27,13 +27,17 @@ import { ProductSelectionPage } from "~/components/product"; import { Product } from "~/types/software"; import { useProduct } from "~/queries/software"; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + const mockConfigMutation = jest.fn(); const tumbleweed: Product = { id: "Tumbleweed", name: "openSUSE Tumbleweed", icon: "tumbleweed.svg", description: "Tumbleweed description...", - registration: "No", + registration: "no", }; const microOs: Product = { @@ -41,7 +45,7 @@ const microOs: Product = { name: "openSUSE MicroOS", icon: "microos.svg", description: "MicroOS description", - registration: "No", + registration: "no", }; jest.mock("~/queries/software", () => ({ diff --git a/web/src/components/software/SoftwarePage.test.tsx b/web/src/components/software/SoftwarePage.test.tsx index 88a99d05e3..132d5b0e23 100644 --- a/web/src/components/software/SoftwarePage.test.tsx +++ b/web/src/components/software/SoftwarePage.test.tsx @@ -28,6 +28,10 @@ import testingPatterns from "./patterns.test.json"; import testingProposal from "./proposal.test.json"; import SoftwarePage from "./SoftwarePage"; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + jest.mock("~/queries/issues", () => ({ useIssues: () => [], })); diff --git a/web/src/components/software/SoftwarePatternsSelection.test.tsx b/web/src/components/software/SoftwarePatternsSelection.test.tsx index 1072054b56..e80ecac6b9 100644 --- a/web/src/components/software/SoftwarePatternsSelection.test.tsx +++ b/web/src/components/software/SoftwarePatternsSelection.test.tsx @@ -28,6 +28,10 @@ import SoftwarePatternsSelection from "./SoftwarePatternsSelection"; const onConfigMutationMock = { mutate: jest.fn() }; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + jest.mock("~/queries/software", () => ({ usePatterns: () => testingPatterns, useConfigMutation: () => onConfigMutationMock, diff --git a/web/src/components/users/RootAuthMethodsPage.test.tsx b/web/src/components/users/RootAuthMethodsPage.test.tsx index 649e4cfd36..e814d30661 100644 --- a/web/src/components/users/RootAuthMethodsPage.test.tsx +++ b/web/src/components/users/RootAuthMethodsPage.test.tsx @@ -27,6 +27,10 @@ import { RootAuthMethodsPage } from "~/components/users"; const mockRootUserMutation = { mutateAsync: jest.fn() }; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + jest.mock("~/queries/users", () => ({ ...jest.requireActual("~/queries/users"), useRootUserMutation: () => mockRootUserMutation, From c14bccc4afceb2c849e1021d15d8ef47344d261a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 5 Dec 2024 16:53:03 +0000 Subject: [PATCH 31/47] fix(web): fix product registration values For using the right value according to the type. Overlooked in previous commits. --- web/src/components/core/ChangeProductLink.test.tsx | 4 ++-- web/src/components/layout/Header.test.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/src/components/core/ChangeProductLink.test.tsx b/web/src/components/core/ChangeProductLink.test.tsx index bc71c1be9b..e6fe63daee 100644 --- a/web/src/components/core/ChangeProductLink.test.tsx +++ b/web/src/components/core/ChangeProductLink.test.tsx @@ -32,14 +32,14 @@ const tumbleweed: Product = { name: "openSUSE Tumbleweed", icon: "tumbleweed.svg", description: "Tumbleweed description...", - registration: "No", + registration: "no", }; const microos: Product = { id: "MicroOS", name: "openSUSE MicroOS", icon: "MicroOS.svg", description: "MicroOS description", - registration: "No", + registration: "no", }; let mockUseProduct: { products: Product[]; selectedProduct?: Product }; diff --git a/web/src/components/layout/Header.test.tsx b/web/src/components/layout/Header.test.tsx index 882a11e904..bd34ec37b8 100644 --- a/web/src/components/layout/Header.test.tsx +++ b/web/src/components/layout/Header.test.tsx @@ -31,14 +31,14 @@ const tumbleweed: Product = { id: "Tumbleweed", name: "openSUSE Tumbleweed", description: "Tumbleweed description...", - registration: "No", + registration: "no", }; const microos: Product = { id: "MicroOS", name: "openSUSE MicroOS", description: "MicroOS description", - registration: "No", + registration: "no", }; let phase: InstallationPhase; From 123a7514d1f4483a66bd19902073f466a231a1ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 13 Dec 2024 11:51:53 +0000 Subject: [PATCH 32/47] feat(rust): remove the SLES 15.6 definition --- products.d/agama-products.spec | 1 - products.d/sles_156.yaml | 163 --------------------------------- 2 files changed, 164 deletions(-) delete mode 100644 products.d/sles_156.yaml diff --git a/products.d/agama-products.spec b/products.d/agama-products.spec index 41aba34169..10e6448cc2 100644 --- a/products.d/agama-products.spec +++ b/products.d/agama-products.spec @@ -52,7 +52,6 @@ Definition of openSUSE products (Tumbleweed, Leap, MicroOS and Slowroll) for the %{_datadir}/agama/products.d/tumbleweed.yaml %{_datadir}/agama/products.d/leap_160.yaml %{_datadir}/agama/products.d/slowroll.yaml -%{_datadir}/agama/products.d/sles_156.yaml %package sle Summary: Definition of SLE products for the Agama installer. diff --git a/products.d/sles_156.yaml b/products.d/sles_156.yaml deleted file mode 100644 index 48c70ca4af..0000000000 --- a/products.d/sles_156.yaml +++ /dev/null @@ -1,163 +0,0 @@ -id: SLES -name: SUSE Linux Enterprise Server 15.6 -registration: "mandatory" -version: "15-6" -# ------------------------------------------------------------------------------ -# WARNING: When changing the product description delete the translations located -# at the at translations/description key below to avoid using obsolete -# translations!! -# ------------------------------------------------------------------------------ -description: "An open, reliable, compliant, and future-proof Linux Server choice - that ensures the enterprise's business continuity. It is the secure and - adaptable OS for long-term supported, innovation-ready infrastructure running - business-critical workloads on-premises, in the cloud, and at the edge." -icon: SUSE.svg -# Do not manually change any translations! See README.md for more details. -translations: - description: - ca: Una opció de servidor de Linux oberta, fiable, compatible i a prova del - futur que garanteix la continuïtat del negoci de l'empresa. És el sistema - operatiu segur i adaptable per a una infraestructura amb suport a llarg - termini i preparada per a la innovació que executa càrregues de treball - crítiques per a l'empresa a les instal·lacions, al núvol i a l'última. - cs: - Otevřená, spolehlivá, kompatibilní a perspektivní volba linuxového serveru, - která zajišťuje kontinuitu podnikání podniku. Je to bezpečný a - přizpůsobivý operační systém pro dlouhodobě podporovanou infrastrukturu - připravenou na inovace, na které běží kritické podnikové úlohy v lokálním - prostředí, v cloudu i na okraji sítě. - es: - Una opción de servidor Linux abierta, confiable, compatible y preparada para - el futuro que garantiza la continuidad del negocio de la empresa. Es el - sistema operativo seguro y adaptable para una infraestructura lista para - la innovación y con soporte a largo plazo que ejecuta cargas de trabajo - críticas para el negocio en las instalaciones, en la nube y en el borde. - ja: - オープンで信頼性が高く、各種の標準にも準拠し、将来性とビジネスの継続性を支援する Linux - サーバです。長期のサポートが提供されていることから、安全性と順応性に優れ、オンプレミスからクラウド、エッジ環境に至るまで、様々な場所で重要なビジネス処理をこなすことのできる革新性の高いインフラストラクチャです。 - pt_BR: - Uma escolha de servidor Linux aberta, confiável, compatível e à prova do - futuro que garante a continuidade dos negócios da empresa. É o SO seguro e - adaptável para infraestrutura com suporte de longo prazo e pronta para - inovação, executando cargas de trabalho críticas para os negócios no - local, na nuvem e na borda. - sv: Ett öppet, pålitligt, kompatibelt och framtidssäkert Linux-serverval som - säkerställer företagets affärskontinuitet. Det är det säkra och - anpassningsbara operativsystemet för långsiktigt stödd, innovationsfärdig - infrastruktur som kör affärskritiska arbetsbelastningar på plats, i molnet - och vid kanten. - tr: - İşletmenin iş sürekliliğini garanti eden açık, güvenilir, uyumlu ve geleceğe - dönük bir Linux Sunucu seçeneği. Uzun vadeli desteklenen, inovasyona hazır - altyapı için güvenli ve uyarlanabilir işletim sistemidir. Şirket içinde, - bulutta ve uçta iş açısından kritik iş yüklerini çalıştırır. -software: - installation_repositories: [] - mandatory_patterns: - - base_traditional - optional_patterns: null # no optional pattern shared - user_patterns: [] - mandatory_packages: - - NetworkManager - optional_packages: null - base_product: SLES - -security: - lsm: selinux - available_lsms: - selinux: - patterns: - - selinux - policy: enforcing - none: - patterns: null - -storage: - space_policy: delete - volumes: - - "/" - - "swap" - volume_templates: - - mount_path: "/" - filesystem: btrfs - btrfs: - snapshots: true - read_only: false - default_subvolume: "@" - subvolumes: - - path: home - - path: opt - - path: root - - path: srv - - path: usr/local - # Unified var subvolume - https://lists.opensuse.org/opensuse-packaging/2017-11/msg00017.html - - path: var - copy_on_write: false - # Architecture specific subvolumes - - path: boot/grub2/arm64-efi - archs: aarch64 - - path: boot/grub2/arm-efi - archs: arm - - path: boot/grub2/i386-pc - archs: x86_64 - - path: boot/grub2/powerpc-ieee1275 - archs: ppc,!board_powernv - - path: boot/grub2/s390x-emu - archs: s390 - - path: boot/grub2/x86_64-efi - archs: x86_64 - - path: boot/grub2/riscv64-efi - archs: riscv64 - size: - auto: true - outline: - required: true - filesystems: - - btrfs - - ext2 - - ext3 - - ext4 - - xfs - auto_size: - base_min: 5 GiB - base_max: 15 GiB - snapshots_increment: 250% - max_fallback_for: - - "/home" - snapshots_configurable: true - - mount_path: "swap" - filesystem: swap - size: - min: 1 GiB - max: 2 GiB - outline: - required: false - filesystems: - - swap - - mount_path: "/home" - filesystem: xfs - size: - auto: false - min: 10 GiB - max: unlimited - outline: - required: false - filesystems: - - btrfs - - ext2 - - ext3 - - ext4 - - xfs - - filesystem: xfs - size: - auto: false - min: 1 GiB - outline: - required: false - filesystems: - - btrfs - - ext2 - - ext3 - - ext4 - - xfs - - vfat From 828771084243331cc14c592f151ffadf76936698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 13 Dec 2024 11:52:08 +0000 Subject: [PATCH 33/47] feat(rust): enable the registration for SLE 16 --- products.d/sles_160.yaml | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/products.d/sles_160.yaml b/products.d/sles_160.yaml index ffe9d6a731..3c095d6b84 100644 --- a/products.d/sles_160.yaml +++ b/products.d/sles_160.yaml @@ -1,5 +1,7 @@ -id: SLES_16.0 +id: SLES name: SUSE Linux Enterprise Server 16.0 Alpha +registration: "mandatory" +version: "16-0" # ------------------------------------------------------------------------------ # WARNING: When changing the product description delete the translations located # at the at translations/description key below to avoid using obsolete @@ -45,18 +47,7 @@ translations: altyapı için güvenli ve uyarlanabilir işletim sistemidir. Şirket içinde, bulutta ve uçta iş açısından kritik iş yüklerini çalıştırır. software: - installation_repositories: - # Use plain HTTP repositories, HTTPS does not work without importing the SSL - # certificate. It is safe as the repository is GPG checked and you neeed VPN - # to reach the internal server anyway. - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-Packages-16.0-x86_64/ - archs: x86_64 - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-Packages-16.0-aarch64/ - archs: aarch64 - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-Packages-16.0-ppc64le/ - archs: ppc - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-Packages-16.0-s390x/ - archs: s390 + installation_repositories: [] mandatory_patterns: - base_traditional From 4ec593adceaa26018903fa9ee1129d49532c9ff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 13 Dec 2024 12:22:25 +0000 Subject: [PATCH 34/47] fix(web): remove an unused variable --- web/src/components/core/IssuesDrawer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/components/core/IssuesDrawer.tsx b/web/src/components/core/IssuesDrawer.tsx index 656e946b25..13d3ae31c2 100644 --- a/web/src/components/core/IssuesDrawer.tsx +++ b/web/src/components/core/IssuesDrawer.tsx @@ -68,7 +68,6 @@ const IssuesDrawer = forwardRef(({ onClose }: { onClose: () => void }, ref) => { if (issues.length === 0) return null; // FIXME: address this better or use the /product(s)? namespace instead of // /registration. - const section = scope === "product" ? "registration" : scope; const ariaLabelId = `${scope}-issues-section`; return ( From 3135178f8bac0103f77e48e6260c36d1ed36af14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 13 Dec 2024 12:37:58 +0000 Subject: [PATCH 35/47] feat(products): enable registration for SLES for SAP --- products.d/sles_sap_160.yaml | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/products.d/sles_sap_160.yaml b/products.d/sles_sap_160.yaml index 20955514ec..8113188e09 100644 --- a/products.d/sles_sap_160.yaml +++ b/products.d/sles_sap_160.yaml @@ -1,5 +1,7 @@ -id: SLES_SAP_16.0 +id: SLES-SAP name: SUSE Linux Enterprise Server for SAP Applications 16.0 Alpha +registration: "mandatory" +version: "16-0" # ------------------------------------------------------------------------------ # WARNING: When changing the product description delete the translations located # at the at translations/description key below to avoid using obsolete @@ -45,18 +47,7 @@ translations: altyapı için güvenli ve uyarlanabilir işletim sistemidir. Şirket içinde, bulutta ve uçta iş açısından kritik iş yüklerini çalıştırır. software: - installation_repositories: - # Use plain HTTP repositories, HTTPS does not work without importing the SSL - # certificate. It is safe as the repository is GPG checked and you neeed VPN - # to reach the internal server anyway. - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-SAP-Packages-16.0-x86_64/ - archs: x86_64 - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-SAP-Packages-16.0-aarch64/ - archs: aarch64 - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-SAP-Packages-16.0-ppc64le/ - archs: ppc - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-SAP-Packages-16.0-s390x/ - archs: s390 + installation_repositories: [] mandatory_patterns: - base_traditional From 8182c1cfd81a85d2602812a0dd979cf258f3f7c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 13 Dec 2024 15:48:52 +0000 Subject: [PATCH 36/47] fix(web): make ESLint happy --- web/src/components/core/LoginPage.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/core/LoginPage.test.tsx b/web/src/components/core/LoginPage.test.tsx index 6a61d4dd7a..8256357ee8 100644 --- a/web/src/components/core/LoginPage.test.tsx +++ b/web/src/components/core/LoginPage.test.tsx @@ -22,7 +22,7 @@ import React from "react"; import { screen, within } from "@testing-library/react"; -import { installerRender, mockRoutes, plainRender } from "~/test-utils"; +import { installerRender, mockRoutes } from "~/test-utils"; import { LoginPage } from "~/components/core"; import { AuthErrors } from "~/context/auth"; import { PlainLayout } from "../layout"; From 6fd4bda0a2921a637a929f6b45f55b41bef5177a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 7 Jan 2025 09:51:58 +0000 Subject: [PATCH 37/47] fix(web): please TypeScript --- web/src/components/product/ProductSelectionProgress.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/product/ProductSelectionProgress.test.tsx b/web/src/components/product/ProductSelectionProgress.test.tsx index 082ac388d2..0171fe9bc0 100644 --- a/web/src/components/product/ProductSelectionProgress.test.tsx +++ b/web/src/components/product/ProductSelectionProgress.test.tsx @@ -30,7 +30,7 @@ import { Product } from "~/types/software"; jest.mock("~/components/core/ProgressReport", () => () =>
ProgressReport Mock
); let isBusy = false; -const tumbleweed: Product = { id: "openSUSE", name: "openSUSE Tumbleweed" }; +const tumbleweed: Product = { id: "openSUSE", name: "openSUSE Tumbleweed", registration: "no" }; jest.mock("~/queries/status", () => ({ ...jest.requireActual("~/queries/status"), From 6d0919ea5d0868f9408943f57441616497479ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 7 Jan 2025 09:52:20 +0000 Subject: [PATCH 38/47] fix(web): do not show the registration alert when registration type unknonw The registration attribute is actually mandatory, but better to prevent displaying the alert if it is missing for some reason. --- web/src/components/product/ProductRegistrationAlert.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/product/ProductRegistrationAlert.tsx b/web/src/components/product/ProductRegistrationAlert.tsx index 4f09c30229..4693a969db 100644 --- a/web/src/components/product/ProductRegistrationAlert.tsx +++ b/web/src/components/product/ProductRegistrationAlert.tsx @@ -50,7 +50,7 @@ export default function ProductRegistrationAlert() { // NOTE: it shouldn't be mounted in these paths, but let's prevent rendering // if so just in case. if (SUPPORTIVE_PATHS.includes(location.pathname)) return; - if (product.registration === "no" || !isEmpty(registration.key)) return; + if (["no", undefined].includes(product.registration) || !isEmpty(registration.key)) return; return ( From 7e4e6dbfe4c5caea297bcbbce76e41641c4b51de Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 8 Jan 2025 09:58:37 +0100 Subject: [PATCH 39/47] fix id to match scc --- products.d/sles_sap_160.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products.d/sles_sap_160.yaml b/products.d/sles_sap_160.yaml index 05088d31c1..39a4b00eb9 100644 --- a/products.d/sles_sap_160.yaml +++ b/products.d/sles_sap_160.yaml @@ -1,4 +1,4 @@ -id: SLES_SAP +id: SLES-SAP name: SUSE Linux Enterprise Server for SAP Applications 16.0 Beta registration: "mandatory" version: "16-0" From 9a988714f05312064bcfdb18d82e69427b0662b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 8 Jan 2025 11:53:26 +0000 Subject: [PATCH 40/47] fix(rust): handle registration errors properly --- rust/agama-lib/src/product/http_client.rs | 21 +++++++++++++++++++-- rust/agama-lib/src/product/store.rs | 13 ++----------- rust/agama-lib/src/software/model.rs | 8 ++++++++ rust/agama-server/src/software/web.rs | 23 +++++++++-------------- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/rust/agama-lib/src/product/http_client.rs b/rust/agama-lib/src/product/http_client.rs index 424a8b49f9..29a36f3b3a 100644 --- a/rust/agama-lib/src/product/http_client.rs +++ b/rust/agama-lib/src/product/http_client.rs @@ -18,6 +18,7 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. +use crate::software::model::RegistrationError; use crate::software::model::RegistrationInfo; use crate::software::model::RegistrationParams; use crate::software::model::SoftwareConfig; @@ -64,13 +65,29 @@ impl ProductHTTPClient { } /// register product - pub async fn register(&self, key: &str, email: &str) -> Result<(u32, String), ServiceError> { + pub async fn register(&self, key: &str, email: &str) -> Result<(), ServiceError> { // note RegistrationParams != RegistrationInfo, fun! let params = RegistrationParams { key: key.to_owned(), email: email.to_owned(), }; + let result = self + .client + .post_void("/software/registration", ¶ms) + .await; - self.client.post("/software/registration", ¶ms).await + let Err(error) = result else { + return Ok(()); + }; + + let message = match error { + ServiceError::BackendError(_, details) => { + let details: RegistrationError = serde_json::from_str(&details).unwrap(); + format!("{} (error code: {})", details.message, details.id) + } + _ => format!("Could not register the product: #{error:?}"), + }; + + Err(ServiceError::FailedRegistration(message)) } } diff --git a/rust/agama-lib/src/product/store.rs b/rust/agama-lib/src/product/store.rs index d43a6166c4..c1a3f42297 100644 --- a/rust/agama-lib/src/product/store.rs +++ b/rust/agama-lib/src/product/store.rs @@ -68,17 +68,8 @@ impl ProductStore { } } if let Some(reg_code) = &settings.registration_code { - let (result, message); - if let Some(email) = &settings.registration_email { - (result, message) = self.product_client.register(reg_code, email).await?; - } else { - (result, message) = self.product_client.register(reg_code, "").await?; - } - // FIXME: name the magic numbers. 3 is Registration not required - // FIXME: well don't register when not required (no regcode in profile) - if result != 0 && result != 3 { - return Err(ServiceError::FailedRegistration(message)); - } + let email = settings.registration_email.as_deref().unwrap_or(""); + self.product_client.register(reg_code, email).await?; probe = true; } diff --git a/rust/agama-lib/src/software/model.rs b/rust/agama-lib/src/software/model.rs index 32618542f1..8556dae11d 100644 --- a/rust/agama-lib/src/software/model.rs +++ b/rust/agama-lib/src/software/model.rs @@ -71,6 +71,14 @@ pub enum RegistrationRequirement { Mandatory = 2, } +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RegistrationError { + /// ID of error. See dbus API for possible values + pub id: u32, + /// human readable error string intended to be displayed to user + pub message: String, +} + /// Software resolvable type (package or pattern). #[derive(Deserialize, Serialize, strum::Display, utoipa::ToSchema)] #[strum(serialize_all = "camelCase")] diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index f800c75cab..40d49be018 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -37,7 +37,10 @@ use agama_lib::{ error::ServiceError, product::{proxies::RegistrationProxy, Product, ProductClient}, software::{ - model::{RegistrationInfo, RegistrationParams, ResolvableParams, SoftwareConfig}, + model::{ + RegistrationError, RegistrationInfo, RegistrationParams, ResolvableParams, + SoftwareConfig, + }, proxies::{Software1Proxy, SoftwareProductProxy}, Pattern, SelectedBy, SoftwareClient, UnknownSelectedBy, }, @@ -49,7 +52,7 @@ use axum::{ routing::{get, post, put}, Json, Router, }; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use std::collections::HashMap; use tokio_stream::{Stream, StreamExt}; @@ -248,14 +251,6 @@ async fn get_registration( Ok(Json(result)) } -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct FailureDetails { - /// ID of error. See dbus API for possible values - id: u32, - /// human readable error string intended to be displayed to user - message: String, -} - /// Register product /// /// * `state`: service state. @@ -265,7 +260,7 @@ pub struct FailureDetails { context_path = "/api/software", responses( (status = 204, description = "registration successfull"), - (status = 422, description = "Registration failed. Details are in body", body = FailureDetails), + (status = 422, description = "Registration failed. Details are in body", body = RegistrationError), (status = 400, description = "The D-Bus service could not perform the action") ) )] @@ -274,10 +269,10 @@ async fn register( Json(config): Json, ) -> Result { let (id, message) = state.product.register(&config.key, &config.email).await?; - let details = FailureDetails { id, message }; if id == 0 { Ok((StatusCode::NO_CONTENT, ().into_response())) } else { + let details = RegistrationError { id, message }; Ok(( StatusCode::UNPROCESSABLE_ENTITY, Json(details).into_response(), @@ -294,13 +289,13 @@ async fn register( context_path = "/api/software", responses( (status = 200, description = "deregistration successfull"), - (status = 422, description = "De-registration failed. Details are in body", body = FailureDetails), + (status = 422, description = "De-registration failed. Details are in body", body = RegistrationError), (status = 400, description = "The D-Bus service could not perform the action") ) )] async fn deregister(State(state): State>) -> Result { let (id, message) = state.product.deregister().await?; - let details = FailureDetails { id, message }; + let details = RegistrationError { id, message }; if id == 0 { Ok((StatusCode::NO_CONTENT, ().into_response())) } else { From 9807c91405043618e7b12d2cd26229bf0e9dec68 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 8 Jan 2025 14:00:18 +0100 Subject: [PATCH 41/47] allow sles4SAP only on supported architectures --- products.d/sles_sap_160.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/products.d/sles_sap_160.yaml b/products.d/sles_sap_160.yaml index 39a4b00eb9..30c86b7671 100644 --- a/products.d/sles_sap_160.yaml +++ b/products.d/sles_sap_160.yaml @@ -1,5 +1,6 @@ id: SLES-SAP name: SUSE Linux Enterprise Server for SAP Applications 16.0 Beta +archs: x86_64,aarch64,ppc registration: "mandatory" version: "16-0" # ------------------------------------------------------------------------------ From 3850472a58214787db8829f2fa4dd227518a38cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 8 Jan 2025 14:09:23 +0000 Subject: [PATCH 42/47] doc(rust): update changes files --- products.d/agama-products.changes | 6 ++++++ rust/package/agama.changes | 6 ++++++ service/package/rubygem-agama-yast.changes | 6 ++++++ web/package/agama-web-ui.changes | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/products.d/agama-products.changes b/products.d/agama-products.changes index 3c0380ef27..c30beb6d34 100644 --- a/products.d/agama-products.changes +++ b/products.d/agama-products.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jan 8 14:07:21 UTC 2025 - Imobach Gonzalez Sosa + +- Add support for products registration (jsc#PED-11192, + gh#agama-project/agama#1809). + ------------------------------------------------------------------- Mon Jan 6 14:41:28 UTC 2025 - Angela Briel diff --git a/rust/package/agama.changes b/rust/package/agama.changes index ecf3c6384f..8fa24ecd3f 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jan 8 14:05:34 UTC 2025 - Imobach Gonzalez Sosa + +- Add support for products registration (jsc#PED-11192, + gh#agama-project/agama#1809). + ------------------------------------------------------------------- Fri Dec 20 12:17:26 UTC 2024 - Josef Reidinger diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 217912e49d..f9f77e40ed 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jan 8 14:05:53 UTC 2025 - Imobach Gonzalez Sosa + +- Add support for products registration (jsc#PED-11192, + gh#agama-project/agama#1809). + ------------------------------------------------------------------- Mon Dec 23 18:40:01 UTC 2024 - Josef Reidinger diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 398cf686d1..6e0d778d32 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jan 8 14:07:13 UTC 2025 - Imobach Gonzalez Sosa + +- Add support for products registration (jsc#PED-11192, + gh#agama-project/agama#1809). + ------------------------------------------------------------------- Fri Dec 20 12:53:41 UTC 2024 - David Diaz From 0cedb6a9491547478cd1acefd1d4005a25608f64 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 8 Jan 2025 16:01:41 +0100 Subject: [PATCH 43/47] fix product registration finish --- service/lib/agama/registration.rb | 4 ++- service/test/agama/registration_test.rb | 36 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb index 655dc96c1d..658a1c9c83 100644 --- a/service/lib/agama/registration.rb +++ b/service/lib/agama/registration.rb @@ -142,7 +142,9 @@ def deregister def finish return unless reg_code - files = [credentials_path(@credentials_file), SUSE::Connect::YaST::GLOBAL_CREDENTIALS_PATH] + files = [GLOBAL_CREDENTIALS_PATH] + files << credentials_path(@credentials_file) if @credentials_file + files.each do |file| dest = File.join(Yast::Installation.destdir, file) FileUtils.cp(file, dest) diff --git a/service/test/agama/registration_test.rb b/service/test/agama/registration_test.rb index c62e258a61..748b35df89 100644 --- a/service/test/agama/registration_test.rb +++ b/service/test/agama/registration_test.rb @@ -33,6 +33,7 @@ subject { described_class.new(manager, logger) } let(:manager) { instance_double(Agama::Software::Manager) } + let(:product) { Agama::Software::Product.new("test").tap { |p| p.version = "5.0" } } let(:logger) { Logger.new($stdout, level: :warn) } @@ -375,4 +376,39 @@ end end end + + describe "#finish" do + context "system is not registered" do + before do + subject.instance_variable_set(:@reg_code, nil) + end + + it "do nothing" do + expect(::FileUtils).to_not receive(:cp) + + subject.finish + end + end + + context "system is registered" do + before do + subject.instance_variable_set(:@reg_code, "test") + subject.instance_variable_set(:@credentials_file, "test") + Yast::Installation.destdir = "/mnt" + allow(::FileUtils).to receive(:cp) + end + + it "copies global credentials file" do + expect(::FileUtils).to receive(:cp).with("/etc/zypp/credentials.d/SCCcredentials", "/mnt/etc/zypp/credentials.d/SCCcredentials") + + subject.finish + end + + it "copies product credentials file" do + expect(::FileUtils).to receive(:cp).with("/etc/zypp/credentials.d/test", "/mnt/etc/zypp/credentials.d/test") + + subject.finish + end + end + end end From d66322c6e3cfda838a6e17f9b908f95a3a54514d Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 8 Jan 2025 16:13:12 +0100 Subject: [PATCH 44/47] make rubocop happy --- service/test/agama/registration_test.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/service/test/agama/registration_test.rb b/service/test/agama/registration_test.rb index 748b35df89..2670ae3856 100644 --- a/service/test/agama/registration_test.rb +++ b/service/test/agama/registration_test.rb @@ -399,13 +399,15 @@ end it "copies global credentials file" do - expect(::FileUtils).to receive(:cp).with("/etc/zypp/credentials.d/SCCcredentials", "/mnt/etc/zypp/credentials.d/SCCcredentials") + expect(::FileUtils).to receive(:cp).with("/etc/zypp/credentials.d/SCCcredentials", + "/mnt/etc/zypp/credentials.d/SCCcredentials") subject.finish end it "copies product credentials file" do - expect(::FileUtils).to receive(:cp).with("/etc/zypp/credentials.d/test", "/mnt/etc/zypp/credentials.d/test") + expect(::FileUtils).to receive(:cp).with("/etc/zypp/credentials.d/test", + "/mnt/etc/zypp/credentials.d/test") subject.finish end From 83a8c0d8a396019ed1376ed37a62953bc9a9adc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 8 Jan 2025 18:27:28 +0000 Subject: [PATCH 45/47] fix(web): fix duplicated variable in ProductSelectionPage tests --- web/src/components/product/ProductSelectionPage.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/product/ProductSelectionPage.test.tsx b/web/src/components/product/ProductSelectionPage.test.tsx index 1a7f4afed7..0c54144f16 100644 --- a/web/src/components/product/ProductSelectionPage.test.tsx +++ b/web/src/components/product/ProductSelectionPage.test.tsx @@ -32,6 +32,7 @@ jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( )); const mockConfigMutation = jest.fn(); + const tumbleweed: Product = { id: "Tumbleweed", name: "openSUSE Tumbleweed", @@ -48,7 +49,6 @@ const microOs: Product = { registration: "no", }; -const mockConfigMutation = jest.fn(); let mockSelectedProduct: Product; jest.mock("~/queries/software", () => ({ From 2c87855a30b2aa6a165e5a5cceafc28d1002b861 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 9 Jan 2025 07:41:57 +0100 Subject: [PATCH 46/47] copy proper location of product credentials --- service/lib/agama/registration.rb | 16 +++++++++++----- service/test/agama/registration_test.rb | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb index 658a1c9c83..e47463131c 100644 --- a/service/lib/agama/registration.rb +++ b/service/lib/agama/registration.rb @@ -142,12 +142,18 @@ def deregister def finish return unless reg_code - files = [GLOBAL_CREDENTIALS_PATH] - files << credentials_path(@credentials_file) if @credentials_file + files = [[ + GLOBAL_CREDENTIALS_PATH, File.join(Yast::Installation.destdir, GLOBAL_CREDENTIALS_PATH) + ]] + if @credentials_file + files << [ + File.join(TARGET_DIR, credentials_path(@credentials_file)), + File.join(Yast::Installation.destdir, credentials_path(@credentials_file)), + ] + end - files.each do |file| - dest = File.join(Yast::Installation.destdir, file) - FileUtils.cp(file, dest) + files.each do |files| + FileUtils.cp(*files) end end diff --git a/service/test/agama/registration_test.rb b/service/test/agama/registration_test.rb index 2670ae3856..1efb802ab5 100644 --- a/service/test/agama/registration_test.rb +++ b/service/test/agama/registration_test.rb @@ -406,7 +406,7 @@ end it "copies product credentials file" do - expect(::FileUtils).to receive(:cp).with("/etc/zypp/credentials.d/test", + expect(::FileUtils).to receive(:cp).with("/run/agama/zypp/etc/zypp/credentials.d/test", "/mnt/etc/zypp/credentials.d/test") subject.finish From 5014eff6a8ff21413e584520530a68993fee1436 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 9 Jan 2025 08:59:28 +0100 Subject: [PATCH 47/47] make rubocop happy --- service/lib/agama/registration.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb index e47463131c..7eb4ef60be 100644 --- a/service/lib/agama/registration.rb +++ b/service/lib/agama/registration.rb @@ -148,12 +148,12 @@ def finish if @credentials_file files << [ File.join(TARGET_DIR, credentials_path(@credentials_file)), - File.join(Yast::Installation.destdir, credentials_path(@credentials_file)), + File.join(Yast::Installation.destdir, credentials_path(@credentials_file)) ] end - files.each do |files| - FileUtils.cp(*files) + files.each do |src_dest| + FileUtils.cp(*src_dest) end end