From b48ae86b033d4ebd8441685d1c100989de696f41 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Wed, 4 Oct 2023 14:43:14 +0100
Subject: [PATCH 01/97] [service] Some renames in Software D-Bus API
---
service/lib/agama/dbus/software/manager.rb | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index 67f1e5cbf7..78bf5ce0a1 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -58,16 +58,9 @@ def initialize(backend, logger)
private_constant :SOFTWARE_INTERFACE
dbus_interface SOFTWARE_INTERFACE do
- dbus_reader :available_base_products, "a(ssa{sv})"
+ dbus_reader :available_products, "a(ssa{sv})"
- dbus_reader :selected_base_product, "s"
-
- # documented way to be able to write to patterns and trigger signal
- attr_writer :selected_patterns
-
- # selected patterns is hash with pattern name as id and 0 for user selected and
- # 1 for auto selected. Can be extended in future e.g. for mandatory patterns
- dbus_attr_reader :selected_patterns, "a{sy}"
+ dbus_reader :selected_product, "s"
dbus_method :SelectProduct, "in ProductID:s" do |product_id|
old_product_id = backend.product
@@ -101,6 +94,13 @@ def initialize(backend, logger)
]
end
+ # documented way to be able to write to patterns and trigger signal
+ attr_writer :selected_patterns
+
+ # selected patterns is hash with pattern name as id and 0 for user selected and
+ # 1 for auto selected. Can be extended in future e.g. for mandatory patterns
+ dbus_attr_reader :selected_patterns, "a{sy}"
+
dbus_method(:AddPattern, "in id:s") { |p| backend.add_pattern(p) }
dbus_method(:RemovePattern, "in id:s") { |p| backend.remove_pattern(p) }
dbus_method(:SetUserPatterns, "in ids:as") { |ids| backend.user_patterns = ids }
From 8bbd5673e3179677340567403720781cc664fef2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Wed, 4 Oct 2023 14:43:44 +0100
Subject: [PATCH 02/97] [service] WIP Registration D-Bus API
---
service/lib/agama/dbus/software/manager.rb | 39 ++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index 78bf5ce0a1..16e49a3f38 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -159,6 +159,45 @@ def finish
busy_while { backend.finish }
end
+ REGISTRATION_INTERFACE = "org.opensuse.Agama1.Registration"
+ private_constant :REGISTRATION_INTERFACE
+
+ dbus_interface REGISTRATION_INTERFACE do
+ dbus_reader :reg_code, "s"
+
+ dbus_reader :email, "s"
+
+ dbus_reader :state, "u"
+
+ dbus_method :Register, "in reg_code:s, in options:a{sv}, out result:u" do
+ |reg_code, options|
+ backend.registration.register(reg_code, email: options["Email"])
+ # map errors to exit codes?
+ 0
+ end
+
+ dbus_method :Deregister, "out result:u" do
+ backend.registration.deregister
+ # map errors to exit codes?
+ 0
+ end
+ end
+
+ def reg_code
+ backend.registration.reg_code || ""
+ end
+
+ def email
+ backend.registration.email || ""
+ end
+
+ # Replace #State by #IsDisabled and #isOptional ?
+ def state
+ return 0 if backend.registration.disabled?
+ return 1 if backend.registration.optional?
+ return 2 unless backend.registration.optional?
+ end
+
private
# @return [Agama::Software]
From c39c3768bb26deca434da63f279a2b075663f624 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Wed, 4 Oct 2023 17:16:11 +0200
Subject: [PATCH 03/97] add registration mock class
---
service/lib/agama/registration.rb | 43 +++++++++++++++++++++++++++++++
1 file changed, 43 insertions(+)
create mode 100644 service/lib/agama/registration.rb
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
new file mode 100644
index 0000000000..e121b421fe
--- /dev/null
+++ b/service/lib/agama/registration.rb
@@ -0,0 +1,43 @@
+# 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 version 2 of the GNU General Public License as published
+# by the Free Software Foundation.
+#
+# 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.
+
+module Agama
+ # Handles everything related to registration of system to SCC, RMT or similar
+ class Registration
+ attr_reader :reg_code
+ attr_reader :email
+
+ # initializes registration with instance of software manager for query about products
+ def initialize(software_manager)
+ @software = software_manager
+ end
+
+ def register(code, email: nil)
+ end
+
+ def deregister
+ end
+
+ def disabled?
+ end
+
+ def optional?
+ end
+ end
+end
From e50337b05061538de788d02aa4248ae22886e1e8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Thu, 5 Oct 2023 15:03:33 +0100
Subject: [PATCH 04/97] [service] Replace Validation by Issues interface
---
service/lib/agama/dbus/clients/software.rb | 10 +--
service/lib/agama/dbus/software/manager.rb | 21 +++---
service/lib/agama/manager.rb | 2 +-
service/lib/agama/software/manager.rb | 81 ++++++++++++++++------
service/lib/agama/software/proposal.rb | 36 +++++-----
5 files changed, 98 insertions(+), 52 deletions(-)
diff --git a/service/lib/agama/dbus/clients/software.rb b/service/lib/agama/dbus/clients/software.rb
index 8b953d705d..f2191118b7 100644
--- a/service/lib/agama/dbus/clients/software.rb
+++ b/service/lib/agama/dbus/clients/software.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Copyright (c) [2022] SUSE LLC
+# Copyright (c) [2022-2023] SUSE LLC
#
# All Rights Reserved.
#
@@ -20,18 +20,18 @@
# find current contact information at www.suse.com.
require "agama/dbus/clients/base"
-require "agama/dbus/clients/with_service_status"
+require "agama/dbus/clients/with_issues"
require "agama/dbus/clients/with_progress"
-require "agama/dbus/clients/with_validation"
+require "agama/dbus/clients/with_service_status"
module Agama
module DBus
module Clients
# D-Bus client for software configuration
class Software < Base
- include WithServiceStatus
+ include WithIssues
include WithProgress
- include WithValidation
+ include WithServiceStatus
TYPES = [:package, :pattern].freeze
private_constant :TYPES
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index 16e49a3f38..26ba2d368e 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Copyright (c) [2022] SUSE LLC
+# Copyright (c) [2022-2023] SUSE LLC
#
# All Rights Reserved.
#
@@ -21,12 +21,12 @@
require "dbus"
require "agama/dbus/base_object"
-require "agama/dbus/with_service_status"
require "agama/dbus/clients/locale"
require "agama/dbus/clients/network"
+require "agama/dbus/interfaces/issues"
require "agama/dbus/interfaces/progress"
require "agama/dbus/interfaces/service_status"
-require "agama/dbus/interfaces/validation"
+require "agama/dbus/with_service_status"
module Agama
module DBus
@@ -36,7 +36,7 @@ class Manager < BaseObject
include WithServiceStatus
include Interfaces::Progress
include Interfaces::ServiceStatus
- include Interfaces::Validation
+ include Interfaces::Issues
PATH = "/org/opensuse/Agama/Software1"
private_constant :PATH
@@ -54,6 +54,13 @@ def initialize(backend, logger)
@selected_patterns = {}
end
+ # List of issues, see {DBus::Interfaces::Issues}
+ #
+ # @return [Array]
+ def issues
+ backend.issues
+ end
+
SOFTWARE_INTERFACE = "org.opensuse.Agama.Software1"
private_constant :SOFTWARE_INTERFACE
@@ -73,7 +80,6 @@ def initialize(backend, logger)
logger.info "Selecting product #{product_id}"
select_product(product_id)
dbus_properties_changed(SOFTWARE_INTERFACE, { "SelectedBaseProduct" => product_id }, [])
- update_validation # as different product means different software selection
end
# value of result hash is category, description, icon, summary and order
@@ -140,13 +146,10 @@ def select_product(product_id)
def probe
busy_while { backend.probe }
-
- update_validation # probe do force proposal
end
def propose
busy_while { backend.propose }
- update_validation
nil # explicit nil as return value
end
@@ -218,6 +221,8 @@ def register_callbacks
backend.on_selected_patterns_change do
self.selected_patterns = compute_patterns
end
+
+ backend.on_issues_change { issues_properties_changed }
end
USER_SELECTED_PATTERN = 0
diff --git a/service/lib/agama/manager.rb b/service/lib/agama/manager.rb
index 4f445aad79..b11b5960cb 100644
--- a/service/lib/agama/manager.rb
+++ b/service/lib/agama/manager.rb
@@ -200,7 +200,7 @@ def on_services_status_change(&block)
#
# @return [Boolean]
def valid?
- [users, software].all?(&:valid?) && !storage.errors?
+ users.valid? && !software.errors? && !storage.errors?
end
# Collects the logs and stores them into an archive
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index e1bcd916b8..20076c732e 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Copyright (c) [2021] SUSE LLC
+# Copyright (c) [2021-2023] SUSE LLC
#
# All Rights Reserved.
#
@@ -19,18 +19,19 @@
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.
-require "yast"
require "fileutils"
-require "agama/config"
-require "agama/helpers"
-require "agama/with_progress"
-require "agama/validation_error"
+require "yast"
+require "yast2/arch_filter"
require "y2packager/product"
require "y2packager/resolvable"
-require "yast2/arch_filter"
+require "agama/config"
+require "agama/helpers"
+require "agama/issue"
require "agama/software/callbacks"
require "agama/software/proposal"
require "agama/software/repositories_manager"
+require "agama/with_progress"
+require "agama/with_issues"
Yast.import "Package"
Yast.import "Packages"
@@ -43,6 +44,7 @@ module Software
# This class is responsible for software handling
class Manager
include Helpers
+ include WithIssues
include WithProgress
GPG_KEYS_GLOB = "/usr/lib/rpm/gnupg/keys/gpg-*"
@@ -78,6 +80,7 @@ def initialize(config, logger)
# patterns selected by user
@user_patterns = []
@selected_patterns_change_callbacks = []
+ update_issues
end
def select_product(name)
@@ -87,6 +90,7 @@ def select_product(name)
@config.pick_product(name)
@product = name
repositories.delete_all
+ update_issues
end
def probe
@@ -109,6 +113,7 @@ def probe
progress.step("Calculating the software proposal") { propose }
Yast::Stage.Set("initial")
+ update_issues
end
def initialize_target_repos
@@ -122,25 +127,11 @@ def propose
proposal.languages = languages
select_resolvables
result = proposal.calculate
+ update_issues
logger.info "Proposal result: #{result.inspect}"
result
end
- # Returns the errors related to the software proposal
- #
- # * Repositories that could not be probed are reported as errors.
- # * If none of the repositories could be probed, do not report missing
- # patterns and/or packages. Those issues does not make any sense if there
- # are no repositories to install from.
- def validate
- errors = repositories.disabled.map do |repo|
- ValidationError.new("Could not read the repository #{repo.name}")
- end
- return errors if repositories.enabled.empty?
-
- errors + proposal.errors
- end
-
# Installs the packages to the target system
def install
steps = proposal.packages_count
@@ -346,6 +337,52 @@ def select_resolvables
def selected_patterns_changed
@selected_patterns_change_callbacks.each(&:call)
end
+
+ # Updates the list of issues.
+ def update_issues
+ self.issues = current_issues
+ end
+
+ # List of current issues.
+ #
+ # @return [Array]
+ def current_issues
+ return [missing_product_issue] unless product
+
+ issues = repos_issues
+
+ # If none of the repositories could be probed, then do not report missing patterns and/or
+ # packages. Those issues does not make any sense if there are no repositories to install
+ # from.
+ issues += proposal.issues if repositories.enabled.any?
+
+ # TODO
+ # issues += registration.issues
+
+ issues
+ end
+
+ # Issue when a product is missing
+ #
+ # @return [Agama::Issue]
+ def missing_product_issue
+ Issue.new("Product not selected yet",
+ source: Issue::Source::CONFIG,
+ severity: Issue::Severity::ERROR)
+ end
+
+ # Issues related to the software proposal.
+ #
+ # Repositories that could not be probed are reported as errors.
+ #
+ # @return [Array]
+ def repos_issues
+ issues = repositories.disabled.map do |repo|
+ Issue.new("Could not read the repository #{repo.name}",
+ source: Issue::Source::SYSTEM,
+ severity: Issue::Severity::WARN)
+ end
+ end
end
end
end
diff --git a/service/lib/agama/software/proposal.rb b/service/lib/agama/software/proposal.rb
index c4310967c2..def5bc1e7b 100644
--- a/service/lib/agama/software/proposal.rb
+++ b/service/lib/agama/software/proposal.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Copyright (c) [2022] SUSE LLC
+# Copyright (c) [2022-2023] SUSE LLC
#
# All Rights Reserved.
#
@@ -20,7 +20,7 @@
# find current contact information at www.suse.com.
require "yast"
-require "agama/validation_error"
+require "agama/issue"
Yast.import "Stage"
Yast.import "Installation"
@@ -47,7 +47,7 @@ module Software
# proposal.add_resolvables("agama", :pattern, ["enhanced_base"])
# proposal.languages = ["en_US", "de_DE"]
# proposal.calculate #=> true
- # proposal.errors #=> []
+ # proposal.issues #=> []
class Proposal
# @return [String,nil] Base product
attr_accessor :base_product
@@ -55,15 +55,15 @@ class Proposal
# @return [Array] List of languages to install
attr_accessor :languages
- # @return [Array] List of errors from the calculated proposal
- attr_reader :errors
+ # @return [Array] List of issues from the calculated proposal
+ attr_reader :issues
# Constructor
#
# @param logger [Logger]
def initialize(logger: nil)
@logger = logger || Logger.new($stdout)
- @errors = []
+ @issues = []
@base_product = nil
@calculated = false
end
@@ -84,14 +84,14 @@ def set_resolvables(unique_id, type, resolvables, optional: false)
#
# @return [Boolean]
def calculate
- @errors.clear
+ @issues.clear
initialize_target
select_base_product
proposal = Yast::Packages.Proposal(force_reset = true, reinit = false, _simple = true)
solve_dependencies
@calculated = true
- @errors = find_errors(proposal)
+ @issues = find_issues(proposal)
valid?
end
@@ -115,7 +115,7 @@ def packages_size
#
# @return [Boolean]
def valid?
- @calculated && @errors.empty?
+ @calculated && @issues.empty?
end
private
@@ -148,21 +148,25 @@ def select_base_product
end
end
- # Returns the errors from the attempt to create a proposal
+ # Returns the issues from the attempt to create a proposal.
#
- # It collects errors from:
+ # It collects issues from:
#
- # * The proposal result
- # * The last solver execution
+ # * The proposal result.
+ # * The last solver execution.
#
# @param proposal_result [Hash] Proposal result; it might contain a "warning" key with warning
# messages.
- # @return [Array] List of errors
- def find_errors(proposal_result)
+ # @return [Array] List of issues.
+ def find_issues(proposal_result)
msgs = []
msgs.concat(warning_messages(proposal_result))
msgs.concat(solver_messages)
- msgs.map { |m| ValidationError.new(m) }
+ msgs.map do |msg|
+ Issue.new(msg,
+ source: Issue::Source::CONFIG,
+ severity: Issue::Severity::WARN)
+ end
end
# Runs the solver to satisfy the solve_dependencies
From c5535f774bb3b763270c299d8de2192d73aef4c1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Thu, 5 Oct 2023 15:04:09 +0100
Subject: [PATCH 05/97] [service] Fix names
---
service/lib/agama/dbus/clients/software.rb | 8 ++++----
service/lib/agama/dbus/software/manager.rb | 4 ++--
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/service/lib/agama/dbus/clients/software.rb b/service/lib/agama/dbus/clients/software.rb
index f2191118b7..5d9321698a 100644
--- a/service/lib/agama/dbus/clients/software.rb
+++ b/service/lib/agama/dbus/clients/software.rb
@@ -55,7 +55,7 @@ def service_name
#
# @return [Array>] name and display name of each product
def available_products
- dbus_object["org.opensuse.Agama.Software1"]["AvailableBaseProducts"].map do |l|
+ dbus_object["org.opensuse.Agama.Software1"]["AvailableProducts"].map do |l|
l[0..1]
end
end
@@ -64,7 +64,7 @@ def available_products
#
# @return [String, nil] name of the product
def selected_product
- product = dbus_object["org.opensuse.Agama.Software1"]["SelectedBaseProduct"]
+ product = dbus_object["org.opensuse.Agama.Software1"]["SelectedProduct"]
return nil if product.empty?
product
@@ -170,8 +170,8 @@ def remove_resolvables(unique_id, type, resolvables, optional: false)
# @param block [Proc] Callback to run when a product is selected
def on_product_selected(&block)
on_properties_change(dbus_object) do |_, changes, _|
- base_product = changes["SelectedBaseProduct"]
- block.call(base_product) unless base_product.nil?
+ product = changes["SelectedProduct"]
+ block.call(product) unless product.nil?
end
end
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index 26ba2d368e..9aed6a01ef 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -127,7 +127,7 @@ def issues
dbus_method(:Finish) { finish }
end
- def available_base_products
+ def available_products
backend.products.map do |id, data|
[id, data["name"], { "description" => data["description"] }].freeze
end
@@ -136,7 +136,7 @@ def available_base_products
# Returns the selected base product
#
# @return [String] Product ID or an empty string if no product is selected
- def selected_base_product
+ def selected_product
backend.product || ""
end
From 851e85f8a2c37fa59056df5f83ad1d2f0d0c080c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 6 Oct 2023 11:49:55 +0100
Subject: [PATCH 06/97] [service] WIP return error number and message
---
service/lib/agama/dbus/software/manager.rb | 62 ++++++++++++++++------
1 file changed, 45 insertions(+), 17 deletions(-)
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index 9aed6a01ef..9ae11ab3bd 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -20,6 +20,7 @@
# find current contact information at www.suse.com.
require "dbus"
+require "suse/connect"
require "agama/dbus/base_object"
require "agama/dbus/clients/locale"
require "agama/dbus/clients/network"
@@ -166,24 +167,17 @@ def finish
private_constant :REGISTRATION_INTERFACE
dbus_interface REGISTRATION_INTERFACE do
- dbus_reader :reg_code, "s"
+ dbus_reader(:reg_code, "s")
- dbus_reader :email, "s"
+ dbus_reader(:email, "s")
- dbus_reader :state, "u"
+ dbus_reader(:state, "u")
- dbus_method :Register, "in reg_code:s, in options:a{sv}, out result:u" do
- |reg_code, options|
- backend.registration.register(reg_code, email: options["Email"])
- # map errors to exit codes?
- 0
+ dbus_method(:Register, "in reg_code:s, in options:a{sv}, out result:(us)") do |*args|
+ [register(*args)]
end
- dbus_method :Deregister, "out result:u" do
- backend.registration.deregister
- # map errors to exit codes?
- 0
- end
+ dbus_method(:Deregister, "out result:(us)") { [deregister] }
end
def reg_code
@@ -194,11 +188,21 @@ def email
backend.registration.email || ""
end
- # Replace #State by #IsDisabled and #isOptional ?
def state
- return 0 if backend.registration.disabled?
- return 1 if backend.registration.optional?
- return 2 unless backend.registration.optional?
+ # TODO
+ 0
+ end
+
+ def register(reg_code, options)
+ connect_result do
+ backend.registration.register(reg_code, email: options["Email"])
+ end
+ end
+
+ def deregister
+ connect_result do
+ backend.registration.deregister
+ end
end
private
@@ -235,6 +239,30 @@ def compute_patterns
patterns
end
+
+ # @return [Array]
+ def connect_result(&block)
+ block.call
+ [0, ""]
+ rescue SocketError => e
+ logger.error("Network error: #{e}")
+ [1, "Connection to registration server failed (network error)"]
+ rescue Timeout::Error => e
+ logger.error("Timeout error: #{e}")
+ [2, "Connection to registration server failed (timeout)"]
+ rescue SUSE::Connect::ApiError => e
+ [3, "Connection to registration server failed"]
+ rescue SUSE::Connect::MissingSccCredentialsFile => e
+ [4, "Connection to registration server failed (missing credentials)"]
+ rescue SUSE::Connect::MalformedSccCredentialsFile => e
+ [5, "Connection to registration server failed (incorrect credentials)"]
+ rescue OpenSSL::SSL::SSLError => e
+ [6, "Connection to registration server failed (invalid certificate)"]
+ rescue JSON::ParserError => e
+ [7, "Connection to registration server failed"]
+ rescue StandardError => e
+ [8, "Connection to registration server failed"]
+ end
end
end
end
From 9300a72c88c3829169c6c60e71b36560c1137119 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Fri, 6 Oct 2023 13:09:37 +0200
Subject: [PATCH 07/97] initial forst working POC
---
service/lib/agama/registration.rb | 30 ++++++++++++++++++++++++++-
service/lib/agama/software/manager.rb | 4 ++++
2 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index e121b421fe..e145d7275d 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -17,6 +17,12 @@
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.
+require "yast"
+require "openstruct"
+
+require "registration/registration"
+require "y2packager/new_repository_setup"
+
module Agama
# Handles everything related to registration of system to SCC, RMT or similar
class Registration
@@ -26,9 +32,26 @@ class Registration
# initializes registration with instance of software manager for query about products
def initialize(software_manager)
@software = software_manager
+ @on_state_change_callbacks = []
end
- def register(code, email: nil)
+ def register(code, email: "")
+ target_distro = "ALP-Dolomite-1-x86_64" # TODO read it
+ registration = Registration::Registration.new # intentional no url yet
+ registration.register(email, code, target_distro)
+ # TODO: fill it properly for scc
+ target_product = OpenStruct.new(
+ arch: "x86_64",
+ identifier: "ALP-Dolomite",
+ version: "1.0",
+ release_type: "ALPHA"
+ )
+ activate_params = {}
+ service = SUSE::Connect::YaST.activate_product(target_product, activate_params, email)
+ Y2Packager::NewRepositorySetup.instance.add_service(service.name)
+
+ @reg_code = code
+ @email = email
end
def deregister
@@ -39,5 +62,10 @@ def disabled?
def optional?
end
+
+ # callback when state changed like when different product is selected
+ def on_state_change(&block)
+ @on_state_change_callbacks << block
+ end
end
end
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index 20076c732e..6721fd489f 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -252,6 +252,10 @@ def used_disk_space
Yast::String.FormatSizeWithPrecision(proposal.packages_size, 1, true)
end
+ def registration
+ @registration ||= Registration.new(self)
+ end
+
private
def proposal
From 12102fdbe9d8c9c078a9a293c2649d5f2cbb634e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Mon, 9 Oct 2023 11:52:48 +0100
Subject: [PATCH 08/97] [service] More fixes to adapt to changes in D-Bus API
---
service/lib/agama/dbus/software/manager.rb | 4 +--
service/lib/agama/dbus/software/proposal.rb | 15 +-------
service/lib/agama/dbus/software_service.rb | 13 ++-----
service/lib/agama/software/manager.rb | 2 +-
service/lib/agama/software/proposal.rb | 40 +++++++++++----------
service/lib/agama/with_issues.rb | 8 +++++
6 files changed, 36 insertions(+), 46 deletions(-)
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index 9ae11ab3bd..a9fc91dd4c 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -80,7 +80,7 @@ def issues
logger.info "Selecting product #{product_id}"
select_product(product_id)
- dbus_properties_changed(SOFTWARE_INTERFACE, { "SelectedBaseProduct" => product_id }, [])
+ dbus_properties_changed(SOFTWARE_INTERFACE, { "SelectedProduct" => product_id }, [])
end
# value of result hash is category, description, icon, summary and order
@@ -260,8 +260,6 @@ def connect_result(&block)
[6, "Connection to registration server failed (invalid certificate)"]
rescue JSON::ParserError => e
[7, "Connection to registration server failed"]
- rescue StandardError => e
- [8, "Connection to registration server failed"]
end
end
end
diff --git a/service/lib/agama/dbus/software/proposal.rb b/service/lib/agama/dbus/software/proposal.rb
index 13cd847d0a..cd0e3587d0 100644
--- a/service/lib/agama/dbus/software/proposal.rb
+++ b/service/lib/agama/dbus/software/proposal.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Copyright (c) [2022] SUSE LLC
+# Copyright (c) [2022-2023] SUSE LLC
#
# All Rights Reserved.
#
@@ -46,8 +46,6 @@ class Proposal < ::DBus::Object
# @param logger [Logger]
def initialize(logger)
@logger = logger
- @on_change_callbacks = []
-
super(PATH)
end
@@ -55,7 +53,6 @@ def initialize(logger)
dbus_method :AddResolvables,
"in Id:s, in Type:y, in Resolvables:as, in Optional:b" do |id, type, resolvables, opt|
Yast::PackagesProposal.AddResolvables(id, TYPES[type], resolvables, optional: opt)
- notify_change!
end
dbus_method :GetResolvables,
@@ -66,28 +63,18 @@ def initialize(logger)
dbus_method :SetResolvables,
"in Id:s, in Type:y, in Resolvables:as, in Optional:b" do |id, type, resolvables, opt|
Yast::PackagesProposal.SetResolvables(id, TYPES[type], resolvables, optional: opt)
- notify_change!
end
dbus_method :RemoveResolvables,
"in Id:s, in Type:y, in Resolvables:as, in Optional:b" do |id, type, resolvables, opt|
Yast::PackagesProposal.RemoveResolvables(id, TYPES[type], resolvables, optional: opt)
- notify_change!
end
end
- def on_change(&block)
- @on_change_callbacks << block
- end
-
private
# @return [Logger]
attr_reader :logger
-
- def notify_change!
- @on_change_callbacks.each(&:call)
- end
end
end
end
diff --git a/service/lib/agama/dbus/software_service.rb b/service/lib/agama/dbus/software_service.rb
index 01f573aad8..a2d7100ec9 100644
--- a/service/lib/agama/dbus/software_service.rb
+++ b/service/lib/agama/dbus/software_service.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Copyright (c) [2022] SUSE LLC
+# Copyright (c) [2022-2023] SUSE LLC
#
# All Rights Reserved.
#
@@ -82,17 +82,10 @@ def service
# @return [Array<::DBus::Object>]
def dbus_objects
@dbus_objects ||= [
- dbus_software_manager,
- Agama::DBus::Software::Proposal.new(logger).tap do |proposal|
- proposal.on_change { dbus_software_manager.update_validation }
- end
+ Agama::DBus::Software::Manager.new(@backend, logger),
+ Agama::DBus::Software::Proposal.new(logger)
]
end
-
- # @return [Agama::DBus::Software::Manager]
- def dbus_software_manager
- @dbus_software_manager ||= Agama::DBus::Software::Manager.new(@backend, logger)
- end
end
end
end
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index 6721fd489f..00d974fafc 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -384,7 +384,7 @@ def repos_issues
issues = repositories.disabled.map do |repo|
Issue.new("Could not read the repository #{repo.name}",
source: Issue::Source::SYSTEM,
- severity: Issue::Severity::WARN)
+ severity: Issue::Severity::ERROR)
end
end
end
diff --git a/service/lib/agama/software/proposal.rb b/service/lib/agama/software/proposal.rb
index def5bc1e7b..45470fc593 100644
--- a/service/lib/agama/software/proposal.rb
+++ b/service/lib/agama/software/proposal.rb
@@ -21,6 +21,7 @@
require "yast"
require "agama/issue"
+require "agama/with_issues"
Yast.import "Stage"
Yast.import "Installation"
@@ -49,23 +50,20 @@ module Software
# proposal.calculate #=> true
# proposal.issues #=> []
class Proposal
+ include WithIssues
+
# @return [String,nil] Base product
attr_accessor :base_product
# @return [Array] List of languages to install
attr_accessor :languages
- # @return [Array] List of issues from the calculated proposal
- attr_reader :issues
-
# Constructor
#
# @param logger [Logger]
def initialize(logger: nil)
@logger = logger || Logger.new($stdout)
- @issues = []
@base_product = nil
- @calculated = false
end
# Adds the given list of resolvables to the proposal
@@ -84,14 +82,12 @@ def set_resolvables(unique_id, type, resolvables, optional: false)
#
# @return [Boolean]
def calculate
- @issues.clear
initialize_target
select_base_product
- proposal = Yast::Packages.Proposal(force_reset = true, reinit = false, _simple = true)
+ @proposal = Yast::Packages.Proposal(force_reset = true, reinit = false, _simple = true)
solve_dependencies
- @calculated = true
- @issues = find_issues(proposal)
+ update_issues
valid?
end
@@ -115,7 +111,7 @@ def packages_size
#
# @return [Boolean]
def valid?
- @calculated && @issues.empty?
+ proposal && !errors?
end
private
@@ -123,6 +119,11 @@ def valid?
# @return [Logger]
attr_reader :logger
+ # Proposal result
+ #
+ # @return [Hash, nil] nil if not calculated yet.
+ attr_reader :proposal
+
# Initializes the target, closing the previous one
def initialize_target
Yast::Pkg.TargetFinish # ensure that previous target is closed
@@ -148,25 +149,28 @@ def select_base_product
end
end
- # Returns the issues from the attempt to create a proposal.
+ # Updates the issues from the attempt to create a proposal.
#
# It collects issues from:
#
# * The proposal result.
# * The last solver execution.
#
- # @param proposal_result [Hash] Proposal result; it might contain a "warning" key with warning
- # messages.
- # @return [Array] List of issues.
- def find_issues(proposal_result)
+ # @return [Array]
+ def update_issues
+ self.issues = []
+ return unless proposal
+
msgs = []
- msgs.concat(warning_messages(proposal_result))
+ msgs.concat(warning_messages(proposal))
msgs.concat(solver_messages)
- msgs.map do |msg|
+ issues = msgs.map do |msg|
Issue.new(msg,
source: Issue::Source::CONFIG,
- severity: Issue::Severity::WARN)
+ severity: Issue::Severity::ERROR)
end
+
+ self.issues = issues
end
# Runs the solver to satisfy the solve_dependencies
diff --git a/service/lib/agama/with_issues.rb b/service/lib/agama/with_issues.rb
index 950ddce6bb..d682200760 100644
--- a/service/lib/agama/with_issues.rb
+++ b/service/lib/agama/with_issues.rb
@@ -29,6 +29,14 @@ def issues
@issues || []
end
+ def errors
+ issues.select(&:error?)
+ end
+
+ def errors?
+ errors.any?
+ end
+
# Sets the list of current issues
#
# @param issues [Array]
From 78c4df01fa4c511c095f52896c1ed9c09d9e4761 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Mon, 9 Oct 2023 11:58:11 +0100
Subject: [PATCH 09/97] [web] Adapt UI to D-Bus API changes
---
web/src/client/software.js | 14 +++++++-------
web/src/client/software.test.js | 4 ++--
web/src/components/overview/SoftwareSection.jsx | 4 ++--
.../components/overview/SoftwareSection.test.jsx | 8 ++++----
4 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/web/src/client/software.js b/web/src/client/software.js
index cc6328eb23..0efbb6c07c 100644
--- a/web/src/client/software.js
+++ b/web/src/client/software.js
@@ -22,7 +22,7 @@
// @ts-check
import DBusClient from "./dbus";
-import { WithStatus, WithProgress, WithValidation } from "./mixins";
+import { WithIssues, WithStatus, WithProgress } from "./mixins";
const SOFTWARE_SERVICE = "org.opensuse.Agama.Software1";
const SOFTWARE_IFACE = "org.opensuse.Agama.Software1";
@@ -65,7 +65,7 @@ class SoftwareBaseClient {
*/
async getProducts() {
const proxy = await this.client.proxy(SOFTWARE_IFACE);
- return proxy.AvailableBaseProducts.map(product => {
+ return proxy.AvailableProducts.map(product => {
const [id, name, meta] = product;
return { id, name, description: meta.description?.v };
});
@@ -89,10 +89,10 @@ class SoftwareBaseClient {
async getSelectedProduct() {
const products = await this.getProducts();
const proxy = await this.client.proxy(SOFTWARE_IFACE);
- if (proxy.SelectedBaseProduct === "") {
+ if (proxy.SelectedProduct === "") {
return null;
}
- return products.find(product => product.id === proxy.SelectedBaseProduct);
+ return products.find(product => product.id === proxy.SelectedProduct);
}
/**
@@ -112,8 +112,8 @@ class SoftwareBaseClient {
*/
onProductChange(handler) {
return this.client.onObjectChanged(SOFTWARE_PATH, SOFTWARE_IFACE, changes => {
- if ("SelectedBaseProduct" in changes) {
- const selected = changes.SelectedBaseProduct.v.toString();
+ if ("SelectedProduct" in changes) {
+ const selected = changes.SelectedProduct.v.toString();
handler(selected);
}
});
@@ -123,7 +123,7 @@ class SoftwareBaseClient {
/**
* Allows getting the list the available products and selecting one for installation.
*/
-class SoftwareClient extends WithValidation(
+class SoftwareClient extends WithIssues(
WithProgress(
WithStatus(SoftwareBaseClient, SOFTWARE_PATH), SOFTWARE_PATH
), SOFTWARE_PATH
diff --git a/web/src/client/software.test.js b/web/src/client/software.test.js
index f8460beb43..5e12a2ff54 100644
--- a/web/src/client/software.test.js
+++ b/web/src/client/software.test.js
@@ -30,11 +30,11 @@ const SOFTWARE_IFACE = "org.opensuse.Agama.Software1";
const softProxy = {
wait: jest.fn(),
- AvailableBaseProducts: [
+ AvailableProducts: [
["MicroOS", "openSUSE MicroOS", {}],
["Tumbleweed", "openSUSE Tumbleweed", {}]
],
- SelectedBaseProduct: "MicroOS"
+ SelectedProduct: "MicroOS"
};
beforeEach(() => {
diff --git a/web/src/components/overview/SoftwareSection.jsx b/web/src/components/overview/SoftwareSection.jsx
index f8f93bc8fb..cc2ab451b4 100644
--- a/web/src/components/overview/SoftwareSection.jsx
+++ b/web/src/components/overview/SoftwareSection.jsx
@@ -83,7 +83,7 @@ export default function SoftwareSection({ showErrors }) {
useEffect(() => {
const updateProposal = async () => {
- const errors = await cancellablePromise(client.getValidationErrors());
+ const errors = await cancellablePromise(client.getIssues());
const size = await cancellablePromise(client.getUsedSpace());
dispatch({ type: "UPDATE_PROPOSAL", payload: { errors, size } });
@@ -155,7 +155,7 @@ export default function SoftwareSection({ showErrors }) {
title={_("Software")}
icon="apps"
loading={state.busy}
- errors={errors}
+ errors={errors.map(e => ({ message: e.description }))}
>
diff --git a/web/src/components/overview/SoftwareSection.test.jsx b/web/src/components/overview/SoftwareSection.test.jsx
index 1299e09bda..d21df0e84c 100644
--- a/web/src/components/overview/SoftwareSection.test.jsx
+++ b/web/src/components/overview/SoftwareSection.test.jsx
@@ -31,7 +31,7 @@ jest.mock("~/client");
let getStatusFn = jest.fn().mockResolvedValue(IDLE);
let getProgressFn = jest.fn().mockResolvedValue({});
-let getValidationErrorsFn = jest.fn().mockResolvedValue([]);
+let getIssuesFn = jest.fn().mockResolvedValue([]);
beforeEach(() => {
createClient.mockImplementation(() => {
@@ -39,7 +39,7 @@ beforeEach(() => {
software: {
getStatus: getStatusFn,
getProgress: getProgressFn,
- getValidationErrors: getValidationErrorsFn,
+ getIssues: getIssuesFn,
onStatusChange: noop,
onProgressChange: noop,
getUsedSpace: jest.fn().mockResolvedValue("500 MB")
@@ -48,7 +48,7 @@ beforeEach(() => {
});
});
-describe("when there proposal is calculated", () => {
+describe("when the proposal is calculated", () => {
beforeEach(() => {
getStatusFn = jest.fn().mockResolvedValue(IDLE);
});
@@ -61,7 +61,7 @@ describe("when there proposal is calculated", () => {
describe("and there are errors", () => {
beforeEach(() => {
- getValidationErrorsFn = jest.fn().mockResolvedValue([{ message: "Could not install..." }]);
+ getIssuesFn = jest.fn().mockResolvedValue([{ description: "Could not install..." }]);
});
it("renders a button to refresh the repositories", async () => {
From 8ac969991f27b388dd72069656f8bdf0457605a1 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Mon, 9 Oct 2023 13:37:30 +0200
Subject: [PATCH 10/97] use suse connect directly without yast
---
service/lib/agama/registration.rb | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index e145d7275d..4d9783f996 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -18,9 +18,9 @@
# find current contact information at www.suse.com.
require "yast"
-require "openstruct"
+require "ostruct"
+require "suse/connect"
-require "registration/registration"
require "y2packager/new_repository_setup"
module Agama
@@ -37,7 +37,16 @@ def initialize(software_manager)
def register(code, email: "")
target_distro = "ALP-Dolomite-1-x86_64" # TODO read it
- registration = Registration::Registration.new # intentional no url yet
+ connect_params = {
+ token: code,
+ email: email
+ }
+
+ login, password = SUSE::Connect::YaST.announce_system(connect_params, distro_target)
+ # write the global credentials
+ # TODO: check if we can do it in memory for libzypp
+ SUSE::Connect::YaST.create_credentials_file(login, password)
+
registration.register(email, code, target_distro)
# TODO: fill it properly for scc
target_product = OpenStruct.new(
From cdc9bb4069e7d16971fbe49efc1509d8d16d438a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Mon, 9 Oct 2023 17:02:53 +0100
Subject: [PATCH 11/97] [service] Work on some tests
---
.../test/agama/dbus/clients/software_test.rb | 8 +-
.../test/agama/dbus/software/manager_test.rb | 80 ++++++-----
service/test/agama/manager_test.rb | 5 +-
service/test/agama/software/manager_test.rb | 126 ++++++++++++------
4 files changed, 133 insertions(+), 86 deletions(-)
diff --git a/service/test/agama/dbus/clients/software_test.rb b/service/test/agama/dbus/clients/software_test.rb
index c6ab60127c..c050e71ce7 100644
--- a/service/test/agama/dbus/clients/software_test.rb
+++ b/service/test/agama/dbus/clients/software_test.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Copyright (c) [2022] SUSE LLC
+# Copyright (c) [2022-2023] SUSE LLC
#
# All Rights Reserved.
#
@@ -20,6 +20,7 @@
# find current contact information at www.suse.com.
require_relative "../../../test_helper"
+require_relative "with_issues_examples"
require_relative "with_service_status_examples"
require_relative "with_progress_examples"
require "agama/dbus/clients/software"
@@ -53,7 +54,7 @@
describe "#available_products" do
before do
- allow(software_iface).to receive(:[]).with("AvailableBaseProducts").and_return(
+ allow(software_iface).to receive(:[]).with("AvailableProducts").and_return(
[
["Tumbleweed", "openSUSE Tumbleweed", {}],
["Leap15.3", "openSUSE Leap 15.3", {}]
@@ -71,7 +72,7 @@
describe "#selected_product" do
before do
- allow(software_iface).to receive(:[]).with("SelectedBaseProduct").and_return(product)
+ allow(software_iface).to receive(:[]).with("SelectedProduct").and_return(product)
end
context "when there is no selected product" do
@@ -180,6 +181,7 @@
end
end
+ include_examples "issues"
include_examples "service status"
include_examples "progress"
end
diff --git a/service/test/agama/dbus/software/manager_test.rb b/service/test/agama/dbus/software/manager_test.rb
index f6eda1223b..edc0ae1946 100644
--- a/service/test/agama/dbus/software/manager_test.rb
+++ b/service/test/agama/dbus/software/manager_test.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Copyright (c) [2022] SUSE LLC
+# Copyright (c) [2022-2023] SUSE LLC
#
# All Rights Reserved.
#
@@ -20,10 +20,13 @@
# find current contact information at www.suse.com.
require_relative "../../../test_helper"
-require "agama/dbus/software/manager"
+require "agama/config"
+require "agama/dbus/clients/locale"
+require "agama/dbus/clients/network"
+require "agama/dbus/interfaces/issues"
require "agama/dbus/interfaces/progress"
require "agama/dbus/interfaces/service_status"
-require "agama/dbus/interfaces/validation"
+require "agama/dbus/software/manager"
require "agama/software"
describe Agama::DBus::Software::Manager do
@@ -31,7 +34,15 @@
let(:logger) { Logger.new($stdout, level: :warn) }
- let(:backend) { instance_double(Agama::Software::Manager) }
+ let(:backend) { Agama::Software::Manager.new(config, logger) }
+
+ let(:config) do
+ Agama::Config.new(YAML.safe_load(File.read(config_path)))
+ end
+
+ let(:config_path) do
+ File.join(FIXTURES_PATH, "root_dir", "etc", "agama.yaml")
+ end
let(:progress_interface) { Agama::DBus::Interfaces::Progress::PROGRESS_INTERFACE }
@@ -39,42 +50,44 @@
Agama::DBus::Interfaces::ServiceStatus::SERVICE_STATUS_INTERFACE
end
- let(:validation_interface) { Agama::DBus::Interfaces::Validation::VALIDATION_INTERFACE }
+ let(:issues_interface) { Agama::DBus::Interfaces::Issues::ISSUES_INTERFACE }
before do
- allow_any_instance_of(described_class).to receive(:register_callbacks)
- allow_any_instance_of(described_class).to receive(:register_progress_callbacks)
- allow_any_instance_of(described_class).to receive(:register_service_status_callbacks)
+ allow(Agama::DBus::Clients::Locale).to receive(:new).and_return(locale_client)
+ allow(Agama::DBus::Clients::Network).to receive(:new).and_return(network_client)
+ allow(backend).to receive(:probe)
+ allow(backend).to receive(:propose)
+ allow(backend).to receive(:install)
+ allow(backend).to receive(:finish)
+ allow(subject).to receive(:dbus_properties_changed)
end
- it "defines Progress D-Bus interface" do
- expect(subject.intfs.keys).to include(progress_interface)
+ let(:locale_client) do
+ instance_double(Agama::DBus::Clients::Locale, on_language_selected: nil)
end
- it "defines ServiceStatus D-Bus interface" do
- expect(subject.intfs.keys).to include(service_status_interface)
+ let(:network_client) do
+ instance_double(Agama::DBus::Clients::Network, on_connection_changed: nil)
end
- it "defines Validation D-Bus interface" do
- expect(subject.intfs.keys).to include(validation_interface)
+ it "defines Issues D-Bus interface" do
+ expect(subject.intfs.keys).to include(issues_interface)
+ end
+
+ it "defines Progress D-Bus interface" do
+ expect(subject.intfs.keys).to include(progress_interface)
end
- it "configures callbacks from Progress interface" do
- expect_any_instance_of(described_class).to receive(:register_progress_callbacks)
- subject
+ it "defines ServiceStatus D-Bus interface" do
+ expect(subject.intfs.keys).to include(service_status_interface)
end
- it "configures callbacks from ServiceStatus interface" do
- expect_any_instance_of(described_class).to receive(:register_service_status_callbacks)
- subject
+ it "emits signal when issues changes" do
+ expect(subject).to receive(:issues_properties_changed)
+ backend.issues = []
end
describe "#probe" do
- before do
- allow(subject).to receive(:update_validation)
- allow(backend).to receive(:probe)
- end
-
it "runs the probing, setting the service as busy meanwhile" do
expect(subject.service_status).to receive(:busy)
expect(backend).to receive(:probe)
@@ -82,20 +95,9 @@
subject.probe
end
-
- it "updates validation" do
- expect(subject).to receive(:update_validation)
-
- subject.probe
- end
end
describe "#propose" do
- before do
- allow(subject).to receive(:update_validation)
- allow(backend).to receive(:propose)
- end
-
it "calculates the proposal, setting the service as busy meanwhile" do
expect(subject.service_status).to receive(:busy)
expect(backend).to receive(:propose)
@@ -103,12 +105,6 @@
subject.propose
end
-
- it "updates validation" do
- expect(subject).to receive(:update_validation)
-
- subject.propose
- end
end
describe "#install" do
diff --git a/service/test/agama/manager_test.rb b/service/test/agama/manager_test.rb
index e63d6595f4..9e465df938 100644
--- a/service/test/agama/manager_test.rb
+++ b/service/test/agama/manager_test.rb
@@ -25,6 +25,7 @@
require "agama/config"
require "agama/question"
require "agama/dbus/service_status"
+require "agama/users"
describe Agama::Manager do
subject { described_class.new(config, logger) }
@@ -42,7 +43,7 @@
instance_double(
Agama::DBus::Clients::Software,
probe: nil, install: nil, propose: nil, finish: nil, on_product_selected: nil,
- on_service_status_change: nil, selected_product: product, valid?: true
+ on_service_status_change: nil, selected_product: product, errors?: false
)
end
let(:users) do
@@ -202,7 +203,7 @@
context "when the software configuration is not valid" do
before do
- allow(software).to receive(:valid?).and_return(false)
+ allow(software).to receive(:errors?).and_return(true)
end
it "returns false" do
diff --git a/service/test/agama/software/manager_test.rb b/service/test/agama/software/manager_test.rb
index 1dd557a6f1..befa2c1787 100644
--- a/service/test/agama/software/manager_test.rb
+++ b/service/test/agama/software/manager_test.rb
@@ -20,12 +20,15 @@
# find current contact information at www.suse.com.
require_relative "../../test_helper"
+require_relative "../with_issues_examples"
require_relative "../with_progress_examples"
require_relative File.join(
SRC_PATH, "agama", "dbus", "y2dir", "software", "modules", "PackageCallbacks.rb"
)
require "agama/config"
+require "agama/issue"
require "agama/software/manager"
+require "agama/software/proposal"
require "agama/dbus/clients/questions"
describe Agama::Software::Manager do
@@ -35,15 +38,19 @@
let(:base_url) { "" }
let(:destdir) { "/mnt" }
let(:gpg_keys) { [] }
+
let(:repositories) do
instance_double(
Agama::Software::RepositoriesManager,
add: nil,
load: nil,
delete_all: nil,
- empty?: true
+ empty?: true,
+ enabled: enabled_repos,
+ disabled: disabled_repos
)
end
+
let(:proposal) do
instance_double(
Agama::Software::Proposal,
@@ -51,10 +58,15 @@
calculate: nil,
:languages= => nil,
set_resolvables: nil,
- packages_count: "500 MB"
+ packages_count: "500 MB",
+ issues: proposal_issues
)
end
+ let(:enabled_repos) { [] }
+ let(:disabled_repos) { [] }
+ let(:proposal_issues) { [] }
+
let(:config_path) do
File.join(FIXTURES_PATH, "root_dir", "etc", "agama.yaml")
end
@@ -81,6 +93,74 @@
allow(Agama::Software::Proposal).to receive(:new).and_return(proposal)
end
+ shared_examples "software issues" do |tested_method|
+ before do
+ allow(subject).to receive(:product).and_return("Tumbleweed")
+ end
+
+ let(:proposal_issues) { [Agama::Issue.new("Proposal issue")] }
+
+ context "if there is no product selected yet" do
+ before do
+ allow(subject).to receive(:product).and_return(nil)
+ end
+
+ it "sets an issue" do
+ subject.public_send(tested_method)
+
+ expect(subject.issues).to contain_exactly(an_object_having_attributes(
+ description: /product not selected/i
+ ))
+ end
+ end
+
+ context "if there are disabled repositories" do
+ let(:disabled_repos) do
+ [
+ instance_double(Agama::Software::Repository, name: "Repo #1"),
+ instance_double(Agama::Software::Repository, name: "Repo #2")
+ ]
+ end
+
+ it "adds an issue for each disabled repository" do
+ subject.public_send(tested_method)
+
+ expect(subject.issues).to include(
+ an_object_having_attributes(
+ description: /could not read the repository Repo #1/i
+ ),
+ an_object_having_attributes(
+ description: /could not read the repository Repo #2/i
+ )
+ )
+ end
+ end
+
+ context "if there is any enabled repository" do
+ let(:enabled_repos) { [instance_double(Agama::Software::Repository, name: "Repo #1")] }
+
+ it "adds the proposal issues" do
+ subject.public_send(tested_method)
+
+ expect(subject.issues).to include(an_object_having_attributes(
+ description: /proposal issue/i
+ ))
+ end
+ end
+
+ context "if there is no enabled repository" do
+ let(:enabled_repos) { [] }
+
+ it "does not add the proposal issues" do
+ subject.public_send(tested_method)
+
+ expect(subject.issues).to_not include(an_object_having_attributes(
+ description: /proposal issue/i
+ ))
+ end
+ end
+ end
+
describe "#probe" do
let(:rootdir) { Dir.mktmpdir }
let(:repos_dir) { File.join(rootdir, "etc", "zypp", "repos.d") }
@@ -120,6 +200,8 @@
expect(repositories).to receive(:load)
subject.probe
end
+
+ include_examples "software issues", "probe"
end
describe "#products" do
@@ -164,6 +246,8 @@
.with("agama", :package, ["mandatory_pkg", "mandatory_pkg_s390"])
subject.propose
end
+
+ include_examples "software issues", "propose"
end
describe "#install" do
@@ -193,43 +277,6 @@
end
end
- describe "#validate" do
- before do
- allow(repositories).to receive(:enabled).and_return(enabled_repos)
- allow(repositories).to receive(:disabled).and_return(disabled_repos)
- allow(proposal).to receive(:errors).and_return([proposal_error])
- end
-
- let(:enabled_repos) { [] }
- let(:disabled_repos) { [] }
- let(:proposal_error) { Agama::ValidationError.new("proposal error") }
-
- context "when there are not enabled repositories" do
- it "does not return the proposal errors" do
- expect(subject.validate).to_not include(proposal_error)
- end
- end
-
- context "when there are disabled repositories" do
- let(:disabled_repos) do
- [instance_double(Agama::Software::Repository, name: "Repo #1")]
- end
-
- it "returns an error for each disabled repository" do
- expect(subject.validate.size).to eq(1)
- error = subject.validate.first
- expect(error.message).to match(/Could not read the repository/)
- end
- end
-
- context "when there are enabled repositories" do
- let(:enabled_repos) { [instance_double(Agama::Software::Repository)] }
- it "returns the proposal errors" do
- expect(subject.validate).to include(proposal_error)
- end
- end
- end
-
describe "#finish" do
let(:rootdir) { Dir.mktmpdir }
let(:repos_dir) { File.join(rootdir, "etc", "zypp", "repos.d") }
@@ -284,5 +331,6 @@
end
end
+ include_examples "issues"
include_examples "progress"
end
From f16b07eaa0782387ad310fe89dbf692d9f326923 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Mon, 9 Oct 2023 17:03:43 +0100
Subject: [PATCH 12/97] [service] Small fixes
---
service/lib/agama/software/manager.rb | 1 -
service/lib/agama/with_issues.rb | 9 ++++-----
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index 00d974fafc..e9255b735c 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -80,7 +80,6 @@ def initialize(config, logger)
# patterns selected by user
@user_patterns = []
@selected_patterns_change_callbacks = []
- update_issues
end
def select_product(name)
diff --git a/service/lib/agama/with_issues.rb b/service/lib/agama/with_issues.rb
index d682200760..dd45a4cf68 100644
--- a/service/lib/agama/with_issues.rb
+++ b/service/lib/agama/with_issues.rb
@@ -29,12 +29,11 @@ def issues
@issues || []
end
- def errors
- issues.select(&:error?)
- end
-
+ # Whether there are errors
+ #
+ # @return [Boolean]
def errors?
- errors.any?
+ issues.any?(&:error?)
end
# Sets the list of current issues
From 94dd8964f6f2d733de841e65e4b9cb324bf8b8cf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Mon, 9 Oct 2023 17:04:08 +0100
Subject: [PATCH 13/97] [service] Improve generation of connect result
---
service/lib/agama/dbus/software/manager.rb | 49 +++++++++++++++++-----
1 file changed, 39 insertions(+), 10 deletions(-)
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index a9fc91dd4c..8b6da5c76f 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -240,26 +240,55 @@ def compute_patterns
patterns
end
- # @return [Array]
+ # Result from calling to SUSE connect.
+ #
+ # @raise [Exception] if an unexpected error is found.
+ #
+ # @return [Array] List including a result code and a description
+ # (e.g., [1, "Connection to registration server failed (network error)"]).
+ #
+ # Possible result codes:
+ # 0: success
+ # 1: network error
+ # 2: timeout error
+ # 3: api error
+ # 4: missing credentials
+ # 5: incorrect credentials
+ # 6: invalid certificate
+ # 7: internal error (e.g., parsing json data)
def connect_result(&block)
block.call
[0, ""]
rescue SocketError => e
- logger.error("Network error: #{e}")
- [1, "Connection to registration server failed (network error)"]
+ connect_result_from_error(e, 1, "network error")
rescue Timeout::Error => e
- logger.error("Timeout error: #{e}")
- [2, "Connection to registration server failed (timeout)"]
+ connect_result_from_error(e, 2, "timeout")
rescue SUSE::Connect::ApiError => e
- [3, "Connection to registration server failed"]
+ connect_result_from_error(e, 3)
rescue SUSE::Connect::MissingSccCredentialsFile => e
- [4, "Connection to registration server failed (missing credentials)"]
+ connect_result_from_error(e, 4, "missing credentials")
rescue SUSE::Connect::MalformedSccCredentialsFile => e
- [5, "Connection to registration server failed (incorrect credentials)"]
+ connect_result_from_error(e, 5, "incorrect credentials")
rescue OpenSSL::SSL::SSLError => e
- [6, "Connection to registration server failed (invalid certificate)"]
+ connect_result_from_error(e, 6, "invalid certificate")
rescue JSON::ParserError => e
- [7, "Connection to registration server failed"]
+ connect_result_from_error(e, 7)
+ end
+
+ # Generates a result the from the given error.
+ #
+ # @param error [Exception]
+ # @param code [Integer]
+ # @param details [String, nil]
+ #
+ # @return [Array] List including a result code and a description.
+ def connect_result_from_error(error, code, details = nil)
+ logger.error("Error connecting to registration server: #{error}")
+
+ description = "Connection to registration server failed"
+ description += " (#{details})" if details
+
+ [code, description]
end
end
end
From b0734a576009cb706bacb728a760116e821186ea Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Mon, 9 Oct 2023 20:56:58 +0200
Subject: [PATCH 14/97] fix missing require
---
service/lib/agama/software/manager.rb | 1 +
1 file changed, 1 insertion(+)
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index e9255b735c..a2ed85ab02 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -27,6 +27,7 @@
require "agama/config"
require "agama/helpers"
require "agama/issue"
+require "agama/registration"
require "agama/software/callbacks"
require "agama/software/proposal"
require "agama/software/repositories_manager"
From 551a643e20d7f2f2d637db4ffa635751b3919878 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Mon, 9 Oct 2023 21:51:02 +0200
Subject: [PATCH 15/97] make rubocop happy
---
service/lib/agama/registration.rb | 19 +++++++++----------
1 file changed, 9 insertions(+), 10 deletions(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index 4d9783f996..da0f60611d 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Copyright (c) [2024] SUSE LLC
#
# All Rights Reserved.
@@ -36,7 +38,7 @@ def initialize(software_manager)
end
def register(code, email: "")
- target_distro = "ALP-Dolomite-1-x86_64" # TODO read it
+ target_distro = "ALP-Dolomite-1-x86_64" # TODO: read it
connect_params = {
token: code,
email: email
@@ -50,9 +52,9 @@ def register(code, email: "")
registration.register(email, code, target_distro)
# TODO: fill it properly for scc
target_product = OpenStruct.new(
- arch: "x86_64",
- identifier: "ALP-Dolomite",
- version: "1.0",
+ arch: "x86_64",
+ identifier: "ALP-Dolomite",
+ version: "1.0",
release_type: "ALPHA"
)
activate_params = {}
@@ -63,14 +65,11 @@ def register(code, email: "")
@email = email
end
- def deregister
- end
+ def deregister; end
- def disabled?
- end
+ def disabled?; end
- def optional?
- end
+ def optional?; end
# callback when state changed like when different product is selected
def on_state_change(&block)
From 409801123d3d565d36de78100de5a03abc30e577 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Mon, 9 Oct 2023 22:01:10 +0200
Subject: [PATCH 16/97] fix typo
---
service/lib/agama/registration.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index da0f60611d..d1dcd8e80b 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -44,7 +44,7 @@ def register(code, email: "")
email: email
}
- login, password = SUSE::Connect::YaST.announce_system(connect_params, distro_target)
+ 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)
From 834a7dc7e5fd6fba7f4836901b19c81bb317db6c Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Mon, 9 Oct 2023 22:12:48 +0200
Subject: [PATCH 17/97] drop removed method from y2-registration
---
service/lib/agama/registration.rb | 1 -
1 file changed, 1 deletion(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index d1dcd8e80b..54e1c8a6c1 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -49,7 +49,6 @@ def register(code, email: "")
# TODO: check if we can do it in memory for libzypp
SUSE::Connect::YaST.create_credentials_file(login, password)
- registration.register(email, code, target_distro)
# TODO: fill it properly for scc
target_product = OpenStruct.new(
arch: "x86_64",
From d875395a566bc5c80ef7fe8806c6530ee072837e Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Tue, 10 Oct 2023 10:57:03 +0200
Subject: [PATCH 18/97] try registation without release type
---
service/lib/agama/registration.rb | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index 54e1c8a6c1..fe209d715a 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -53,8 +53,7 @@ def register(code, email: "")
target_product = OpenStruct.new(
arch: "x86_64",
identifier: "ALP-Dolomite",
- version: "1.0",
- release_type: "ALPHA"
+ version: "1.0"
)
activate_params = {}
service = SUSE::Connect::YaST.activate_product(target_product, activate_params, email)
From 861c20290ad96adc46dcba9bba0a8779b709b7d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Tue, 10 Oct 2023 10:23:32 +0100
Subject: [PATCH 19/97] [service] Small improvements
---
service/lib/agama/dbus/software/manager.rb | 12 ++++++------
service/lib/agama/software/proposal.rb | 2 +-
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index 8b6da5c76f..2be008be69 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -244,7 +244,7 @@ def compute_patterns
#
# @raise [Exception] if an unexpected error is found.
#
- # @return [Array] List including a result code and a description
+ # @return [Array(Integer, String)] List including a result code and a description
# (e.g., [1, "Connection to registration server failed (network error)"]).
#
# Possible result codes:
@@ -275,20 +275,20 @@ def connect_result(&block)
connect_result_from_error(e, 7)
end
- # Generates a result the from the given error.
+ # Generates a result from a given error.
#
# @param error [Exception]
- # @param code [Integer]
+ # @param result_code [Integer]
# @param details [String, nil]
#
- # @return [Array] List including a result code and a description.
- def connect_result_from_error(error, code, details = nil)
+ # @return [Array(Integer, String)] List including a result code and a description.
+ def connect_result_from_error(error, result_code, details = nil)
logger.error("Error connecting to registration server: #{error}")
description = "Connection to registration server failed"
description += " (#{details})" if details
- [code, description]
+ [result_code, description]
end
end
end
diff --git a/service/lib/agama/software/proposal.rb b/service/lib/agama/software/proposal.rb
index 45470fc593..383e1509d5 100644
--- a/service/lib/agama/software/proposal.rb
+++ b/service/lib/agama/software/proposal.rb
@@ -111,7 +111,7 @@ def packages_size
#
# @return [Boolean]
def valid?
- proposal && !errors?
+ !(proposal.nil? || errors?)
end
private
From 719d4214e2308c555d4bb9e7dfd3fe14bf3d32ce Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Tue, 10 Oct 2023 10:23:49 +0100
Subject: [PATCH 20/97] [service] Fix tests
---
service/test/agama/software/proposal_test.rb | 22 +++++++++-----------
1 file changed, 10 insertions(+), 12 deletions(-)
diff --git a/service/test/agama/software/proposal_test.rb b/service/test/agama/software/proposal_test.rb
index 3c850b466f..8dfdf1cb94 100644
--- a/service/test/agama/software/proposal_test.rb
+++ b/service/test/agama/software/proposal_test.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Copyright (c) [2022] SUSE LLC
+# Copyright (c) [2022-2023] SUSE LLC
#
# All Rights Reserved.
#
@@ -77,9 +77,9 @@
end
context "when no errors were reported" do
- it "does not register any error" do
+ it "does not register any issue" do
subject.calculate
- expect(subject.errors).to be_empty
+ expect(subject.issues).to be_empty
end
end
@@ -88,10 +88,10 @@
{ "warning_level" => :blocker, "warning" => "Could not install..." }
end
- it "registers the corresponding validation error" do
+ it "registers the corresponding issue" do
subject.calculate
- expect(subject.errors).to eq(
- [Agama::ValidationError.new("Could not install...")]
+ expect(subject.issues).to contain_exactly(
+ an_object_having_attributes({ description: "Could not install..." })
)
end
end
@@ -100,13 +100,11 @@
let(:last_error) { "Solving errors..." }
let(:solve_errors) { 5 }
- it "registers them as validation errors" do
+ it "registers them as issues" do
subject.calculate
- expect(subject.errors).to eq(
- [
- Agama::ValidationError.new("Solving errors..."),
- Agama::ValidationError.new("Found 5 dependency issues.")
- ]
+ expect(subject.issues).to contain_exactly(
+ an_object_having_attributes(description: "Solving errors..."),
+ an_object_having_attributes(description: "Found 5 dependency issues.")
)
end
end
From 9881d893f5a772352a6469c710c67feaebf7eaa3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Tue, 10 Oct 2023 13:39:45 +0100
Subject: [PATCH 21/97] [service] Continue working on the D-Bus API
---
service/lib/agama/dbus/software/manager.rb | 117 +++++--
service/lib/agama/registration.rb | 37 ++-
.../test/agama/dbus/software/manager_test.rb | 292 ++++++++++++++++++
3 files changed, 404 insertions(+), 42 deletions(-)
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index 2be008be69..4219607ce1 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -28,6 +28,7 @@
require "agama/dbus/interfaces/progress"
require "agama/dbus/interfaces/service_status"
require "agama/dbus/with_service_status"
+require "agama/registration"
module Agama
module DBus
@@ -171,10 +172,10 @@ def finish
dbus_reader(:email, "s")
- dbus_reader(:state, "u")
+ dbus_reader(:requirement, "u")
dbus_method(:Register, "in reg_code:s, in options:a{sv}, out result:(us)") do |*args|
- [register(*args)]
+ [register(args[0], email: args[1]["Email"])]
end
dbus_method(:Deregister, "out result:(us)") { [deregister] }
@@ -188,20 +189,75 @@ def email
backend.registration.email || ""
end
- def state
- # TODO
- 0
+ # Registration requirement.
+ #
+ # @return [Integer] Possible values:
+ # 0: not required
+ # 1: optional
+ # 2: mandatory
+ def requirement
+ case backend.registration.requirement
+ when Agama::Registration::Requirement::MANDATORY
+ 2
+ when Agama::Registration::Requirement::OPTIONAL
+ 1
+ else
+ 0
+ end
end
- def register(reg_code, options)
- connect_result do
- backend.registration.register(reg_code, email: options["Email"])
+ # Tries to register with the given registration code.
+ #
+ # @param reg_code [String]
+ # @param email [String, nil]
+ #
+ # @return [Array(Integer, String)] Result code and a description.
+ # Possible result codes:
+ # 0: success
+ # 1: missing product
+ # 2: already registered
+ # 3: network error
+ # 4: timeout error
+ # 5: api error
+ # 6: missing credentials
+ # 7: incorrect credentials
+ # 8: invalid certificate
+ # 9: internal error (e.g., parsing json data)
+ def register(reg_code, email: nil)
+ if !backend.product
+ [1, "Product not selected yet"]
+ elsif backend.registration.reg_code
+ [2, "Product already registered"]
+ else
+ connect_result(first_error_code: 3) do
+ backend.registration.register(reg_code, email: email)
+ end
end
end
+ # Tries to deregister.
+ #
+ # @return [Array(Integer, String)] Result code and a description.
+ # Possible result codes:
+ # 0: success
+ # 1: missing product
+ # 2: not registered yet
+ # 3: network error
+ # 4: timeout error
+ # 5: api error
+ # 6: missing credentials
+ # 7: incorrect credentials
+ # 8: invalid certificate
+ # 9: internal error (e.g., parsing json data)
def deregister
- connect_result do
- backend.registration.deregister
+ if !backend.product
+ [1, "Product not selected yet"]
+ elsif !backend.registration.reg_code
+ [2, "Product not registered yet"]
+ else
+ connect_result(first_error_code: 3) do
+ backend.registration.deregister
+ end
end
end
@@ -226,6 +282,8 @@ def register_callbacks
self.selected_patterns = compute_patterns
end
+ backend.registration.on_change { registration_properties_changed }
+
backend.on_issues_change { issues_properties_changed }
end
@@ -240,55 +298,50 @@ def compute_patterns
patterns
end
+ def registration_properties_changed
+ dbus_properties_changed(REGISTRATION_INTERFACE,
+ interfaces_and_properties[REGISTRATION_INTERFACE], [])
+ end
+
# Result from calling to SUSE connect.
#
# @raise [Exception] if an unexpected error is found.
#
# @return [Array(Integer, String)] List including a result code and a description
# (e.g., [1, "Connection to registration server failed (network error)"]).
- #
- # Possible result codes:
- # 0: success
- # 1: network error
- # 2: timeout error
- # 3: api error
- # 4: missing credentials
- # 5: incorrect credentials
- # 6: invalid certificate
- # 7: internal error (e.g., parsing json data)
- def connect_result(&block)
+ def connect_result(first_error_code: 1, &block)
block.call
[0, ""]
rescue SocketError => e
- connect_result_from_error(e, 1, "network error")
+ connect_result_from_error(e, first_error_code, "network error")
rescue Timeout::Error => e
- connect_result_from_error(e, 2, "timeout")
+ connect_result_from_error(e, first_error_code + 1, "timeout")
rescue SUSE::Connect::ApiError => e
- connect_result_from_error(e, 3)
+ connect_result_from_error(e, first_error_code + 2)
rescue SUSE::Connect::MissingSccCredentialsFile => e
- connect_result_from_error(e, 4, "missing credentials")
+ connect_result_from_error(e, first_error_code + 3, "missing credentials")
rescue SUSE::Connect::MalformedSccCredentialsFile => e
- connect_result_from_error(e, 5, "incorrect credentials")
+ connect_result_from_error(e, first_error_code + 4, "incorrect credentials")
rescue OpenSSL::SSL::SSLError => e
- connect_result_from_error(e, 6, "invalid certificate")
+ connect_result_from_error(e, first_error_code + 5, "invalid certificate")
rescue JSON::ParserError => e
- connect_result_from_error(e, 7)
+ connect_result_from_error(e, first_error_code + 6)
end
# Generates a result from a given error.
#
# @param error [Exception]
- # @param result_code [Integer]
+ # @param error_code [Integer]
# @param details [String, nil]
#
- # @return [Array(Integer, String)] List including a result code and a description.
- def connect_result_from_error(error, result_code, details = nil)
+ # @return [Array(Integer, String)] List including an error code and a description.
+ def connect_result_from_error(error, error_code, details = nil)
logger.error("Error connecting to registration server: #{error}")
description = "Connection to registration server failed"
description += " (#{details})" if details
- [result_code, description]
+ [error_code, description]
end
end
end
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index fe209d715a..afe0356e02 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -31,10 +31,15 @@ class Registration
attr_reader :reg_code
attr_reader :email
+ module Requirement
+ NOT_REQUIRED = :not_required
+ OPTIONAL = :optional
+ MANDATORY = :mandatory
+ end
+
# initializes registration with instance of software manager for query about products
def initialize(software_manager)
@software = software_manager
- @on_state_change_callbacks = []
end
def register(code, email: "")
@@ -51,9 +56,9 @@ def register(code, email: "")
# TODO: fill it properly for scc
target_product = OpenStruct.new(
- arch: "x86_64",
- identifier: "ALP-Dolomite",
- version: "1.0"
+ arch: "x86_64",
+ identifier: "ALP-Dolomite",
+ version: "1.0"
)
activate_params = {}
service = SUSE::Connect::YaST.activate_product(target_product, activate_params, email)
@@ -61,17 +66,29 @@ def register(code, email: "")
@reg_code = code
@email = email
+ run_on_change_callbacks
end
- def deregister; end
-
- def disabled?; end
+ # TODO
+ def deregister
+ # run_on_change_callbacks
+ end
- def optional?; end
+ # TODO: check whether the selected product requires registration
+ def requirement
+ Requirement::NOT_REQUIRED
+ end
# callback when state changed like when different product is selected
- def on_state_change(&block)
- @on_state_change_callbacks << block
+ def on_change(&block)
+ @on_change_callbacks ||= []
+ @on_change_callbacks << block
+ end
+
+ private
+
+ def run_on_change_callbacks
+ @on_change_callbacks&.map(&:call)
end
end
end
diff --git a/service/test/agama/dbus/software/manager_test.rb b/service/test/agama/dbus/software/manager_test.rb
index edc0ae1946..9749fccec1 100644
--- a/service/test/agama/dbus/software/manager_test.rb
+++ b/service/test/agama/dbus/software/manager_test.rb
@@ -136,4 +136,296 @@
expect(installed).to eq(true)
end
end
+
+ describe "#reg_code" do
+ before do
+ allow(backend.registration).to receive(:reg_code).and_return(reg_code)
+ end
+
+ context "if there is no registered product yet" do
+ let(:reg_code) { nil }
+
+ it "returns an empty string" do
+ expect(subject.reg_code).to eq("")
+ end
+ end
+
+ context "if there is a registered product" do
+ let(:reg_code) { "123XX432" }
+
+ it "returns the registration code" do
+ expect(subject.reg_code).to eq("123XX432")
+ end
+ end
+ end
+
+ describe "#email" do
+ before do
+ allow(backend.registration).to receive(:email).and_return(email)
+ end
+
+ context "if there is no registered email" do
+ let(:email) { nil }
+
+ it "returns an empty string" do
+ expect(subject.email).to eq("")
+ end
+ end
+
+ context "if there is a registered email" do
+ let(:email) { "test@suse.com" }
+
+ it "returns the registered email" do
+ expect(subject.email).to eq("test@suse.com")
+ end
+ end
+ end
+
+ describe "#requirement" do
+ before do
+ allow(backend.registration).to receive(:requirement).and_return(requirement)
+ end
+
+ context "if the registration is not required" do
+ let(:requirement) { Agama::Registration::Requirement::NOT_REQUIRED }
+
+ it "returns 0" do
+ expect(subject.requirement).to eq(0)
+ end
+ end
+
+ context "if the registration is optional" do
+ let(:requirement) { Agama::Registration::Requirement::OPTIONAL }
+
+ it "returns 1" do
+ expect(subject.requirement).to eq(1)
+ end
+ end
+
+ context "if the registration is mandatory" do
+ let(:requirement) { Agama::Registration::Requirement::MANDATORY }
+
+ it "returns 2" do
+ expect(subject.requirement).to eq(2)
+ end
+ end
+ end
+
+ describe "#register" do
+ before do
+ allow(backend).to receive(:product).and_return("Tumbleweed")
+ allow(backend.registration).to receive(:reg_code).and_return(nil)
+ end
+
+ context "if there is no product selected yet" do
+ before do
+ allow(backend).to receive(:product).and_return(nil)
+ end
+
+ it "returns result code 1 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(1, /product not selected/i)
+ end
+ end
+
+ context "if the product is already registered" do
+ before do
+ allow(backend.registration).to receive(:reg_code).and_return("123XX432")
+ end
+
+ it "returns result code 2 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(2, /product already registered/i)
+ end
+ end
+
+ context "if the registration is correctly done" do
+ before do
+ allow(backend.registration).to receive(:register)
+ end
+
+ it "returns result code 0 without description" do
+ expect(subject.register("123XX432")).to contain_exactly(0, "")
+ end
+ end
+
+ context "if there is a network error" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(SocketError)
+ end
+
+ it "returns result code 3 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(3, /network error/)
+ end
+ end
+
+ context "if there is a timeout" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(Timeout::Error)
+ end
+
+ it "returns result code 4 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(4, /timeout/)
+ end
+ end
+
+ context "if there is an API error" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(SUSE::Connect::ApiError, "")
+ end
+
+ it "returns result code 5 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(5, /registration server failed/)
+ end
+ end
+
+ context "if there is a missing credials error" do
+ before do
+ allow(backend.registration)
+ .to receive(:register).and_raise(SUSE::Connect::MissingSccCredentialsFile)
+ end
+
+ it "returns result code 6 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(6, /missing credentials/)
+ end
+ end
+
+ context "if there is an incorrect credials error" do
+ before do
+ allow(backend.registration)
+ .to receive(:register).and_raise(SUSE::Connect::MalformedSccCredentialsFile)
+ end
+
+ it "returns result code 7 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(7, /incorrect credentials/)
+ end
+ end
+
+ context "if there is an invalid certificate error" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(OpenSSL::SSL::SSLError)
+ end
+
+ it "returns result code 8 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(8, /invalid certificate/)
+ end
+ end
+
+ context "if there is an internal error" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(JSON::ParserError)
+ end
+
+ it "returns result code 9 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(9, /registration server failed/)
+ end
+ end
+ end
+
+ describe "#deregister" do
+ before do
+ allow(backend).to receive(:product).and_return("Tumbleweed")
+ allow(backend.registration).to receive(:reg_code).and_return("123XX432")
+ end
+
+ context "if there is no product selected yet" do
+ before do
+ allow(backend).to receive(:product).and_return(nil)
+ end
+
+ it "returns result code 1 and description" do
+ expect(subject.deregister).to contain_exactly(1, /product not selected/i)
+ end
+ end
+
+ context "if the product is not registered yet" do
+ before do
+ allow(backend.registration).to receive(:reg_code).and_return(nil)
+ end
+
+ it "returns result code 2 and description" do
+ expect(subject.deregister).to contain_exactly(2, /product not registered/i)
+ end
+ end
+
+ context "if the deregistration is correctly done" do
+ before do
+ allow(backend.registration).to receive(:deregister)
+ end
+
+ it "returns result code 0 without description" do
+ expect(subject.deregister).to contain_exactly(0, "")
+ end
+ end
+
+ context "if there is a network error" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(SocketError)
+ end
+
+ it "returns result code 3 and description" do
+ expect(subject.deregister).to contain_exactly(3, /network error/)
+ end
+ end
+
+ context "if there is a timeout" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(Timeout::Error)
+ end
+
+ it "returns result code 4 and description" do
+ expect(subject.deregister).to contain_exactly(4, /timeout/)
+ end
+ end
+
+ context "if there is an API error" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(SUSE::Connect::ApiError, "")
+ end
+
+ it "returns result code 5 and description" do
+ expect(subject.deregister).to contain_exactly(5, /registration server failed/)
+ end
+ end
+
+ context "if there is a missing credials error" do
+ before do
+ allow(backend.registration)
+ .to receive(:deregister).and_raise(SUSE::Connect::MissingSccCredentialsFile)
+ end
+
+ it "returns result code 6 and description" do
+ expect(subject.deregister).to contain_exactly(6, /missing credentials/)
+ end
+ end
+
+ context "if there is an incorrect credials error" do
+ before do
+ allow(backend.registration)
+ .to receive(:deregister).and_raise(SUSE::Connect::MalformedSccCredentialsFile)
+ end
+
+ it "returns result code 7 and description" do
+ expect(subject.deregister).to contain_exactly(7, /incorrect credentials/)
+ end
+ end
+
+ context "if there is an invalid certificate error" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(OpenSSL::SSL::SSLError)
+ end
+
+ it "returns result code 8 and description" do
+ expect(subject.deregister).to contain_exactly(8, /invalid certificate/)
+ end
+ end
+
+ context "if there is an internal error" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(JSON::ParserError)
+ end
+
+ it "returns result code 9 and description" do
+ expect(subject.deregister).to contain_exactly(9, /registration server failed/)
+ end
+ end
+ end
end
From 4f90278fcfa79ad3631d583f104bb8d285c2ee4e Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Tue, 10 Oct 2023 15:28:38 +0200
Subject: [PATCH 22/97] support deregister
---
service/lib/agama/registration.rb | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index afe0356e02..e6274b9800 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -69,9 +69,22 @@ def register(code, email: "")
run_on_change_callbacks
end
- # TODO
def deregister
- # run_on_change_callbacks
+ connect_params = {
+ email: email
+ }
+ SUSE::Connect::YaST.deactivate_system(connect_params)
+
+ # TODO: fill it properly for scc
+ target_product = OpenStruct.new(
+ arch: "x86_64",
+ identifier: "ALP-Dolomite",
+ version: "1.0"
+ )
+ deactivate_params = {}
+ service = SUSE::Connect::YaST.activate_product(target_product, deactivate_params)
+ Y2Packager::NewRepositorySetup.instance.services.delete(service.name)
+ run_on_change_callbacks
end
# TODO: check whether the selected product requires registration
From fe89d84ce807f9dc4a0f0dcc74ef3ca4eec06e9e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Tue, 10 Oct 2023 14:53:03 +0100
Subject: [PATCH 23/97] [service] Add registration issue
---
service/lib/agama/software/manager.rb | 22 +++++++++--
service/test/agama/software/manager_test.rb | 42 +++++++++++++++++++--
2 files changed, 56 insertions(+), 8 deletions(-)
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index a2ed85ab02..21f51656e8 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -359,10 +359,7 @@ def current_issues
# packages. Those issues does not make any sense if there are no repositories to install
# from.
issues += proposal.issues if repositories.enabled.any?
-
- # TODO
- # issues += registration.issues
-
+ issues << missing_registration_issue if missing_registration?
issues
end
@@ -387,6 +384,23 @@ def repos_issues
severity: Issue::Severity::ERROR)
end
end
+
+ # Issue when a product requires registration but it is not registered yet.
+ #
+ # @return [Agama::Issue]
+ def missing_registration_issue
+ Issue.new("Product must be registered",
+ source: Issue::Source::SYSTEM,
+ severity: Issue::Severity::ERROR)
+ end
+
+ # Whether the registration is missing.
+ #
+ # @return [Boolean]
+ def missing_registration?
+ registration.reg_code.nil? &&
+ registration.requirement == Agama::Registration::Requirement::MANDATORY
+ end
end
end
end
diff --git a/service/test/agama/software/manager_test.rb b/service/test/agama/software/manager_test.rb
index befa2c1787..4095f91775 100644
--- a/service/test/agama/software/manager_test.rb
+++ b/service/test/agama/software/manager_test.rb
@@ -27,6 +27,7 @@
)
require "agama/config"
require "agama/issue"
+require "agama/registration"
require "agama/software/manager"
require "agama/software/proposal"
require "agama/dbus/clients/questions"
@@ -95,15 +96,16 @@
shared_examples "software issues" do |tested_method|
before do
- allow(subject).to receive(:product).and_return("Tumbleweed")
+ allow(subject).to receive(:product).and_return(product)
+ allow(subject.registration).to receive(:reg_code).and_return(reg_code)
end
+ let(:product) { "ALP-Dolomite" }
+ let(:reg_code) { "123XX432" }
let(:proposal_issues) { [Agama::Issue.new("Proposal issue")] }
context "if there is no product selected yet" do
- before do
- allow(subject).to receive(:product).and_return(nil)
- end
+ let(:product) { nil }
it "sets an issue" do
subject.public_send(tested_method)
@@ -159,6 +161,38 @@
))
end
end
+
+ context "if the product is not registered" do
+ let(:reg_code) { nil }
+
+ before do
+ allow(subject.registration).to receive(:requirement).and_return(reg_requirement)
+ end
+
+ context "and registration is mandatory" do
+ let(:reg_requirement) { Agama::Registration::Requirement::MANDATORY }
+
+ it "adds registration issue" do
+ subject.public_send(tested_method)
+
+ expect(subject.issues).to include(an_object_having_attributes(
+ description: /product must be registered/i
+ ))
+ end
+ end
+
+ context "and registration is not mandatory" do
+ let(:reg_requirement) { Agama::Registration::Requirement::OPTIONAL }
+
+ it "does not add registration issue" do
+ subject.public_send(tested_method)
+
+ expect(subject.issues).to_not include(an_object_having_attributes(
+ description: /product must be registered/i
+ ))
+ end
+ end
+ end
end
describe "#probe" do
From 7ccfa41acec7155f1bbefdf065c20c5da6440b80 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Tue, 10 Oct 2023 16:14:45 +0200
Subject: [PATCH 24/97] add to deregister also code
---
service/lib/agama/registration.rb | 2 ++
1 file changed, 2 insertions(+)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index e6274b9800..f9234aaf4f 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -50,6 +50,7 @@ def register(code, email: "")
}
login, password = SUSE::Connect::YaST.announce_system(connect_params, target_distro)
+ @system_code = code # remember code to be able to deregister
# write the global credentials
# TODO: check if we can do it in memory for libzypp
SUSE::Connect::YaST.create_credentials_file(login, password)
@@ -71,6 +72,7 @@ def register(code, email: "")
def deregister
connect_params = {
+ token: @system_code,
email: email
}
SUSE::Connect::YaST.deactivate_system(connect_params)
From 60ab7baea34ce1570f98b03c4b864e29e6c52924 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Tue, 10 Oct 2023 16:38:29 +0200
Subject: [PATCH 25/97] fix typo and order
---
service/lib/agama/registration.rb | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index f9234aaf4f..252c574b35 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -71,12 +71,6 @@ def register(code, email: "")
end
def deregister
- connect_params = {
- token: @system_code,
- email: email
- }
- SUSE::Connect::YaST.deactivate_system(connect_params)
-
# TODO: fill it properly for scc
target_product = OpenStruct.new(
arch: "x86_64",
@@ -84,8 +78,15 @@ def deregister
version: "1.0"
)
deactivate_params = {}
- service = SUSE::Connect::YaST.activate_product(target_product, deactivate_params)
+ service = SUSE::Connect::YaST.deactivate_product(target_product, deactivate_params)
Y2Packager::NewRepositorySetup.instance.services.delete(service.name)
+
+ connect_params = {
+ token: @system_code,
+ email: email
+ }
+ SUSE::Connect::YaST.deactivate_system(connect_params)
+
run_on_change_callbacks
end
From 18f5de9dadaae7295ee57b8561c5f949fca11923 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Tue, 10 Oct 2023 17:05:22 +0200
Subject: [PATCH 26/97] fix base product cannot be deactivated
---
service/lib/agama/registration.rb | 14 +++-----------
1 file changed, 3 insertions(+), 11 deletions(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index 252c574b35..2186bbfa91 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -62,8 +62,8 @@ def register(code, email: "")
version: "1.0"
)
activate_params = {}
- service = SUSE::Connect::YaST.activate_product(target_product, activate_params, email)
- Y2Packager::NewRepositorySetup.instance.add_service(service.name)
+ @service = SUSE::Connect::YaST.activate_product(target_product, activate_params, email)
+ Y2Packager::NewRepositorySetup.instance.add_service(@service.name)
@reg_code = code
@email = email
@@ -71,15 +71,7 @@ def register(code, email: "")
end
def deregister
- # TODO: fill it properly for scc
- target_product = OpenStruct.new(
- arch: "x86_64",
- identifier: "ALP-Dolomite",
- version: "1.0"
- )
- deactivate_params = {}
- service = SUSE::Connect::YaST.deactivate_product(target_product, deactivate_params)
- Y2Packager::NewRepositorySetup.instance.services.delete(service.name)
+ Y2Packager::NewRepositorySetup.instance.services.delete(@service.name)
connect_params = {
token: @system_code,
From a4cc72ba6536e7fa1b44aca780b1e252609360d0 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Tue, 10 Oct 2023 17:35:18 +0200
Subject: [PATCH 27/97] remove credentials file
---
service/lib/agama/registration.rb | 2 ++
1 file changed, 2 insertions(+)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index 2186bbfa91..da0c847fdd 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -19,6 +19,7 @@
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.
+require "fileutils"
require "yast"
require "ostruct"
require "suse/connect"
@@ -78,6 +79,7 @@ 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
run_on_change_callbacks
end
From 895a092e4085aa5b0fc936f98b75b830c0dfdf37 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Tue, 10 Oct 2023 17:52:05 +0200
Subject: [PATCH 28/97] properly uninitialize internal variables
---
service/lib/agama/registration.rb | 3 +++
1 file changed, 3 insertions(+)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index da0c847fdd..7ae513ee8f 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -81,6 +81,9 @@ def deregister
SUSE::Connect::YaST.deactivate_system(connect_params)
FileUtils.rm(SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE) # connect does not remove it itself
+ # reset varibles here
+ @reg_code = nil
+ @email = nil
run_on_change_callbacks
end
From cd6a22b520ee1abf6ffd3b7b4f67d2980e816637 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Wed, 11 Oct 2023 16:45:30 +0100
Subject: [PATCH 29/97] [service] Add Product and ProductBuilder
---
service/lib/agama/config.rb | 27 +++++--
service/lib/agama/software/product.rb | 58 ++++++++++++++
service/lib/agama/software/product_builder.rb | 78 +++++++++++++++++++
3 files changed, 158 insertions(+), 5 deletions(-)
create mode 100644 service/lib/agama/software/product.rb
create mode 100644 service/lib/agama/software/product_builder.rb
diff --git a/service/lib/agama/config.rb b/service/lib/agama/config.rb
index 0a038abadf..8f528d550a 100644
--- a/service/lib/agama/config.rb
+++ b/service/lib/agama/config.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Copyright (c) [2022] SUSE LLC
+# Copyright (c) [2022-2023] SUSE LLC
#
# All Rights Reserved.
#
@@ -92,10 +92,7 @@ def products
# cannot use `data` here to avoid endless loop as in data we use
# pick_product that select product from products
- @products = @pure_data["products"].select do |_key, value|
- value["archs"].nil? ||
- Yast2::ArchFilter.from_string(value["archs"]).match?
- end
+ @products = @pure_data["products"].select { |_k, v| arch_match?(v["archs"]) }
end
# Whether there are more than one product
@@ -120,6 +117,20 @@ def merge(config)
Config.new(simple_merge(data, config.data))
end
+ def arch_elements_from(*keys, property: nil)
+ keys.map!(&:to_s)
+ elements = pure_data.dig(*keys)
+ return [] unless elements
+
+ elements.map do |element|
+ if !element.is_a?(Hash)
+ element
+ elsif arch_match?(element["archs"])
+ property ? element[property.to_s] : element
+ end
+ end.compact
+ end
+
private
# Simple deep merge
@@ -138,5 +149,11 @@ def simple_merge(a_hash, another_hash)
end
end
end
+
+ def arch_match?(archs)
+ return true if archs.nil?
+
+ Yast2::ArchFilter.from_string(archs).match?
+ end
end
end
diff --git a/service/lib/agama/software/product.rb b/service/lib/agama/software/product.rb
new file mode 100644
index 0000000000..244304bbed
--- /dev/null
+++ b/service/lib/agama/software/product.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+# Copyright (c) [2023] SUSE LLC
+#
+# All Rights Reserved.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of version 2 of the GNU General Public License as published
+# by the Free Software Foundation.
+#
+# 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.
+
+require "agama/software/repositories_manager"
+
+module Agama
+ module Software
+ # Represents a product that Agama can install.
+ class Product
+ attr_reader :id
+
+ attr_accessor :display_name
+
+ attr_accessor :description
+
+ attr_accessor :name
+
+ attr_accessor :version
+
+ attr_accessor :repositories
+
+ attr_accessor :mandatory_packages
+
+ attr_accessor :optional_packages
+
+ attr_accessor :mandatory_patterns
+
+ attr_accessor :optional_patterns
+
+ def initialize(id)
+ @id = id
+ @repositories = []
+ @mandatory_packages = []
+ @optional_packages = []
+ @mandatory_patterns = []
+ @optional_patterns = []
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/software/product_builder.rb b/service/lib/agama/software/product_builder.rb
new file mode 100644
index 0000000000..13a709b735
--- /dev/null
+++ b/service/lib/agama/software/product_builder.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+# Copyright (c) [2023] SUSE LLC
+#
+# All Rights Reserved.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of version 2 of the GNU General Public License as published
+# by the Free Software Foundation.
+#
+# 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.
+
+require "agama/software/product"
+
+module Agama
+ module Software
+ # Builds products from the information of a config file.
+ class ProductBuilder
+ def initialize(config)
+ @config = config
+ end
+
+ def build
+ config.products.map do |id, attrs|
+ data = product_data_from_config(id)
+
+ Agama::Software::Product.new(id).tap do |product|
+ product.display_name = attrs["name"]
+ product.description = attrs["description"]
+ product.name = data[:name]
+ product.version = data[:version]
+ product.repositories = data[:repositories]
+ product.mandatory_packages = data[:mandatory_packages]
+ product.optional_packages = data[:optional_packages]
+ product.mandatory_patterns = data[:mandatory_patterns]
+ product.optional_patterns = data[:optional_patterns]
+ end
+ end
+ end
+
+ private
+
+ # @return [Agama::Config]
+ attr_reader :config
+
+ def product_data_from_config(id)
+ {
+ name: config.pure_data.dig(id, "software", "base_product"),
+ version: config.pure_data.dig(id, "software", "version"),
+ repositories: config.arch_elements_from(
+ id, "software", "installation_repositories", property: :url
+ ),
+ mandatory_packages: config.arch_elements_from(
+ id, "software", "mandatory_packages", property: :package
+ ),
+ optional_packages: config.arch_elements_from(
+ id, "software", "optional_packages", property: :package
+ ),
+ mandatory_patterns: config.arch_elements_from(
+ id, "software", "mandatory_patterns", property: :pattern
+ ),
+ optional_patterns: config.arch_elements_from(
+ id, "software", "optional_patterns", property: :pattern
+ )
+ }
+ end
+ end
+ end
+end
From b50ac90f9bdf0682c46d763a70dc059730516391 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Wed, 11 Oct 2023 16:46:32 +0100
Subject: [PATCH 30/97] [service] Adapt manager to use products
---
service/lib/agama/dbus/software/manager.rb | 8 +-
service/lib/agama/software/manager.rb | 100 +++++++++------------
2 files changed, 48 insertions(+), 60 deletions(-)
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index 4219607ce1..cea6f9e67e 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -72,7 +72,7 @@ def issues
dbus_reader :selected_product, "s"
dbus_method :SelectProduct, "in ProductID:s" do |product_id|
- old_product_id = backend.product
+ old_product_id = backend.product&.id
if old_product_id == product_id
logger.info "Do not changing the product as it is still the same (#{product_id})"
@@ -130,8 +130,8 @@ def issues
end
def available_products
- backend.products.map do |id, data|
- [id, data["name"], { "description" => data["description"] }].freeze
+ backend.products.map do |product|
+ [product.id, product.display_name, { "description" => product.description }]
end
end
@@ -139,7 +139,7 @@ def available_products
#
# @return [String] Product ID or an empty string if no product is selected
def selected_product
- backend.product || ""
+ backend.product&.id || ""
end
def select_product(product_id)
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index 21f51656e8..a424c12f49 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -21,7 +21,6 @@
require "fileutils"
require "yast"
-require "yast2/arch_filter"
require "y2packager/product"
require "y2packager/resolvable"
require "agama/config"
@@ -29,6 +28,8 @@
require "agama/issue"
require "agama/registration"
require "agama/software/callbacks"
+require "agama/software/product"
+require "agama/software/product_builder"
require "agama/software/proposal"
require "agama/software/repositories_manager"
require "agama/with_progress"
@@ -51,6 +52,9 @@ class Manager
GPG_KEYS_GLOB = "/usr/lib/rpm/gnupg/keys/gpg-*"
private_constant :GPG_KEYS_GLOB
+ # Selected product
+ #
+ # @return [Agama::Product, nil]
attr_reader :product
DEFAULT_LANGUAGES = ["en_US"].freeze
@@ -58,42 +62,44 @@ class Manager
attr_accessor :languages
- # FIXME: what about defining a Product class?
- # @return [Array>] An array containing the product ID and
- # additional information in a hash
+ # Available products for installation.
+ #
+ # @return [Array]
attr_reader :products
+ # @return [Agama::RepositoriesManager]
attr_reader :repositories
def initialize(config, logger)
@config = config
@logger = logger
@languages = DEFAULT_LANGUAGES
- @products = @config.products
- if @config.multi_product?
- @product = nil
- else
- @product = @products.keys.first # use the available product as default
- @config.pick_product(@product)
- end
+ @products = build_products
+ @product = @products.first if @products.size == 1
@repositories = RepositoriesManager.new
- on_progress_change { logger.info progress.to_s }
# patterns selected by user
@user_patterns = []
@selected_patterns_change_callbacks = []
+
+ on_progress_change { logger.info(progress.to_s) }
end
- def select_product(name)
- return if name == @product
- raise ArgumentError unless @products[name]
+ def select_product(id)
+ return if id == product&.id
+
+ new_product = @products.find { |p| p.id == id }
- @config.pick_product(name)
- @product = name
+ raise ArgumentError unless new_product
+
+ @product = new_product
repositories.delete_all
update_issues
end
def probe
+ # Should an error be raised?
+ return unless product
+
logger.info "Probing software"
# as we use liveDVD with normal like ENV, lets temporary switch to normal to use its repos
@@ -123,7 +129,10 @@ def initialize_target_repos
# Updates the software proposal
def propose
- proposal.base_product = selected_base_product
+ # Should an error be raised?
+ return unless product
+
+ proposal.base_product = product.name
proposal.languages = languages
select_resolvables
result = proposal.calculate
@@ -258,13 +267,20 @@ def registration
private
- def proposal
- @proposal ||= Proposal.new
- end
+ # @return [Agama::Config]
+ attr_reader :config
# @return [Logger]
attr_reader :logger
+ def build_products
+ ProductBuilder.new(config).build
+ end
+
+ def proposal
+ @proposal ||= Proposal.new
+ end
+
def import_gpg_keys
gpg_keys = Dir.glob(GPG_KEYS_GLOB).map(&:to_s)
logger.info "Importing GPG keys: #{gpg_keys}"
@@ -273,27 +289,8 @@ def import_gpg_keys
end
end
- def arch_select(section)
- collection = @config.data["software"][section] || []
- collection.select { |c| !c.is_a?(Hash) || arch_match?(c["archs"]) }
- end
-
- def arch_collection_for(section, key)
- arch_select(section).map { |r| r.is_a?(Hash) ? r[key] : r }
- end
-
- def selected_base_product
- @config.data["software"]["base_product"]
- end
-
- def arch_match?(archs)
- return true if archs.nil?
-
- Yast2::ArchFilter.from_string(archs).match?
- end
-
def add_base_repos
- arch_collection_for("installation_repositories", "url").map { |url| repositories.add(url) }
+ product.repositories.each { |url| repositories.add(url) }
end
REPOS_BACKUP = "/etc/zypp/repos.d.agama.backup"
@@ -321,21 +318,12 @@ def restore_original_repos
FileUtils.mv(REPOS_BACKUP, REPOS_DIR)
end
- # adds resolvables from yaml config for given product
+ # Adds resolvables for selected product
def select_resolvables
- mandatory_patterns = arch_collection_for("mandatory_patterns", "pattern")
- proposal.set_resolvables("agama", :pattern, mandatory_patterns)
-
- optional_patterns = arch_collection_for("optional_patterns", "pattern")
- proposal.set_resolvables("agama", :pattern, optional_patterns,
- optional: true)
-
- mandatory_packages = arch_collection_for("mandatory_packages", "package")
- proposal.set_resolvables("agama", :package, mandatory_packages)
-
- optional_packages = arch_collection_for("optional_packages", "package")
- proposal.set_resolvables("agama", :package, optional_packages,
- optional: true)
+ proposal.set_resolvables("agama", :pattern, product.mandatory_patterns)
+ proposal.set_resolvables("agama", :pattern, product.optional_patterns, optional: true)
+ proposal.set_resolvables("agama", :package, product.mandatory_packages)
+ proposal.set_resolvables("agama", :package, product.optional_packages, optional: true)
end
def selected_patterns_changed
@@ -378,7 +366,7 @@ def missing_product_issue
#
# @return [Array]
def repos_issues
- issues = repositories.disabled.map do |repo|
+ repositories.disabled.map do |repo|
Issue.new("Could not read the repository #{repo.name}",
source: Issue::Source::SYSTEM,
severity: Issue::Severity::ERROR)
From 66c509af197902033c40b086605b688519c6b109 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Wed, 11 Oct 2023 16:47:57 +0100
Subject: [PATCH 31/97] [service] Register selected product instead of
hardcoded
---
service/lib/agama/registration.rb | 34 +++++++++++++++++++++++--------
1 file changed, 25 insertions(+), 9 deletions(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index 7ae513ee8f..a9763f39b2 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Copyright (c) [2024] SUSE LLC
+# Copyright (c) [2023] SUSE LLC
#
# All Rights Reserved.
#
@@ -23,13 +23,15 @@
require "yast"
require "ostruct"
require "suse/connect"
-
require "y2packager/new_repository_setup"
+Yast.import "Arch"
+
module Agama
# Handles everything related to registration of system to SCC, RMT or similar
class Registration
attr_reader :reg_code
+
attr_reader :email
module Requirement
@@ -44,23 +46,23 @@ def initialize(software_manager)
end
def register(code, email: "")
- target_distro = "ALP-Dolomite-1-x86_64" # TODO: read it
+ return unless product
+
connect_params = {
token: code,
email: email
}
login, password = SUSE::Connect::YaST.announce_system(connect_params, target_distro)
- @system_code = code # remember code to be able to deregister
# write the global credentials
# TODO: check if we can do it in memory for libzypp
SUSE::Connect::YaST.create_credentials_file(login, password)
# TODO: fill it properly for scc
target_product = OpenStruct.new(
- arch: "x86_64",
- identifier: "ALP-Dolomite",
- version: "1.0"
+ arch: Yast::Arch.rpm_arch,
+ identifier: product.id,
+ version: product.version
)
activate_params = {}
@service = SUSE::Connect::YaST.activate_product(target_product, activate_params, email)
@@ -75,7 +77,7 @@ def deregister
Y2Packager::NewRepositorySetup.instance.services.delete(@service.name)
connect_params = {
- token: @system_code,
+ token: reg_code,
email: email
}
SUSE::Connect::YaST.deactivate_system(connect_params)
@@ -87,8 +89,10 @@ def deregister
run_on_change_callbacks
end
- # TODO: check whether the selected product requires registration
def requirement
+ return Requirement::NOT_REQUIRED unless product
+ return Requirement::MANDATORY if product.repositories.none?
+
Requirement::NOT_REQUIRED
end
@@ -100,6 +104,18 @@ def on_change(&block)
private
+ attr_reader :software
+
+ def product
+ software.product
+ end
+
+ # E.g., "ALP-Dolomite-1-x86_64"
+ def target_distro
+ v = version.to_s.split(".").first || "1"
+ "#{product.id}-#{v}-#{Yast::Arch.rpm_arch}"
+ end
+
def run_on_change_callbacks
@on_change_callbacks&.map(&:call)
end
From b36e988f316889b3534a83cca4ddb9e803ac32da Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Wed, 11 Oct 2023 16:48:27 +0100
Subject: [PATCH 32/97] [service] Add version to config file
---
service/etc/agama.yaml | 1 +
1 file changed, 1 insertion(+)
diff --git a/service/etc/agama.yaml b/service/etc/agama.yaml
index 8192ebf72a..524d80676b 100644
--- a/service/etc/agama.yaml
+++ b/service/etc/agama.yaml
@@ -46,6 +46,7 @@ ALP-Dolomite:
archs: ppc64
optional_packages: null
base_product: ALP-Dolomite
+ version: "1.0"
security:
tpm_luks_open: true
From 3632f6a46c68585691cbe9f7e64b5c7130baf4db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Wed, 11 Oct 2023 17:06:36 +0100
Subject: [PATCH 33/97] [web] Fix pattern selector
---
web/src/components/software/PatternSelector.jsx | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/web/src/components/software/PatternSelector.jsx b/web/src/components/software/PatternSelector.jsx
index 905bb40cf9..524baa8fab 100644
--- a/web/src/components/software/PatternSelector.jsx
+++ b/web/src/components/software/PatternSelector.jsx
@@ -153,7 +153,7 @@ function PatternSelector() {
const refresh = async () => {
setSelected(await client.software.selectedPatterns());
setUsed(await client.software.getUsedSpace());
- setErrors(await client.software.getValidationErrors());
+ setErrors(await client.software.getIssues());
};
refresh();
@@ -166,7 +166,7 @@ function PatternSelector() {
const loadData = async () => {
setSelected(await client.software.selectedPatterns());
setUsed(await client.software.getUsedSpace());
- setErrors(await client.software.getValidationErrors());
+ setErrors(await client.software.getIssues());
setPatterns(await client.software.patterns(true));
};
@@ -205,10 +205,14 @@ function PatternSelector() {
// if there is just a single error then the error is displayed directly instead of this summary
const errorLabel = sprintf(_("%d errors"), errors.length);
+ // FIXME: ValidationErrors should be replaced by an equivalent component to show issues.
+ // Note that only the Users client uses the old Validation D-Bus interface.
+ const validationErrors = errors.map(e => ({ message: e.description }));
+
return (
<>
-
+
Date: Thu, 12 Oct 2023 17:39:09 +0200
Subject: [PATCH 34/97] add pkg related stuff after registration
---
service/lib/agama/registration.rb | 52 ++++++++++++++++++++++++++-
service/lib/agama/software/manager.rb | 2 +-
2 files changed, 52 insertions(+), 2 deletions(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index a9763f39b2..0ef795a611 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -41,8 +41,9 @@ module Requirement
end
# initializes registration with instance of software manager for query about products
- def initialize(software_manager)
+ def initialize(software_manager, logger)
@software = software_manager
+ @logger = logger
end
def register(code, email: "")
@@ -67,6 +68,7 @@ def register(code, email: "")
activate_params = {}
@service = SUSE::Connect::YaST.activate_product(target_product, activate_params, email)
Y2Packager::NewRepositorySetup.instance.add_service(@service.name)
+ add_service(@service)
@reg_code = code
@email = email
@@ -75,6 +77,7 @@ def register(code, email: "")
def deregister
Y2Packager::NewRepositorySetup.instance.services.delete(@service.name)
+ remove_service(@service)
connect_params = {
token: reg_code,
@@ -119,5 +122,52 @@ def target_distro
def run_on_change_callbacks
@on_change_callbacks&.map(&:call)
end
+
+ # code is based on https://github.com/yast/yast-registration/blob/master/src/lib/registration/sw_mgmt.rb#L365
+ # TODO: move it to software manager
+ # rubocop:disable Metrics/AbcSize
+ def add_service(service)
+ # save repositories before refreshing added services (otherwise
+ # pkg-bindings will treat them as removed by the service refresh and
+ # unload them)
+ if !Yast::Pkg.SourceSaveAll
+ # error message
+ @logger.error("Saving repository configuration failed.")
+ end
+
+ @logger.info "Adding service #{service.name.inspect} (#{service.url})"
+ if !Yast::Pkg.ServiceAdd(service.name, service.url.to_s)
+ raise format("Adding service '%s' failed.", service.name)
+ end
+
+ if !Yast::Pkg.ServiceSet(service.name, "autorefresh" => true)
+ # error message
+ raise format("Updating service '%s' failed.", service.name)
+ end
+
+ # refresh works only for saved services
+ if !Yast::Pkg.ServiceSave(service_name)
+ # error message
+ raise format("Saving service '%s' failed.", service_name)
+ end
+
+ # Force refreshing due timing issues (bnc#967828)
+ if !Yast::Pkg.ServiceForceRefresh(service_name)
+ # error message
+ raise format("Refreshing service '%s' failed.", service_name)
+ end
+ ensure
+ Pkg.SourceSaveAll
+ end
+ # rubocop:enable Metrics/AbcSize
+
+ # TODO: move it to software manager
+ def remove_service(service)
+ if Yast::Pkg.ServiceDelete(service.name) && !Pkg.SourceSaveAll
+ raise format("Removing service '%s' failed.", service_name)
+ end
+
+ true
+ end
end
end
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index 32b76ef87f..b3530fa01d 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -263,7 +263,7 @@ def used_disk_space
end
def registration
- @registration ||= Registration.new(self)
+ @registration ||= Registration.new(self, @logger)
end
private
From 61147b78a0065d5f65b9d33fc1d32b33e8976f79 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Thu, 12 Oct 2023 21:21:33 +0200
Subject: [PATCH 35/97] fix crash for unknown version
---
service/lib/agama/registration.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index 0ef795a611..fb8d4c803f 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -115,7 +115,7 @@ def product
# E.g., "ALP-Dolomite-1-x86_64"
def target_distro
- v = version.to_s.split(".").first || "1"
+ v = product.version.to_s.split(".").first || "1"
"#{product.id}-#{v}-#{Yast::Arch.rpm_arch}"
end
From 72cbf5369da79b14ca349db44fdbcce353288dc1 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Thu, 12 Oct 2023 21:43:32 +0200
Subject: [PATCH 36/97] fix crash for wrong namespace
---
service/lib/agama/registration.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index fb8d4c803f..b0930618d9 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -157,7 +157,7 @@ def add_service(service)
raise format("Refreshing service '%s' failed.", service_name)
end
ensure
- Pkg.SourceSaveAll
+ Yast::Pkg.SourceSaveAll
end
# rubocop:enable Metrics/AbcSize
From 92754795028848c26680739ca4c9773ede895859 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Thu, 12 Oct 2023 22:28:00 +0200
Subject: [PATCH 37/97] fix typos
---
service/lib/agama/registration.rb | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index b0930618d9..f2af710ed2 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -146,15 +146,15 @@ def add_service(service)
end
# refresh works only for saved services
- if !Yast::Pkg.ServiceSave(service_name)
+ if !Yast::Pkg.ServiceSave(service.name)
# error message
- raise format("Saving service '%s' failed.", service_name)
+ raise format("Saving service '%s' failed.", service.name)
end
# Force refreshing due timing issues (bnc#967828)
- if !Yast::Pkg.ServiceForceRefresh(service_name)
+ if !Yast::Pkg.ServiceForceRefresh(service.name)
# error message
- raise format("Refreshing service '%s' failed.", service_name)
+ raise format("Refreshing service '%s' failed.", service.name)
end
ensure
Yast::Pkg.SourceSaveAll
From bede9c318aa5551b4750b511c72aedf4e490aa9a Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Fri, 13 Oct 2023 09:43:24 +0200
Subject: [PATCH 38/97] add service specific credentials file
---
service/lib/agama/registration.rb | 23 ++++++++++++++++++++++-
1 file changed, 22 insertions(+), 1 deletion(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index f2af710ed2..7c0fe03e8e 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -67,6 +67,11 @@ def register(code, email: "")
)
activate_params = {}
@service = SUSE::Connect::YaST.activate_product(target_product, activate_params, 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)
+ end
Y2Packager::NewRepositorySetup.instance.add_service(@service.name)
add_service(@service)
@@ -85,8 +90,13 @@ def deregister
}
SUSE::Connect::YaST.deactivate_system(connect_params)
FileUtils.rm(SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE) # connect does not remove it itself
+ if @credentials_file
+ path = File.join(SUSE::Connect::YaST::DEFAULT_CREDENTIALS_DIR, @credentials_file)
+ FileUtils.rm(path)
+ @credentials_file = nil
+ end
- # reset varibles here
+ # reset variables here
@reg_code = nil
@email = nil
run_on_change_callbacks
@@ -169,5 +179,16 @@ def remove_service(service)
true
end
+
+ # taken from https://github.com/yast/yast-registration/blob/master/src/lib/registration/url_helpers.rb#L109
+ def credentials_from_url(url)
+ parsed_url = URI(url)
+ params = Hash[URI.decode_www_form(parsed_url.query)]
+
+ params["credentials"]
+ rescue
+ # if something goes wrong try to continue like if there is no credentials param
+ nil
+ end
end
end
From a7b856d8308caaef708e223fb9395a95939bdf1c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Mon, 16 Oct 2023 11:37:02 +0100
Subject: [PATCH 39/97] [service] Improve documentation
---
service/lib/agama/config.rb | 30 ++++++++++++++++++
service/lib/agama/registration.rb | 24 +++++++++++---
service/lib/agama/software/manager.rb | 12 ++++++-
service/lib/agama/software/product.rb | 31 +++++++++++++++++++
service/lib/agama/software/product_builder.rb | 8 +++++
5 files changed, 100 insertions(+), 5 deletions(-)
diff --git a/service/lib/agama/config.rb b/service/lib/agama/config.rb
index 8f528d550a..8f447f2b5a 100644
--- a/service/lib/agama/config.rb
+++ b/service/lib/agama/config.rb
@@ -117,6 +117,32 @@ def merge(config)
Config.new(simple_merge(data, config.data))
end
+ # Elements that match the current arch.
+ #
+ # @example
+ # config.pure_data = {
+ # ALP-Dolomite: {
+ # software: {
+ # installation_repositories: {
+ # - url: https://updates.suse.com/SUSE/Products/ALP-Dolomite/1.0/x86_64/product/
+ # archs: x86_64
+ # - url: https://updates.suse.com/SUSE/Products/ALP-Dolomite/1.0/aarch64/product/
+ # archs: aarch64
+ # - https://updates.suse.com/SUSE/Products/ALP-Dolomite/1.0/noarch/
+ # }
+ # }
+ # }
+ # }
+ #
+ # Yast::Arch.rpm_arch #=> "x86_64"
+ # config.arch_elements_from("ALP-Dolomite", "software", "installation_repositories",
+ # property: :url) #=> ["https://.../SUSE/Products/ALP-Dolomite/1.0/x86_64/product/",
+ # #=> "https://updates.suse.com/SUSE/Products/ALP-Dolomite/1.0/noarch/"]
+ #
+ # @param keys [Array] Config data keys of the collection.
+ # @param property [Symbol|String|nil] Property to retrieve of the elements.
+ #
+ # @return [Array]
def arch_elements_from(*keys, property: nil)
keys.map!(&:to_s)
elements = pure_data.dig(*keys)
@@ -150,6 +176,10 @@ def simple_merge(a_hash, another_hash)
end
end
+ # Whether the current arch matches any of the given archs.
+ #
+ # @param archs [Array]
+ # @return [Boolean]
def arch_match?(archs)
return true if archs.nil?
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index 7c0fe03e8e..2dbfcbac1a 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -28,10 +28,16 @@
Yast.import "Arch"
module Agama
- # Handles everything related to registration of system to SCC, RMT or similar
+ # Handles everything related to registration of system to SCC, RMT or similar.
class Registration
+ # Code used for registering the product.
+ #
+ # @return [String, nil] nil if the product is not registered yet.
attr_reader :reg_code
+ # Email used for registering the product.
+ #
+ # @return [String, nil]
attr_reader :email
module Requirement
@@ -40,7 +46,8 @@ module Requirement
MANDATORY = :mandatory
end
- # initializes registration with instance of software manager for query about products
+ # @param software_manager [Agama::Software::Manager]
+ # @param logger [Logger]
def initialize(software_manager, logger)
@software = software_manager
@logger = logger
@@ -102,6 +109,9 @@ def deregister
run_on_change_callbacks
end
+ # Indicates whether the registration is optional, mandatory or not required.
+ #
+ # @return [Symbol] See {Requirement}.
def requirement
return Requirement::NOT_REQUIRED unless product
return Requirement::MANDATORY if product.repositories.none?
@@ -109,7 +119,7 @@ def requirement
Requirement::NOT_REQUIRED
end
- # callback when state changed like when different product is selected
+ # Callbacks to be called when registration changes (e.g., a different product is selected).
def on_change(&block)
@on_change_callbacks ||= []
@on_change_callbacks << block
@@ -117,13 +127,19 @@ def on_change(&block)
private
+ # @return [Agama::Software::Manager]
attr_reader :software
+ # Currently selected product.
+ #
+ # @return [Agama::Software::Product, nil]
def product
software.product
end
- # E.g., "ALP-Dolomite-1-x86_64"
+ # Product name expected by SCC.
+ #
+ # @return [String] E.g., "ALP-Dolomite-1-x86_64".
def target_distro
v = product.version.to_s.split(".").first || "1"
"#{product.id}-#{v}-#{Yast::Arch.rpm_arch}"
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index b3530fa01d..2c33d22a34 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -52,7 +52,7 @@ class Manager
GPG_KEYS_GLOB = "/usr/lib/rpm/gnupg/keys/gpg-*"
private_constant :GPG_KEYS_GLOB
- # Selected product
+ # Selected product.
#
# @return [Agama::Product, nil]
attr_reader :product
@@ -70,6 +70,8 @@ class Manager
# @return [Agama::RepositoriesManager]
attr_reader :repositories
+ # @param config [Agama::Config]
+ # @param logger [Logger]
def initialize(config, logger)
@config = config
@logger = logger
@@ -84,6 +86,11 @@ def initialize(config, logger)
on_progress_change { logger.info(progress.to_s) }
end
+ # Selects a product with the given id.
+ #
+ # @raise {ArgumentError} If id is unknown.
+ #
+ # @param id [String]
def select_product(id)
return if id == product&.id
@@ -274,6 +281,9 @@ def registration
# @return [Logger]
attr_reader :logger
+ # Generates a list of products according to the information of the config file.
+ #
+ # @return [Array]
def build_products
ProductBuilder.new(config).build
end
diff --git a/service/lib/agama/software/product.rb b/service/lib/agama/software/product.rb
index 244304bbed..bcbfba8003 100644
--- a/service/lib/agama/software/product.rb
+++ b/service/lib/agama/software/product.rb
@@ -25,26 +25,57 @@ module Agama
module Software
# Represents a product that Agama can install.
class Product
+ # Product id.
+ #
+ # @return [String]
attr_reader :id
+ # Name of the product to be display.
+ #
+ # @return [String]
attr_accessor :display_name
+ # Description of the product.
+ #
+ # @return [String]
attr_accessor :description
+ # Internal name of the product. This is relevant for registering the product.
+ #
+ # @return [String]
attr_accessor :name
+ # Version of the product. This is relevant for registering the product.
+ #
+ # @return [String] E.g., "1.0".
attr_accessor :version
+ # List of repositories.
+ #
+ # @return [Array] Empty if the product requires registration.
attr_accessor :repositories
+ # Mandatory packages.
+ #
+ # @return [Array]
attr_accessor :mandatory_packages
+ # Optional packages.
+ #
+ # @return [Array]
attr_accessor :optional_packages
+ # Mandatory patterns.
+ #
+ # @return [Array]
attr_accessor :mandatory_patterns
+ # Optional patterns.
+ #
+ # @return [Array]
attr_accessor :optional_patterns
+ # @param id [string] Product id.
def initialize(id)
@id = id
@repositories = []
diff --git a/service/lib/agama/software/product_builder.rb b/service/lib/agama/software/product_builder.rb
index 13a709b735..7982c37ade 100644
--- a/service/lib/agama/software/product_builder.rb
+++ b/service/lib/agama/software/product_builder.rb
@@ -25,10 +25,14 @@ module Agama
module Software
# Builds products from the information of a config file.
class ProductBuilder
+ # @param config [Agama::Config]
def initialize(config)
@config = config
end
+ # Builds the products.
+ #
+ # @return [Array]
def build
config.products.map do |id, attrs|
data = product_data_from_config(id)
@@ -52,6 +56,10 @@ def build
# @return [Agama::Config]
attr_reader :config
+ # Data from config, filtering by arch.
+ #
+ # @param id [String]
+ # @return [Hash]
def product_data_from_config(id)
{
name: config.pure_data.dig(id, "software", "base_product"),
From db7567ac822f0e41b860a2104eec40a212fcae56 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Mon, 16 Oct 2023 11:41:33 +0100
Subject: [PATCH 40/97] [service] Rubocop
---
service/lib/agama/registration.rb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index 2dbfcbac1a..911e5d6d9c 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -199,10 +199,10 @@ def remove_service(service)
# taken from https://github.com/yast/yast-registration/blob/master/src/lib/registration/url_helpers.rb#L109
def credentials_from_url(url)
parsed_url = URI(url)
- params = Hash[URI.decode_www_form(parsed_url.query)]
+ params = URI.decode_www_form(parsed_url.query).to_h
params["credentials"]
- rescue
+ rescue StandardError
# if something goes wrong try to continue like if there is no credentials param
nil
end
From ef1a0f296a869c159963e7c8dd1f1d238556496f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Tue, 17 Oct 2023 11:41:06 +0100
Subject: [PATCH 41/97] [service] Minor fixes and tests
---
service/lib/agama/config.rb | 26 +-
service/lib/agama/software/manager.rb | 2 +
service/lib/agama/software/product.rb | 2 -
service/test/agama/config_test.rb | 128 +++++++-
.../test/agama/dbus/software/manager_test.rb | 270 +++++++++--------
service/test/agama/software/manager_test.rb | 102 ++++---
.../agama/software/product_builder_test.rb | 286 ++++++++++++++++++
service/test/agama/with_issues_examples.rb | 2 -
8 files changed, 634 insertions(+), 184 deletions(-)
create mode 100644 service/test/agama/software/product_builder_test.rb
diff --git a/service/lib/agama/config.rb b/service/lib/agama/config.rb
index 8f447f2b5a..bbd9558ba7 100644
--- a/service/lib/agama/config.rb
+++ b/service/lib/agama/config.rb
@@ -121,15 +121,19 @@ def merge(config)
#
# @example
# config.pure_data = {
- # ALP-Dolomite: {
- # software: {
- # installation_repositories: {
- # - url: https://updates.suse.com/SUSE/Products/ALP-Dolomite/1.0/x86_64/product/
- # archs: x86_64
- # - url: https://updates.suse.com/SUSE/Products/ALP-Dolomite/1.0/aarch64/product/
- # archs: aarch64
- # - https://updates.suse.com/SUSE/Products/ALP-Dolomite/1.0/noarch/
- # }
+ # "ALP-Dolomite" => {
+ # "software" => {
+ # "installation_repositories" => [
+ # {
+ # "url" => "https://updates.suse.com/SUSE/Products/ALP-Dolomite/1.0/x86_64/product/",
+ # "archs" => "x86_64"
+ # },
+ # {
+ # "url" => https://updates.suse.com/SUSE/Products/ALP-Dolomite/1.0/aarch64/product/",
+ # "archs" => "aarch64"
+ # },
+ # "https://updates.suse.com/SUSE/Products/ALP-Dolomite/1.0/noarch/"
+ # ]
# }
# }
# }
@@ -146,7 +150,7 @@ def merge(config)
def arch_elements_from(*keys, property: nil)
keys.map!(&:to_s)
elements = pure_data.dig(*keys)
- return [] unless elements
+ return [] unless elements.is_a?(Array)
elements.map do |element|
if !element.is_a?(Hash)
@@ -178,7 +182,7 @@ def simple_merge(a_hash, another_hash)
# Whether the current arch matches any of the given archs.
#
- # @param archs [Array]
+ # @param archs [String] E.g., "x86_64,aarch64"
# @return [Boolean]
def arch_match?(archs)
return true if archs.nil?
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index 2c33d22a34..dd88d5881b 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -83,6 +83,8 @@ def initialize(config, logger)
@user_patterns = []
@selected_patterns_change_callbacks = []
+ update_issues
+
on_progress_change { logger.info(progress.to_s) }
end
diff --git a/service/lib/agama/software/product.rb b/service/lib/agama/software/product.rb
index bcbfba8003..71f1ade8ac 100644
--- a/service/lib/agama/software/product.rb
+++ b/service/lib/agama/software/product.rb
@@ -19,8 +19,6 @@
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.
-require "agama/software/repositories_manager"
-
module Agama
module Software
# Represents a product that Agama can install.
diff --git a/service/test/agama/config_test.rb b/service/test/agama/config_test.rb
index c61714ee9c..bdb7eea27f 100644
--- a/service/test/agama/config_test.rb
+++ b/service/test/agama/config_test.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Copyright (c) [2022] SUSE LLC
+# Copyright (c) [2022-2023] SUSE LLC
#
# All Rights Reserved.
#
@@ -20,8 +20,11 @@
# find current contact information at www.suse.com.
require_relative "../test_helper"
+require "yast"
require "agama/config"
+Yast.import "Arch"
+
describe Agama::Config do
let(:config) { described_class.new("web" => { "ssl" => "SOMETHING" }) }
@@ -119,4 +122,127 @@
end
end
end
+
+ describe "#arch_elements_from" do
+ subject { described_class.new(data) }
+
+ context "when the given set of keys does not match any data" do
+ let(:data) do
+ {
+ "Product1" => {
+ "name" => "Test product 1"
+ }
+ }
+ end
+
+ it "returns an empty array" do
+ expect(subject.arch_elements_from("Product1", "some", "collection")).to be_empty
+ end
+ end
+
+ context "when the given set of keys does not contain a collection" do
+ let(:data) do
+ {
+ "Product1" => {
+ "name" => "Test product 1"
+ }
+ }
+ end
+
+ it "returns an empty array" do
+ expect(subject.arch_elements_from("Product1", "name")).to be_empty
+ end
+ end
+
+ context "when the given set of keys contains a collection" do
+ let(:data) do
+ {
+ "Product1" => {
+ "some" => {
+ "collection" => [
+ "element1",
+ {
+ "element" => "element2"
+ },
+ {
+ "element" => "element3",
+ "archs" => "x86_64"
+ },
+ {
+ "element" => "element4",
+ "archs" => "x86_64,aarch64"
+ },
+ {
+ "element" => "element5",
+ "archs" => "ppc64"
+ }
+ ]
+ }
+ }
+ }
+ end
+
+ before do
+ allow(Yast::Arch).to receive("x86_64").and_return(true)
+ allow(Yast::Arch).to receive("aarch64").and_return(false)
+ allow(Yast::Arch).to receive("ppc64").and_return(false)
+ end
+
+ it "returns all the elements that match the current arch" do
+ elements = subject.arch_elements_from("Product1", "some", "collection")
+
+ expect(elements).to contain_exactly(
+ "element1",
+ { "element" => "element2" },
+ { "element" => "element3", "archs" => "x86_64" },
+ { "element" => "element4", "archs" => "x86_64,aarch64" }
+ )
+ end
+
+ context "and there are no elements matching the current arch" do
+ let(:data) do
+ {
+ "Product1" => {
+ "some" => {
+ "collection" => [
+ {
+ "element" => "element1",
+ "archs" => "aarch64"
+ },
+ {
+ "element" => "element2",
+ "archs" => "ppc64"
+ }
+ ]
+ }
+ }
+ }
+ end
+
+ it "returns an empty list" do
+ elements = subject.arch_elements_from("Product1", "some", "collection")
+
+ expect(elements).to be_empty
+ end
+ end
+
+ context "and some property is requested" do
+ it "returns the property from all elements that match the current arch" do
+ elements = subject.arch_elements_from(
+ "Product1", "some", "collection", property: :element
+ )
+
+ expect(elements).to contain_exactly("element1", "element2", "element3", "element4")
+ end
+ end
+
+ context "and the requested property does not exit" do
+ it "only return elements that are direct values" do
+ elements = subject.arch_elements_from("Product1", "some", "collection", property: :foo)
+
+ expect(elements).to contain_exactly("element1")
+ end
+ end
+ end
+ end
end
diff --git a/service/test/agama/dbus/software/manager_test.rb b/service/test/agama/dbus/software/manager_test.rb
index 9749fccec1..17a5504e4d 100644
--- a/service/test/agama/dbus/software/manager_test.rb
+++ b/service/test/agama/dbus/software/manager_test.rb
@@ -213,218 +213,220 @@
describe "#register" do
before do
- allow(backend).to receive(:product).and_return("Tumbleweed")
allow(backend.registration).to receive(:reg_code).and_return(nil)
end
context "if there is no product selected yet" do
- before do
- allow(backend).to receive(:product).and_return(nil)
- end
-
it "returns result code 1 and description" do
expect(subject.register("123XX432")).to contain_exactly(1, /product not selected/i)
end
end
- context "if the product is already registered" do
+ context "if there is a selected product" do
before do
- allow(backend.registration).to receive(:reg_code).and_return("123XX432")
+ backend.select_product("Tumbleweed")
end
- it "returns result code 2 and description" do
- expect(subject.register("123XX432")).to contain_exactly(2, /product already registered/i)
- end
- end
+ context "if the product is already registered" do
+ before do
+ allow(backend.registration).to receive(:reg_code).and_return("123XX432")
+ end
- context "if the registration is correctly done" do
- before do
- allow(backend.registration).to receive(:register)
+ it "returns result code 2 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(2, /product already registered/i)
+ end
end
- it "returns result code 0 without description" do
- expect(subject.register("123XX432")).to contain_exactly(0, "")
- end
- end
+ context "if there is a network error" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(SocketError)
+ end
- context "if there is a network error" do
- before do
- allow(backend.registration).to receive(:register).and_raise(SocketError)
+ it "returns result code 3 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(3, /network error/)
+ end
end
- it "returns result code 3 and description" do
- expect(subject.register("123XX432")).to contain_exactly(3, /network error/)
- end
- end
+ context "if there is a timeout" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(Timeout::Error)
+ end
- context "if there is a timeout" do
- before do
- allow(backend.registration).to receive(:register).and_raise(Timeout::Error)
+ it "returns result code 4 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(4, /timeout/)
+ end
end
- it "returns result code 4 and description" do
- expect(subject.register("123XX432")).to contain_exactly(4, /timeout/)
- end
- end
+ context "if there is an API error" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(SUSE::Connect::ApiError, "")
+ end
- context "if there is an API error" do
- before do
- allow(backend.registration).to receive(:register).and_raise(SUSE::Connect::ApiError, "")
+ it "returns result code 5 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(5, /registration server failed/)
+ end
end
- it "returns result code 5 and description" do
- expect(subject.register("123XX432")).to contain_exactly(5, /registration server failed/)
- end
- end
+ context "if there is a missing credials error" do
+ before do
+ allow(backend.registration)
+ .to receive(:register).and_raise(SUSE::Connect::MissingSccCredentialsFile)
+ end
- context "if there is a missing credials error" do
- before do
- allow(backend.registration)
- .to receive(:register).and_raise(SUSE::Connect::MissingSccCredentialsFile)
+ it "returns result code 6 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(6, /missing credentials/)
+ end
end
- it "returns result code 6 and description" do
- expect(subject.register("123XX432")).to contain_exactly(6, /missing credentials/)
- end
- end
+ context "if there is an incorrect credials error" do
+ before do
+ allow(backend.registration)
+ .to receive(:register).and_raise(SUSE::Connect::MalformedSccCredentialsFile)
+ end
- context "if there is an incorrect credials error" do
- before do
- allow(backend.registration)
- .to receive(:register).and_raise(SUSE::Connect::MalformedSccCredentialsFile)
+ it "returns result code 7 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(7, /incorrect credentials/)
+ end
end
- it "returns result code 7 and description" do
- expect(subject.register("123XX432")).to contain_exactly(7, /incorrect credentials/)
- end
- end
+ context "if there is an invalid certificate error" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(OpenSSL::SSL::SSLError)
+ end
- context "if there is an invalid certificate error" do
- before do
- allow(backend.registration).to receive(:register).and_raise(OpenSSL::SSL::SSLError)
+ it "returns result code 8 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(8, /invalid certificate/)
+ end
end
- it "returns result code 8 and description" do
- expect(subject.register("123XX432")).to contain_exactly(8, /invalid certificate/)
- end
- end
+ context "if there is an internal error" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(JSON::ParserError)
+ end
- context "if there is an internal error" do
- before do
- allow(backend.registration).to receive(:register).and_raise(JSON::ParserError)
+ it "returns result code 9 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(9, /registration server failed/)
+ end
end
- it "returns result code 9 and description" do
- expect(subject.register("123XX432")).to contain_exactly(9, /registration server failed/)
+ context "if the registration is correctly done" do
+ before do
+ allow(backend.registration).to receive(:register)
+ end
+
+ it "returns result code 0 without description" do
+ expect(subject.register("123XX432")).to contain_exactly(0, "")
+ end
end
end
end
describe "#deregister" do
before do
- allow(backend).to receive(:product).and_return("Tumbleweed")
allow(backend.registration).to receive(:reg_code).and_return("123XX432")
end
context "if there is no product selected yet" do
- before do
- allow(backend).to receive(:product).and_return(nil)
- end
-
it "returns result code 1 and description" do
expect(subject.deregister).to contain_exactly(1, /product not selected/i)
end
end
- context "if the product is not registered yet" do
+ context "if there is a selected product" do
before do
- allow(backend.registration).to receive(:reg_code).and_return(nil)
+ backend.select_product("Tumbleweed")
end
- it "returns result code 2 and description" do
- expect(subject.deregister).to contain_exactly(2, /product not registered/i)
- end
- end
+ context "if the product is not registered yet" do
+ before do
+ allow(backend.registration).to receive(:reg_code).and_return(nil)
+ end
- context "if the deregistration is correctly done" do
- before do
- allow(backend.registration).to receive(:deregister)
+ it "returns result code 2 and description" do
+ expect(subject.deregister).to contain_exactly(2, /product not registered/i)
+ end
end
- it "returns result code 0 without description" do
- expect(subject.deregister).to contain_exactly(0, "")
- end
- end
+ context "if there is a network error" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(SocketError)
+ end
- context "if there is a network error" do
- before do
- allow(backend.registration).to receive(:deregister).and_raise(SocketError)
+ it "returns result code 3 and description" do
+ expect(subject.deregister).to contain_exactly(3, /network error/)
+ end
end
- it "returns result code 3 and description" do
- expect(subject.deregister).to contain_exactly(3, /network error/)
- end
- end
+ context "if there is a timeout" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(Timeout::Error)
+ end
- context "if there is a timeout" do
- before do
- allow(backend.registration).to receive(:deregister).and_raise(Timeout::Error)
+ it "returns result code 4 and description" do
+ expect(subject.deregister).to contain_exactly(4, /timeout/)
+ end
end
- it "returns result code 4 and description" do
- expect(subject.deregister).to contain_exactly(4, /timeout/)
- end
- end
+ context "if there is an API error" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(SUSE::Connect::ApiError, "")
+ end
- context "if there is an API error" do
- before do
- allow(backend.registration).to receive(:deregister).and_raise(SUSE::Connect::ApiError, "")
+ it "returns result code 5 and description" do
+ expect(subject.deregister).to contain_exactly(5, /registration server failed/)
+ end
end
- it "returns result code 5 and description" do
- expect(subject.deregister).to contain_exactly(5, /registration server failed/)
- end
- end
+ context "if there is a missing credials error" do
+ before do
+ allow(backend.registration)
+ .to receive(:deregister).and_raise(SUSE::Connect::MissingSccCredentialsFile)
+ end
- context "if there is a missing credials error" do
- before do
- allow(backend.registration)
- .to receive(:deregister).and_raise(SUSE::Connect::MissingSccCredentialsFile)
+ it "returns result code 6 and description" do
+ expect(subject.deregister).to contain_exactly(6, /missing credentials/)
+ end
end
- it "returns result code 6 and description" do
- expect(subject.deregister).to contain_exactly(6, /missing credentials/)
- end
- end
+ context "if there is an incorrect credials error" do
+ before do
+ allow(backend.registration)
+ .to receive(:deregister).and_raise(SUSE::Connect::MalformedSccCredentialsFile)
+ end
- context "if there is an incorrect credials error" do
- before do
- allow(backend.registration)
- .to receive(:deregister).and_raise(SUSE::Connect::MalformedSccCredentialsFile)
+ it "returns result code 7 and description" do
+ expect(subject.deregister).to contain_exactly(7, /incorrect credentials/)
+ end
end
- it "returns result code 7 and description" do
- expect(subject.deregister).to contain_exactly(7, /incorrect credentials/)
- end
- end
+ context "if there is an invalid certificate error" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(OpenSSL::SSL::SSLError)
+ end
- context "if there is an invalid certificate error" do
- before do
- allow(backend.registration).to receive(:deregister).and_raise(OpenSSL::SSL::SSLError)
+ it "returns result code 8 and description" do
+ expect(subject.deregister).to contain_exactly(8, /invalid certificate/)
+ end
end
- it "returns result code 8 and description" do
- expect(subject.deregister).to contain_exactly(8, /invalid certificate/)
- end
- end
+ context "if there is an internal error" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(JSON::ParserError)
+ end
- context "if there is an internal error" do
- before do
- allow(backend.registration).to receive(:deregister).and_raise(JSON::ParserError)
+ it "returns result code 9 and description" do
+ expect(subject.deregister).to contain_exactly(9, /registration server failed/)
+ end
end
- it "returns result code 9 and description" do
- expect(subject.deregister).to contain_exactly(9, /registration server failed/)
+ context "if the deregistration is correctly done" do
+ before do
+ allow(backend.registration).to receive(:deregister)
+ end
+
+ it "returns result code 0 without description" do
+ expect(subject.deregister).to contain_exactly(0, "")
+ end
end
end
end
diff --git a/service/test/agama/software/manager_test.rb b/service/test/agama/software/manager_test.rb
index 4095f91775..984d0d89b3 100644
--- a/service/test/agama/software/manager_test.rb
+++ b/service/test/agama/software/manager_test.rb
@@ -29,6 +29,7 @@
require "agama/issue"
require "agama/registration"
require "agama/software/manager"
+require "agama/software/product"
require "agama/software/proposal"
require "agama/dbus/clients/questions"
@@ -94,28 +95,78 @@
allow(Agama::Software::Proposal).to receive(:new).and_return(proposal)
end
- shared_examples "software issues" do |tested_method|
+ describe "#new" do
before do
- allow(subject).to receive(:product).and_return(product)
- allow(subject.registration).to receive(:reg_code).and_return(reg_code)
+ allow_any_instance_of(Agama::Software::ProductBuilder)
+ .to receive(:build).and_return(products)
end
- let(:product) { "ALP-Dolomite" }
- let(:reg_code) { "123XX432" }
- let(:proposal_issues) { [Agama::Issue.new("Proposal issue")] }
+ context "if there are several products" do
+ let(:products) do
+ [Agama::Software::Product.new("test1"), Agama::Software::Product.new("test2")]
+ end
- context "if there is no product selected yet" do
- let(:product) { nil }
+ it "does not select a product by default" do
+ manager = described_class.new(config, logger)
- it "sets an issue" do
- subject.public_send(tested_method)
+ expect(manager.product).to be_nil
+ end
+
+ it "adds a not selected product issue" do
+ manager = described_class.new(config, logger)
- expect(subject.issues).to contain_exactly(an_object_having_attributes(
+ expect(manager.issues).to contain_exactly(an_object_having_attributes(
description: /product not selected/i
))
end
end
+ context "if there is only a product" do
+ let(:products) { [product] }
+
+ let(:product) do
+ Agama::Software::Product.new("test1").tap { |p| p.repositories = product_repositories }
+ end
+
+ let(:product_repositories) { [] }
+
+ it "selects the product" do
+ manager = described_class.new(config, logger)
+
+ expect(manager.product.id).to eq("test1")
+ end
+
+ context "if the product requires registration" do
+ let(:product_repositories) { [] }
+
+ it "adds a registration issue" do
+ manager = described_class.new(config, logger)
+
+ expect(manager.issues).to include(an_object_having_attributes(
+ description: /product must be registered/i
+ ))
+ end
+
+ context "if the product does not require registration" do
+ let(:product_repositories) { ["https://test"] }
+
+ it "does not add issues" do
+ expect(subject.issues).to be_empty
+ end
+ end
+ end
+ end
+ end
+
+ shared_examples "software issues" do |tested_method|
+ before do
+ subject.select_product("Tumbleweed")
+ allow(subject.registration).to receive(:reg_code).and_return(reg_code)
+ end
+
+ let(:reg_code) { "123XX432" }
+ let(:proposal_issues) { [Agama::Issue.new("Proposal issue")] }
+
context "if there are disabled repositories" do
let(:disabled_repos) do
[
@@ -204,6 +255,7 @@
stub_const("Agama::Software::Manager::REPOS_DIR", repos_dir)
stub_const("Agama::Software::Manager::REPOS_BACKUP", backup_repos_dir)
FileUtils.mkdir_p(repos_dir)
+ subject.select_product("Tumbleweed")
end
after do
@@ -242,11 +294,11 @@
it "returns the list of known products" do
products = subject.products
expect(products.size).to eq(3)
- id, data = products.first
- expect(id).to eq("Tumbleweed")
- expect(data).to include(
- "name" => "openSUSE Tumbleweed",
- "description" => String
+ expect(products).to all(be_a(Agama::Software::Product))
+ expect(products).to contain_exactly(
+ an_object_having_attributes(id: "Tumbleweed"),
+ an_object_having_attributes(id: "Leap Micro"),
+ an_object_having_attributes(id: "Leap")
)
end
end
@@ -254,7 +306,6 @@
describe "#propose" do
before do
subject.select_product("Tumbleweed")
- allow(Yast::Arch).to receive(:s390).and_return(false)
end
it "creates a new proposal for the selected product" do
@@ -264,23 +315,6 @@
subject.propose
end
- it "adds the patterns and packages to install depending on the system architecture" do
- expect(proposal).to receive(:set_resolvables)
- .with("agama", :pattern, ["enhanced_base"])
- expect(proposal).to receive(:set_resolvables)
- .with("agama", :pattern, ["optional_base"], optional: true)
- expect(proposal).to receive(:set_resolvables)
- .with("agama", :package, ["mandatory_pkg"])
- expect(proposal).to receive(:set_resolvables)
- .with("agama", :package, ["optional_pkg"], optional: true)
- subject.propose
-
- expect(Yast::Arch).to receive(:s390).and_return(true)
- expect(proposal).to receive(:set_resolvables)
- .with("agama", :package, ["mandatory_pkg", "mandatory_pkg_s390"])
- subject.propose
- end
-
include_examples "software issues", "propose"
end
diff --git a/service/test/agama/software/product_builder_test.rb b/service/test/agama/software/product_builder_test.rb
new file mode 100644
index 0000000000..e3dd6a4345
--- /dev/null
+++ b/service/test/agama/software/product_builder_test.rb
@@ -0,0 +1,286 @@
+# frozen_string_literal: true
+
+# Copyright (c) [2022-2023] SUSE LLC
+#
+# All Rights Reserved.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of version 2 of the GNU General Public License as published
+# by the Free Software Foundation.
+#
+# 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.
+
+require_relative "../../test_helper"
+require "yast"
+require "agama/config"
+require "agama/software/product"
+require "agama/software/product_builder"
+
+Yast.import "Arch"
+
+describe Agama::Software::ProductBuilder do
+ subject { described_class.new(config) }
+
+ let(:config) { Agama::Config.new(data) }
+
+ let(:data) do
+ {
+ "products" => {
+ "Test1" => {
+ "name" => "Product Test 1",
+ "description" => "This is a test product named Test 1"
+ },
+ "Test2" => {
+ "name" => "Product Test 2",
+ "description" => "This is a test product named Test 2",
+ "archs" => "x86_64,aarch64"
+ },
+ "Test3" => {
+ "name" => "Product Test 3",
+ "description" => "This is a test product named Test 3",
+ "archs" => "ppc64,aarch64"
+ }
+ },
+ "Test1" => {
+ "software" => {
+ "installation_repositories" => [
+ {
+ "url" => "https://repos/test1/x86_64/product/",
+ "archs" => "x86_64"
+ },
+ {
+ "url" => "https://repos/test1/aarch64/product/",
+ "archs" => "aarch64"
+ }
+ ],
+ "mandatory_packages" => [
+ {
+ "package" => "package1-1"
+ },
+ "package1-2",
+ {
+ "package" => "package1-3",
+ "archs" => "aarch64,x86_64"
+ },
+ {
+ "package" => "package1-4",
+ "archs" => "ppc64"
+ }
+ ],
+ "optional_packages" => ["package1-5"],
+ "mandatory_patterns" => ["pattern1-1", "pattern1-2"],
+ "optional_patterns" => [
+ {
+ "pattern" => "pattern1-3",
+ "archs" => "x86_64"
+ },
+ {
+ "pattern" => "pattern1-4",
+ "archs" => "aarch64"
+ }
+ ],
+ "base_product" => "Test1",
+ "version" => "1.0"
+ }
+ },
+ "Test2" => {
+ "software" => {
+ "mandatory_patterns" => ["pattern2-1"],
+ "base_product" => "Test2",
+ "version" => "2.0"
+ }
+ },
+ "Test3" => {
+ "software" => {
+ "installation_repositories" => ["https://repos/test3/product/"],
+ "optional_patterns" => [
+ {
+ "pattern" => "pattern3-1",
+ "archs" => "aarch64"
+ }
+ ],
+ "base_product" => "Test3"
+ }
+ }
+ }
+ end
+
+ describe "#build" do
+ context "for x86_64" do
+ before do
+ allow(Yast::Arch).to receive("x86_64").and_return(true)
+ allow(Yast::Arch).to receive("aarch64").and_return(false)
+ allow(Yast::Arch).to receive("ppc64").and_return(false)
+ allow(Yast::Arch).to receive("s390").and_return(false)
+ end
+
+ it "generates products according to the current architecture" do
+ products = subject.build
+
+ expect(products).to all(be_a(Agama::Software::Product))
+
+ expect(products).to contain_exactly(
+ an_object_having_attributes(
+ id: "Test1",
+ display_name: "Product Test 1",
+ description: "This is a test product named Test 1",
+ name: "Test1",
+ version: "1.0",
+ repositories: ["https://repos/test1/x86_64/product/"],
+ mandatory_patterns: ["pattern1-1", "pattern1-2"],
+ optional_patterns: ["pattern1-3"],
+ mandatory_packages: ["package1-1", "package1-2", "package1-3"],
+ optional_packages: ["package1-5"]
+ ),
+ an_object_having_attributes(
+ id: "Test2",
+ display_name: "Product Test 2",
+ description: "This is a test product named Test 2",
+ name: "Test2",
+ version: "2.0",
+ repositories: [],
+ mandatory_patterns: ["pattern2-1"],
+ optional_patterns: [],
+ mandatory_packages: [],
+ optional_packages: []
+ )
+ )
+ end
+ end
+
+ context "for aarch64" do
+ before do
+ allow(Yast::Arch).to receive("x86_64").and_return(false)
+ allow(Yast::Arch).to receive("aarch64").and_return(true)
+ allow(Yast::Arch).to receive("ppc64").and_return(false)
+ allow(Yast::Arch).to receive("s390").and_return(false)
+ end
+
+ it "generates products according to the current architecture" do
+ products = subject.build
+
+ expect(products).to all(be_a(Agama::Software::Product))
+
+ expect(products).to contain_exactly(
+ an_object_having_attributes(
+ id: "Test1",
+ display_name: "Product Test 1",
+ description: "This is a test product named Test 1",
+ name: "Test1",
+ version: "1.0",
+ repositories: ["https://repos/test1/aarch64/product/"],
+ mandatory_patterns: ["pattern1-1", "pattern1-2"],
+ optional_patterns: ["pattern1-4"],
+ mandatory_packages: ["package1-1", "package1-2", "package1-3"],
+ optional_packages: ["package1-5"]
+ ),
+ an_object_having_attributes(
+ id: "Test2",
+ display_name: "Product Test 2",
+ description: "This is a test product named Test 2",
+ name: "Test2",
+ version: "2.0",
+ repositories: [],
+ mandatory_patterns: ["pattern2-1"],
+ optional_patterns: [],
+ mandatory_packages: [],
+ optional_packages: []
+ ),
+ an_object_having_attributes(
+ id: "Test3",
+ display_name: "Product Test 3",
+ description: "This is a test product named Test 3",
+ name: "Test3",
+ version: nil,
+ repositories: ["https://repos/test3/product/"],
+ mandatory_patterns: [],
+ optional_patterns: ["pattern3-1"],
+ mandatory_packages: [],
+ optional_packages: []
+ )
+ )
+ end
+ end
+
+ context "for ppc64" do
+ before do
+ allow(Yast::Arch).to receive("x86_64").and_return(false)
+ allow(Yast::Arch).to receive("aarch64").and_return(false)
+ allow(Yast::Arch).to receive("ppc64").and_return(true)
+ allow(Yast::Arch).to receive("s390").and_return(false)
+ end
+
+ it "generates products according to the current architecture" do
+ products = subject.build
+
+ expect(products).to all(be_a(Agama::Software::Product))
+
+ expect(products).to contain_exactly(
+ an_object_having_attributes(
+ id: "Test1",
+ display_name: "Product Test 1",
+ description: "This is a test product named Test 1",
+ name: "Test1",
+ version: "1.0",
+ repositories: [],
+ mandatory_patterns: ["pattern1-1", "pattern1-2"],
+ optional_patterns: [],
+ mandatory_packages: ["package1-1", "package1-2", "package1-4"],
+ optional_packages: ["package1-5"]
+ ),
+ an_object_having_attributes(
+ id: "Test3",
+ display_name: "Product Test 3",
+ description: "This is a test product named Test 3",
+ name: "Test3",
+ version: nil,
+ repositories: ["https://repos/test3/product/"],
+ mandatory_patterns: [],
+ optional_patterns: [],
+ mandatory_packages: [],
+ optional_packages: []
+ )
+ )
+ end
+ end
+
+ context "for s390" do
+ before do
+ allow(Yast::Arch).to receive("x86_64").and_return(false)
+ allow(Yast::Arch).to receive("aarch64").and_return(false)
+ allow(Yast::Arch).to receive("ppc64").and_return(false)
+ allow(Yast::Arch).to receive("s390").and_return(true)
+ end
+
+ it "generates products according to the current architecture" do
+ products = subject.build
+
+ expect(products).to all(be_a(Agama::Software::Product))
+
+ expect(products).to contain_exactly(
+ an_object_having_attributes(
+ id: "Test1",
+ display_name: "Product Test 1",
+ description: "This is a test product named Test 1",
+ name: "Test1",
+ version: "1.0",
+ repositories: [],
+ mandatory_patterns: ["pattern1-1", "pattern1-2"],
+ optional_patterns: [],
+ mandatory_packages: ["package1-1", "package1-2"],
+ optional_packages: ["package1-5"]
+ )
+ )
+ end
+ end
+ end
+end
diff --git a/service/test/agama/with_issues_examples.rb b/service/test/agama/with_issues_examples.rb
index e3ce105822..5a68e8180b 100644
--- a/service/test/agama/with_issues_examples.rb
+++ b/service/test/agama/with_issues_examples.rb
@@ -27,8 +27,6 @@
let(:issues) { [Agama::Issue.new("Issue 1"), Agama::Issue.new("Issue 2")] }
it "sets the given list of issues" do
- expect(subject.issues).to be_empty
-
subject.issues = issues
expect(subject.issues).to contain_exactly(
From dfd547fc94a3ae6e30adbbc3dcbdaf258b19bc69 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Tue, 17 Oct 2023 13:38:26 +0100
Subject: [PATCH 42/97] [service] Prevent change of product if needed
- A new product cannot be selected before deregistering the current one.
- D-Bus API for seleting a product now returns a code for success or
error.
---
service/lib/agama/dbus/software/manager.rb | 38 +++++++++++++-----
.../test/agama/dbus/software/manager_test.rb | 39 ++++++++++++++++++-
2 files changed, 65 insertions(+), 12 deletions(-)
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index 135c7442c3..adf4cd0cb3 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -71,17 +71,17 @@ def issues
dbus_reader :selected_product, "s"
- dbus_method :SelectProduct, "in ProductID:s" do |product_id|
- old_product_id = backend.product&.id
+ dbus_method :SelectProduct, "in id:s, out result:(us)" do |id|
+ logger.info "Selecting product #{id}"
- if old_product_id == product_id
- logger.info "Do not changing the product as it is still the same (#{product_id})"
- return
+ code, description = select_product(id)
+
+ if code == 0
+ dbus_properties_changed(SOFTWARE_INTERFACE, { "SelectedProduct" => id }, [])
+ dbus_properties_changed(REGISTRATION_INTERFACE, { "Requirement" => requirement }, [])
end
- logger.info "Selecting product #{product_id}"
- select_product(product_id)
- dbus_properties_changed(SOFTWARE_INTERFACE, { "SelectedProduct" => product_id }, [])
+ [[code, description]]
end
# value of result hash is category, description, icon, summary and order
@@ -142,8 +142,26 @@ def selected_product
backend.product&.id || ""
end
- def select_product(product_id)
- backend.select_product(product_id)
+ # Selects a product.
+ #
+ # @param id [String] Product id.
+ # @return [Array(Integer, String)] Result code and a description.
+ # Possible result codes:
+ # 0: success
+ # 1: already selected
+ # 2: deregister first
+ # 3: unknown product
+ def select_product(id)
+ if backend.product&.id == id
+ [1, "Product is already selected"]
+ elsif backend.registration.reg_code
+ [2, "Current product must be deregistered first"]
+ else
+ backend.select_product(id)
+ [0, ""]
+ end
+ rescue ArgumentError
+ [3, "Unknown product"]
end
def probe
diff --git a/service/test/agama/dbus/software/manager_test.rb b/service/test/agama/dbus/software/manager_test.rb
index 17a5504e4d..a8f6d04e8d 100644
--- a/service/test/agama/dbus/software/manager_test.rb
+++ b/service/test/agama/dbus/software/manager_test.rb
@@ -87,6 +87,41 @@
backend.issues = []
end
+ describe "select_product" do
+ context "if the product is correctly selected" do
+ it "returns result code 0 with empty description" do
+ expect(subject.select_product("Tumbleweed")).to contain_exactly(0, "")
+ end
+ end
+
+ context "if the given product is already selected" do
+ before do
+ subject.select_product("Tumbleweed")
+ end
+
+ it "returns result code 1 and description" do
+ expect(subject.select_product("Tumbleweed")).to contain_exactly(1, /already selected/)
+ end
+ end
+
+ context "if the current product is registered" do
+ before do
+ subject.select_product("Leap")
+ allow(backend.registration).to receive(:reg_code).and_return("123XX432")
+ end
+
+ it "returns result code 2 and description" do
+ expect(subject.select_product("Tumbleweed")).to contain_exactly(2, /must be deregistered/)
+ end
+ end
+
+ context "if the product is unknown" do
+ it "returns result code 3 and description" do
+ expect(subject.select_product("Unknown")).to contain_exactly(3, /unknown product/i)
+ end
+ end
+ end
+
describe "#probe" do
it "runs the probing, setting the service as busy meanwhile" do
expect(subject.service_status).to receive(:busy)
@@ -314,7 +349,7 @@
allow(backend.registration).to receive(:register)
end
- it "returns result code 0 without description" do
+ it "returns result code 0 with empty description" do
expect(subject.register("123XX432")).to contain_exactly(0, "")
end
end
@@ -424,7 +459,7 @@
allow(backend.registration).to receive(:deregister)
end
- it "returns result code 0 without description" do
+ it "returns result code 0 with empty description" do
expect(subject.deregister).to contain_exactly(0, "")
end
end
From dd866c7996426d2e8b2d44166f4004ee896ce01e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Tue, 17 Oct 2023 15:48:56 +0100
Subject: [PATCH 43/97] [service] Document Registration D-Bus interface
---
.../bus/org.opensuse.Agama.Software1.bus.xml | 39 ++++++++--
.../org.opensuse.Agama1.Registration.bus.xml | 1 +
.../org.opensuse.Agama1.Registration.doc.xml | 75 +++++++++++++++++++
3 files changed, 107 insertions(+), 8 deletions(-)
create mode 120000 doc/dbus/bus/org.opensuse.Agama1.Registration.bus.xml
create mode 100644 doc/dbus/org.opensuse.Agama1.Registration.doc.xml
diff --git a/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml
index 82970322e0..44b5873a17 100644
--- a/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml
+++ b/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml
@@ -29,11 +29,21 @@
-
+
+
-
+
+
+
+
+
+
+
+
+
+
@@ -54,20 +64,33 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
diff --git a/doc/dbus/bus/org.opensuse.Agama1.Registration.bus.xml b/doc/dbus/bus/org.opensuse.Agama1.Registration.bus.xml
new file mode 120000
index 0000000000..d0feb248d1
--- /dev/null
+++ b/doc/dbus/bus/org.opensuse.Agama1.Registration.bus.xml
@@ -0,0 +1 @@
+org.opensuse.Agama.Software1.bus.xml
\ No newline at end of file
diff --git a/doc/dbus/org.opensuse.Agama1.Registration.doc.xml b/doc/dbus/org.opensuse.Agama1.Registration.doc.xml
new file mode 100644
index 0000000000..60890ef53c
--- /dev/null
+++ b/doc/dbus/org.opensuse.Agama1.Registration.doc.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From 6c4c742bfd0a5cd6fd752c32c52afc5ce06ba285 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Tue, 17 Oct 2023 16:05:37 +0100
Subject: [PATCH 44/97] [service] Partially document Software D-Bus interface
---
doc/dbus/org.opensuse.Agama.Software1.doc.xml | 71 +++++++++++++++++++
1 file changed, 71 insertions(+)
create mode 100644 doc/dbus/org.opensuse.Agama.Software1.doc.xml
diff --git a/doc/dbus/org.opensuse.Agama.Software1.doc.xml b/doc/dbus/org.opensuse.Agama.Software1.doc.xml
new file mode 100644
index 0000000000..927f671a8a
--- /dev/null
+++ b/doc/dbus/org.opensuse.Agama.Software1.doc.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From 2bb1a00f87e65d8d49f08892faefcebd54e835d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Tue, 17 Oct 2023 16:31:56 +0100
Subject: [PATCH 45/97] [service] Fix D-Bus documentation
---
doc/dbus/org.opensuse.Agama.Software1.doc.xml | 2 ++
doc/dbus/org.opensuse.Agama1.Registration.doc.xml | 2 ++
2 files changed, 4 insertions(+)
diff --git a/doc/dbus/org.opensuse.Agama.Software1.doc.xml b/doc/dbus/org.opensuse.Agama.Software1.doc.xml
index 927f671a8a..82bc7e5208 100644
--- a/doc/dbus/org.opensuse.Agama.Software1.doc.xml
+++ b/doc/dbus/org.opensuse.Agama.Software1.doc.xml
@@ -1,5 +1,7 @@
+
+
From cc4ba9595ceb4ee8033003848b737dea1cabbe0d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Tue, 17 Oct 2023 16:43:23 +0100
Subject: [PATCH 46/97] [web] Fix tests
---
web/src/components/software/PatternSelector.test.jsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/web/src/components/software/PatternSelector.test.jsx b/web/src/components/software/PatternSelector.test.jsx
index 58923ff724..3797997570 100644
--- a/web/src/components/software/PatternSelector.test.jsx
+++ b/web/src/components/software/PatternSelector.test.jsx
@@ -31,7 +31,7 @@ import PatternSelector from "./PatternSelector";
jest.mock("~/client");
const selectedPatternsFn = jest.fn().mockResolvedValue([]);
const getUsedSpaceFn = jest.fn().mockResolvedValue();
-const getValidationErrorsFn = jest.fn().mockResolvedValue([]);
+const getIssuesFn = jest.fn().mockResolvedValue([]);
const patternsFn = jest.fn().mockResolvedValue(test_patterns);
beforeEach(() => {
@@ -40,7 +40,7 @@ beforeEach(() => {
software: {
selectedPatterns: selectedPatternsFn,
getUsedSpace: getUsedSpaceFn,
- getValidationErrors: getValidationErrorsFn,
+ getIssues: getIssuesFn,
patterns: patternsFn
},
};
From 88141e16a8b0d1b95273b9440b1e0bef5f6d76bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Wed, 18 Oct 2023 12:45:13 +0100
Subject: [PATCH 47/97] [rust] Adapt to Software D-Bus changes
---
rust/agama-lib/src/error.rs | 2 ++
rust/agama-lib/src/software/client.rs | 17 +++++++++++---
rust/agama-lib/src/software/proxies.rs | 31 +++++++++++++++++++++-----
rust/agama-lib/src/software/store.rs | 11 +++------
4 files changed, 44 insertions(+), 17 deletions(-)
diff --git a/rust/agama-lib/src/error.rs b/rust/agama-lib/src/error.rs
index e9a9f205ca..5ecde908ca 100644
--- a/rust/agama-lib/src/error.rs
+++ b/rust/agama-lib/src/error.rs
@@ -18,6 +18,8 @@ pub enum ServiceError {
Anyhow(#[from] anyhow::Error),
#[error("Wrong user parameters: '{0:?}'")]
WrongUser(Vec),
+ #[error("Result error ({0}): {1}")]
+ Result(u32, String),
}
#[derive(Error, Debug)]
diff --git a/rust/agama-lib/src/software/client.rs b/rust/agama-lib/src/software/client.rs
index 1a56faf9da..a4bdb05ee0 100644
--- a/rust/agama-lib/src/software/client.rs
+++ b/rust/agama-lib/src/software/client.rs
@@ -30,7 +30,7 @@ impl<'a> SoftwareClient<'a> {
pub async fn products(&self) -> Result, ServiceError> {
let products: Vec = self
.software_proxy
- .available_base_products()
+ .available_products()
.await?
.into_iter()
.map(|(id, name, data)| {
@@ -50,11 +50,22 @@ impl<'a> SoftwareClient<'a> {
/// Returns the selected product to install
pub async fn product(&self) -> Result {
- Ok(self.software_proxy.selected_base_product().await?)
+ Ok(self.software_proxy.selected_product().await?)
}
/// Selects the product to install
pub async fn select_product(&self, product_id: &str) -> Result<(), ServiceError> {
- Ok(self.software_proxy.select_product(product_id).await?)
+ let result = self.software_proxy.select_product(product_id).await?;
+
+ match result {
+ (0, _) => Ok(()),
+ (3, description) => {
+ let products = self.products().await?;
+ let ids: Vec = products.into_iter().map(|p| p.id).collect();
+ let error = format!("{0}. Available products: '{1:?}'", description, ids);
+ Err(ServiceError::Result(3, error))
+ }
+ (code, description) => Err(ServiceError::Result(code, description)),
+ }
}
}
diff --git a/rust/agama-lib/src/software/proxies.rs b/rust/agama-lib/src/software/proxies.rs
index 8803fce495..b9721bbf4a 100644
--- a/rust/agama-lib/src/software/proxies.rs
+++ b/rust/agama-lib/src/software/proxies.rs
@@ -1,6 +1,6 @@
//! D-Bus interface proxies for: `org.opensuse.Agama.Software1.*`
//!
-//! This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data.`.
+//! This code was generated by `zbus-xmlgen` `3.1.1` from DBus introspection data.
use zbus::dbus_proxy;
#[dbus_proxy(
@@ -9,6 +9,9 @@ use zbus::dbus_proxy;
default_path = "/org/opensuse/Agama/Software1"
)]
trait Software1 {
+ /// AddPattern method
+ fn add_pattern(&self, id: &str) -> zbus::Result<()>;
+
/// Finish method
fn finish(&self) -> zbus::Result<()>;
@@ -18,6 +21,12 @@ trait Software1 {
/// IsPackageInstalled method
fn is_package_installed(&self, name: &str) -> zbus::Result;
+ /// ListPatterns method
+ fn list_patterns(
+ &self,
+ filtered: bool,
+ ) -> zbus::Result>;
+
/// Probe method
fn probe(&self) -> zbus::Result<()>;
@@ -27,15 +36,21 @@ trait Software1 {
/// ProvisionsSelected method
fn provisions_selected(&self, provisions: &[&str]) -> zbus::Result>;
+ /// RemovePattern method
+ fn remove_pattern(&self, id: &str) -> zbus::Result<()>;
+
/// SelectProduct method
- fn select_product(&self, product_id: &str) -> zbus::Result<()>;
+ fn select_product(&self, id: &str) -> zbus::Result<(u32, String)>;
+
+ /// SetUserPatterns method
+ fn set_user_patterns(&self, ids: &[&str]) -> zbus::Result<()>;
/// UsedDiskSpace method
fn used_disk_space(&self) -> zbus::Result;
- /// AvailableBaseProducts property
+ /// AvailableProducts property
#[dbus_proxy(property)]
- fn available_base_products(
+ fn available_products(
&self,
) -> zbus::Result<
Vec<(
@@ -45,9 +60,13 @@ trait Software1 {
)>,
>;
- /// SelectedBaseProduct property
+ /// SelectedPatterns property
+ #[dbus_proxy(property)]
+ fn selected_patterns(&self) -> zbus::Result>;
+
+ /// SelectedProduct property
#[dbus_proxy(property)]
- fn selected_base_product(&self) -> zbus::Result;
+ fn selected_product(&self) -> zbus::Result;
}
#[dbus_proxy(
diff --git a/rust/agama-lib/src/software/store.rs b/rust/agama-lib/src/software/store.rs
index 45706ed531..9a8a5fb365 100644
--- a/rust/agama-lib/src/software/store.rs
+++ b/rust/agama-lib/src/software/store.rs
@@ -29,15 +29,10 @@ impl<'a> SoftwareStore<'a> {
pub async fn store(&self, settings: &SoftwareSettings) -> Result<(), ServiceError> {
if let Some(product) = &settings.product {
- let products = self.software_client.products().await?;
- let ids: Vec = products.into_iter().map(|p| p.id).collect();
- if ids.contains(product) {
- self.software_client.select_product(product).await?;
- self.manager_client.probe().await?;
- } else {
- return Err(ServiceError::UnknownProduct(product.clone(), ids));
- }
+ self.software_client.select_product(product).await?;
+ self.manager_client.probe().await?;
}
+
Ok(())
}
}
From 4032595112ba54b6fdde482c8226916bd6e1e09d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Wed, 18 Oct 2023 15:40:00 +0100
Subject: [PATCH 48/97] [rust] Use better error name
---
rust/agama-lib/src/error.rs | 6 ++----
rust/agama-lib/src/software/client.rs | 4 ++--
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/rust/agama-lib/src/error.rs b/rust/agama-lib/src/error.rs
index 5ecde908ca..7124e00cbb 100644
--- a/rust/agama-lib/src/error.rs
+++ b/rust/agama-lib/src/error.rs
@@ -10,16 +10,14 @@ pub enum ServiceError {
DBus(#[from] zbus::Error),
#[error("Could not connect to Agama bus at '{0}'")]
DBusConnectionError(String, #[source] zbus::Error),
- #[error("Unknown product '{0}'. Available products: '{1:?}'")]
- UnknownProduct(String, Vec),
// it's fine to say only "Error" because the original
// specific error will be printed too
#[error("Error: {0}")]
Anyhow(#[from] anyhow::Error),
#[error("Wrong user parameters: '{0:?}'")]
WrongUser(Vec),
- #[error("Result error ({0}): {1}")]
- Result(u32, String),
+ #[error("Error: {0}")]
+ UnsuccessfulAction(String),
}
#[derive(Error, Debug)]
diff --git a/rust/agama-lib/src/software/client.rs b/rust/agama-lib/src/software/client.rs
index a4bdb05ee0..8e90b5a474 100644
--- a/rust/agama-lib/src/software/client.rs
+++ b/rust/agama-lib/src/software/client.rs
@@ -63,9 +63,9 @@ impl<'a> SoftwareClient<'a> {
let products = self.products().await?;
let ids: Vec = products.into_iter().map(|p| p.id).collect();
let error = format!("{0}. Available products: '{1:?}'", description, ids);
- Err(ServiceError::Result(3, error))
+ Err(ServiceError::UnsuccessfulAction(error))
}
- (code, description) => Err(ServiceError::Result(code, description)),
+ (_, description) => Err(ServiceError::UnsuccessfulAction(description)),
}
}
}
From 70b2def848ea87fc37076ebd4005f4692ad1589e Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Wed, 18 Oct 2023 21:35:59 +0200
Subject: [PATCH 49/97] move service operations to software and fix target init
---
service/lib/agama/registration.rb | 51 ++-------------------------
service/lib/agama/software/manager.rb | 48 +++++++++++++++++++++++++
2 files changed, 50 insertions(+), 49 deletions(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index 911e5d6d9c..7e1d9369b6 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -80,7 +80,7 @@ def register(code, email: "")
SUSE::Connect::YaST.create_credentials_file(login, password, @credentials_file)
end
Y2Packager::NewRepositorySetup.instance.add_service(@service.name)
- add_service(@service)
+ @software.add_service(@service)
@reg_code = code
@email = email
@@ -89,7 +89,7 @@ def register(code, email: "")
def deregister
Y2Packager::NewRepositorySetup.instance.services.delete(@service.name)
- remove_service(@service)
+ @software.remove_service(@service)
connect_params = {
token: reg_code,
@@ -149,53 +149,6 @@ def run_on_change_callbacks
@on_change_callbacks&.map(&:call)
end
- # code is based on https://github.com/yast/yast-registration/blob/master/src/lib/registration/sw_mgmt.rb#L365
- # TODO: move it to software manager
- # rubocop:disable Metrics/AbcSize
- def add_service(service)
- # save repositories before refreshing added services (otherwise
- # pkg-bindings will treat them as removed by the service refresh and
- # unload them)
- if !Yast::Pkg.SourceSaveAll
- # error message
- @logger.error("Saving repository configuration failed.")
- end
-
- @logger.info "Adding service #{service.name.inspect} (#{service.url})"
- if !Yast::Pkg.ServiceAdd(service.name, service.url.to_s)
- raise format("Adding service '%s' failed.", service.name)
- end
-
- if !Yast::Pkg.ServiceSet(service.name, "autorefresh" => true)
- # error message
- raise format("Updating service '%s' failed.", service.name)
- end
-
- # refresh works only for saved services
- if !Yast::Pkg.ServiceSave(service.name)
- # error message
- raise format("Saving service '%s' failed.", service.name)
- end
-
- # Force refreshing due timing issues (bnc#967828)
- if !Yast::Pkg.ServiceForceRefresh(service.name)
- # error message
- raise format("Refreshing service '%s' failed.", service.name)
- end
- ensure
- Yast::Pkg.SourceSaveAll
- end
- # rubocop:enable Metrics/AbcSize
-
- # TODO: move it to software manager
- def remove_service(service)
- if Yast::Pkg.ServiceDelete(service.name) && !Pkg.SourceSaveAll
- raise format("Removing service '%s' failed.", service_name)
- end
-
- true
- end
-
# taken from https://github.com/yast/yast-registration/blob/master/src/lib/registration/url_helpers.rb#L109
def credentials_from_url(url)
parsed_url = URI(url)
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index dd88d5881b..107606364c 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -275,6 +275,54 @@ def registration
@registration ||= Registration.new(self, @logger)
end
+ # 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_repos
+ # save repositories before refreshing added services (otherwise
+ # pkg-bindings will treat them as removed by the service refresh and
+ # unload them)
+ if !Yast::Pkg.SourceSaveAll
+ # error message
+ @logger.error("Saving repository configuration failed.")
+ end
+
+ @logger.info "Adding service #{service.name.inspect} (#{service.url})"
+ if !Yast::Pkg.ServiceAdd(service.name, service.url.to_s)
+ raise format("Adding service '%s' failed.", service.name)
+ end
+
+ if !Yast::Pkg.ServiceSet(service.name, "autorefresh" => true)
+ # error message
+ raise format("Updating service '%s' failed.", service.name)
+ end
+
+ # refresh works only for saved services
+ if !Yast::Pkg.ServiceSave(service.name)
+ # error message
+ raise format("Saving service '%s' failed.", service.name)
+ end
+
+ # Force refreshing due timing issues (bnc#967828)
+ if !Yast::Pkg.ServiceForceRefresh(service.name)
+ # error message
+ raise format("Refreshing service '%s' failed.", service.name)
+ end
+ ensure
+ Yast::Pkg.SourceSaveAll
+ end
+ # rubocop:enable Metrics/AbcSize
+
+ def remove_service(service)
+ if Yast::Pkg.ServiceDelete(service.name) && !Pkg.SourceSaveAll
+ raise format("Removing service '%s' failed.", service_name)
+ end
+
+ true
+ end
+
+
private
# @return [Agama::Config]
From 5a38dac3ee5ee6ea1963ed2f0c033a0b13aa6838 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Wed, 18 Oct 2023 21:50:33 +0200
Subject: [PATCH 50/97] fix namespace
---
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 107606364c..ef1a1be3b6 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -315,7 +315,7 @@ def add_service(service)
# rubocop:enable Metrics/AbcSize
def remove_service(service)
- if Yast::Pkg.ServiceDelete(service.name) && !Pkg.SourceSaveAll
+ if Yast::Pkg.ServiceDelete(service.name) && !Yast::Pkg.SourceSaveAll
raise format("Removing service '%s' failed.", service_name)
end
From 254dfe4ced069c383e71943b1188d9c8a4d7c72e Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Wed, 18 Oct 2023 21:58:11 +0200
Subject: [PATCH 51/97] add dependency on suse connect it is used
---
service/package/gem2rpm.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/service/package/gem2rpm.yml b/service/package/gem2rpm.yml
index cc70ad5bc8..b0e012ffa8 100644
--- a/service/package/gem2rpm.yml
+++ b/service/package/gem2rpm.yml
@@ -28,6 +28,8 @@
Requires: open-iscsi
Requires: yast2-iscsi-client >= 4.5.7
Requires: yast2-users
+ # required for registration
+ Requires: suseconnect-ruby-bindings
# yast2 with ArchFilter
Requires: yast2 >= 4.5.20
%ifarch s390 s390x
From 6c6cba26411cc1c2b276d74c8360f948259e6816 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Thu, 19 Oct 2023 15:28:56 +0100
Subject: [PATCH 52/97] [service] Export a separate product object
---
service/lib/agama/dbus/clients/software.rb | 12 +-
service/lib/agama/dbus/clients/with_issues.rb | 24 +-
service/lib/agama/dbus/software.rb | 3 +-
service/lib/agama/dbus/software/manager.rb | 201 +-----------
service/lib/agama/dbus/software/product.rb | 285 ++++++++++++++++++
service/lib/agama/dbus/software_service.rb | 1 +
service/lib/agama/software/manager.rb | 46 ++-
7 files changed, 346 insertions(+), 226 deletions(-)
create mode 100644 service/lib/agama/dbus/software/product.rb
diff --git a/service/lib/agama/dbus/clients/software.rb b/service/lib/agama/dbus/clients/software.rb
index 5d9321698a..8f94786773 100644
--- a/service/lib/agama/dbus/clients/software.rb
+++ b/service/lib/agama/dbus/clients/software.rb
@@ -42,6 +42,9 @@ def initialize
@dbus_object = service["/org/opensuse/Agama/Software1"]
@dbus_object.introspect
+ @dbus_product = service["/org/opensuse/Agama/Software1/Product"]
+ @dbus_product.introspect
+
@dbus_proposal = service["/org/opensuse/Agama/Software1/Proposal"]
@dbus_proposal.introspect
end
@@ -55,7 +58,7 @@ def service_name
#
# @return [Array>] name and display name of each product
def available_products
- dbus_object["org.opensuse.Agama.Software1"]["AvailableProducts"].map do |l|
+ dbus_product["org.opensuse.Agama.Software1.Product"]["AvailableProducts"].map do |l|
l[0..1]
end
end
@@ -64,7 +67,7 @@ def available_products
#
# @return [String, nil] name of the product
def selected_product
- product = dbus_object["org.opensuse.Agama.Software1"]["SelectedProduct"]
+ product = dbus_product["org.opensuse.Agama.Software1.Product"]["SelectedProduct"]
return nil if product.empty?
product
@@ -74,7 +77,7 @@ def selected_product
#
# @param name [String]
def select_product(name)
- dbus_object.SelectProduct(name)
+ dbus_product.SelectProduct(name)
end
# Starts the probing process
@@ -180,6 +183,9 @@ def on_product_selected(&block)
# @return [::DBus::Object]
attr_reader :dbus_object
+ # @return [::DBus::Object]
+ attr_reader :dbus_product
+
# @return [::DBus::Object]
attr_reader :dbus_proposal
end
diff --git a/service/lib/agama/dbus/clients/with_issues.rb b/service/lib/agama/dbus/clients/with_issues.rb
index a4a1dc363e..cc7ba3ea4e 100644
--- a/service/lib/agama/dbus/clients/with_issues.rb
+++ b/service/lib/agama/dbus/clients/with_issues.rb
@@ -28,10 +28,25 @@ module WithIssues
ISSUES_IFACE = "org.opensuse.Agama1.Issues"
private_constant :ISSUES_IFACE
- # Returns the issues
+ # Returns issues from all objects that implement the issues interface.
#
# @return [Array]
def issues
+ objects_with_issues_interface.map { |o| issues_from(o) }.flatten
+ end
+
+ # Determines whether there are errors
+ #
+ # @return [Boolean]
+ def errors?
+ issues.any?(&:error?)
+ end
+
+ def objects_with_issues_interface
+ service.root.descendant_objects.select { |o| o.interfaces.include?(ISSUES_IFACE) }
+ end
+
+ def issues_from(dbus_object)
sources = [nil, Issue::Source::SYSTEM, Issue::Source::CONFIG]
severities = [Issue::Severity::WARN, Issue::Severity::ERROR]
@@ -42,13 +57,6 @@ def issues
severity: severities[dbus_issue[3]])
end
end
-
- # Determines whether there are errors
- #
- # @return [Boolean]
- def errors?
- issues.any?(&:error?)
- end
end
end
end
diff --git a/service/lib/agama/dbus/software.rb b/service/lib/agama/dbus/software.rb
index 670cc4517e..ace66f64fc 100644
--- a/service/lib/agama/dbus/software.rb
+++ b/service/lib/agama/dbus/software.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Copyright (c) [2022] SUSE LLC
+# Copyright (c) [2022-2023] SUSE LLC
#
# All Rights Reserved.
#
@@ -28,4 +28,5 @@ module Software
end
require "agama/dbus/software/manager"
+require "agama/dbus/software/product"
require "agama/dbus/software/proposal"
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index adf4cd0cb3..06df099ee3 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -20,7 +20,6 @@
# find current contact information at www.suse.com.
require "dbus"
-require "suse/connect"
require "agama/dbus/base_object"
require "agama/dbus/clients/locale"
require "agama/dbus/clients/network"
@@ -28,7 +27,6 @@
require "agama/dbus/interfaces/progress"
require "agama/dbus/interfaces/service_status"
require "agama/dbus/with_service_status"
-require "agama/registration"
module Agama
module DBus
@@ -56,7 +54,7 @@ def initialize(backend, logger)
@selected_patterns = {}
end
- # List of issues, see {DBus::Interfaces::Issues}
+ # List of software related issues, see {DBus::Interfaces::Issues}
#
# @return [Array]
def issues
@@ -67,23 +65,6 @@ def issues
private_constant :SOFTWARE_INTERFACE
dbus_interface SOFTWARE_INTERFACE do
- dbus_reader :available_products, "a(ssa{sv})"
-
- dbus_reader :selected_product, "s"
-
- dbus_method :SelectProduct, "in id:s, out result:(us)" do |id|
- logger.info "Selecting product #{id}"
-
- code, description = select_product(id)
-
- if code == 0
- dbus_properties_changed(SOFTWARE_INTERFACE, { "SelectedProduct" => id }, [])
- dbus_properties_changed(REGISTRATION_INTERFACE, { "Requirement" => requirement }, [])
- end
-
- [[code, description]]
- end
-
# value of result hash is category, description, icon, summary and order
dbus_method :ListPatterns, "in Filtered:b, out Result:a{s(sssss)}" do |filtered|
[
@@ -129,41 +110,6 @@ def issues
dbus_method(:Finish) { finish }
end
- def available_products
- backend.products.map do |product|
- [product.id, product.display_name, { "description" => product.description }]
- end
- end
-
- # Returns the selected base product
- #
- # @return [String] Product ID or an empty string if no product is selected
- def selected_product
- backend.product&.id || ""
- end
-
- # Selects a product.
- #
- # @param id [String] Product id.
- # @return [Array(Integer, String)] Result code and a description.
- # Possible result codes:
- # 0: success
- # 1: already selected
- # 2: deregister first
- # 3: unknown product
- def select_product(id)
- if backend.product&.id == id
- [1, "Product is already selected"]
- elsif backend.registration.reg_code
- [2, "Current product must be deregistered first"]
- else
- backend.select_product(id)
- [0, ""]
- end
- rescue ArgumentError
- [3, "Unknown product"]
- end
-
def probe
busy_while { backend.probe }
end
@@ -182,103 +128,6 @@ def finish
busy_while { backend.finish }
end
- REGISTRATION_INTERFACE = "org.opensuse.Agama1.Registration"
- private_constant :REGISTRATION_INTERFACE
-
- dbus_interface REGISTRATION_INTERFACE do
- dbus_reader(:reg_code, "s")
-
- dbus_reader(:email, "s")
-
- dbus_reader(:requirement, "u")
-
- dbus_method(:Register, "in reg_code:s, in options:a{sv}, out result:(us)") do |*args|
- [register(args[0], email: args[1]["Email"])]
- end
-
- dbus_method(:Deregister, "out result:(us)") { [deregister] }
- end
-
- def reg_code
- backend.registration.reg_code || ""
- end
-
- def email
- backend.registration.email || ""
- end
-
- # Registration requirement.
- #
- # @return [Integer] Possible values:
- # 0: not required
- # 1: optional
- # 2: mandatory
- def requirement
- case backend.registration.requirement
- when Agama::Registration::Requirement::MANDATORY
- 2
- when Agama::Registration::Requirement::OPTIONAL
- 1
- else
- 0
- end
- end
-
- # Tries to register with the given registration code.
- #
- # @param reg_code [String]
- # @param email [String, nil]
- #
- # @return [Array(Integer, String)] Result code and a description.
- # Possible result codes:
- # 0: success
- # 1: missing product
- # 2: already registered
- # 3: network error
- # 4: timeout error
- # 5: api error
- # 6: missing credentials
- # 7: incorrect credentials
- # 8: invalid certificate
- # 9: internal error (e.g., parsing json data)
- def register(reg_code, email: nil)
- if !backend.product
- [1, "Product not selected yet"]
- elsif backend.registration.reg_code
- [2, "Product already registered"]
- else
- connect_result(first_error_code: 3) do
- backend.registration.register(reg_code, email: email)
- end
- end
- end
-
- # Tries to deregister.
- #
- # @return [Array(Integer, String)] Result code and a description.
- # Possible result codes:
- # 0: success
- # 1: missing product
- # 2: not registered yet
- # 3: network error
- # 4: timeout error
- # 5: api error
- # 6: missing credentials
- # 7: incorrect credentials
- # 8: invalid certificate
- # 9: internal error (e.g., parsing json data)
- def deregister
- if !backend.product
- [1, "Product not selected yet"]
- elsif !backend.registration.reg_code
- [2, "Product not registered yet"]
- else
- connect_result(first_error_code: 3) do
- backend.registration.deregister
- end
- end
- end
-
private
# @return [Agama::Software]
@@ -300,8 +149,6 @@ def register_callbacks
self.selected_patterns = compute_patterns
end
- backend.registration.on_change { registration_properties_changed }
-
backend.on_issues_change { issues_properties_changed }
end
@@ -315,52 +162,6 @@ def compute_patterns
patterns
end
-
- def registration_properties_changed
- dbus_properties_changed(REGISTRATION_INTERFACE,
- interfaces_and_properties[REGISTRATION_INTERFACE], [])
- end
-
- # Result from calling to SUSE connect.
- #
- # @raise [Exception] if an unexpected error is found.
- #
- # @return [Array(Integer, String)] List including a result code and a description
- # (e.g., [1, "Connection to registration server failed (network error)"]).
- def connect_result(first_error_code: 1, &block)
- block.call
- [0, ""]
- rescue SocketError => e
- connect_result_from_error(e, first_error_code, "network error")
- rescue Timeout::Error => e
- connect_result_from_error(e, first_error_code + 1, "timeout")
- rescue SUSE::Connect::ApiError => e
- connect_result_from_error(e, first_error_code + 2)
- rescue SUSE::Connect::MissingSccCredentialsFile => e
- connect_result_from_error(e, first_error_code + 3, "missing credentials")
- rescue SUSE::Connect::MalformedSccCredentialsFile => e
- connect_result_from_error(e, first_error_code + 4, "incorrect credentials")
- rescue OpenSSL::SSL::SSLError => e
- connect_result_from_error(e, first_error_code + 5, "invalid certificate")
- rescue JSON::ParserError => e
- connect_result_from_error(e, first_error_code + 6)
- end
-
- # Generates a result from a given error.
- #
- # @param error [Exception]
- # @param error_code [Integer]
- # @param details [String, nil]
- #
- # @return [Array(Integer, String)] List including an error code and a description.
- def connect_result_from_error(error, error_code, details = nil)
- logger.error("Error connecting to registration server: #{error}")
-
- description = "Connection to registration server failed"
- description += " (#{details})" if details
-
- [error_code, description]
- end
end
end
end
diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb
new file mode 100644
index 0000000000..2029d9c2a7
--- /dev/null
+++ b/service/lib/agama/dbus/software/product.rb
@@ -0,0 +1,285 @@
+# frozen_string_literal: true
+
+# Copyright (c) [2023] SUSE LLC
+#
+# All Rights Reserved.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of version 2 of the GNU General Public License as published
+# by the Free Software Foundation.
+#
+# 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.
+
+require "dbus"
+require "suse/connect"
+require "agama/dbus/base_object"
+require "agama/dbus/interfaces/issues"
+require "agama/registration"
+
+module Agama
+ module DBus
+ module Software
+ # D-Bus object to manage product configuration
+ class Product < BaseObject
+ include Interfaces::Issues
+
+ PATH = "/org/opensuse/Agama/Software1/Product"
+ private_constant :PATH
+
+ # Constructor
+ #
+ # @param backend [Agama::Software]
+ # @param logger [Logger]
+ def initialize(backend, logger)
+ super(PATH, logger: logger)
+ @backend = backend
+ @logger = logger
+ register_callbacks
+ end
+
+ # List of issues, see {DBus::Interfaces::Issues}
+ #
+ # @return [Array]
+ def issues
+ backend.product_issues
+ end
+
+ def available_products
+ backend.products.map do |product|
+ [product.id, product.display_name, { "description" => product.description }]
+ end
+ end
+
+ # Returns the selected base product
+ #
+ # @return [String] Product ID or an empty string if no product is selected
+ def selected_product
+ backend.product&.id || ""
+ end
+
+ # Selects a product.
+ #
+ # @param id [String] Product id.
+ # @return [Array(Integer, String)] Result code and a description.
+ # Possible result codes:
+ # 0: success
+ # 1: already selected
+ # 2: deregister first
+ # 3: unknown product
+ def select_product(id)
+ if backend.product&.id == id
+ [1, "Product is already selected"]
+ elsif backend.registration.reg_code
+ [2, "Current product must be deregistered first"]
+ else
+ backend.select_product(id)
+ [0, ""]
+ end
+ rescue ArgumentError
+ [3, "Unknown product"]
+ end
+
+ PRODUCT_INTERFACE = "org.opensuse.Agama.Software1.Product"
+ private_constant :PRODUCT_INTERFACE
+
+ dbus_interface PRODUCT_INTERFACE do
+ dbus_reader :available_products, "a(ssa{sv})"
+
+ dbus_reader :selected_product, "s"
+
+ dbus_method :SelectProduct, "in id:s, out result:(us)" do |id|
+ logger.info "Selecting product #{id}"
+
+ code, description = select_product(id)
+
+ if code == 0
+ dbus_properties_changed(PRODUCT_INTERFACE, { "SelectedProduct" => id }, [])
+ dbus_properties_changed(REGISTRATION_INTERFACE, { "Requirement" => requirement }, [])
+ # FIXME: Product issues might change after selecting a product. Nevertheless,
+ # #on_issues_change callbacks should be used for emitting issues signals, ensuring
+ # they are emitted every time the backend changes its issues. Currently,
+ # #on_issues_change cannot be used for product issues. Note that Software::Manager
+ # backend takes care of both software and product issues. And it already uses
+ # #on_issues_change callbacks for software related issues.
+ issues_properties_changed
+ end
+
+ [[code, description]]
+ end
+ end
+
+ def reg_code
+ backend.registration.reg_code || ""
+ end
+
+ def email
+ backend.registration.email || ""
+ end
+
+ # Registration requirement.
+ #
+ # @return [Integer] Possible values:
+ # 0: not required
+ # 1: optional
+ # 2: mandatory
+ def requirement
+ case backend.registration.requirement
+ when Agama::Registration::Requirement::MANDATORY
+ 2
+ when Agama::Registration::Requirement::OPTIONAL
+ 1
+ else
+ 0
+ end
+ end
+
+ # Tries to register with the given registration code.
+ #
+ # @param reg_code [String]
+ # @param email [String, nil]
+ #
+ # @return [Array(Integer, String)] Result code and a description.
+ # Possible result codes:
+ # 0: success
+ # 1: missing product
+ # 2: already registered
+ # 3: network error
+ # 4: timeout error
+ # 5: api error
+ # 6: missing credentials
+ # 7: incorrect credentials
+ # 8: invalid certificate
+ # 9: internal error (e.g., parsing json data)
+ def register(reg_code, email: nil)
+ if !backend.product
+ [1, "Product not selected yet"]
+ elsif backend.registration.reg_code
+ [2, "Product already registered"]
+ else
+ connect_result(first_error_code: 3) do
+ backend.registration.register(reg_code, email: email)
+ end
+ end
+ end
+
+ # Tries to deregister.
+ #
+ # @return [Array(Integer, String)] Result code and a description.
+ # Possible result codes:
+ # 0: success
+ # 1: missing product
+ # 2: not registered yet
+ # 3: network error
+ # 4: timeout error
+ # 5: api error
+ # 6: missing credentials
+ # 7: incorrect credentials
+ # 8: invalid certificate
+ # 9: internal error (e.g., parsing json data)
+ def deregister
+ if !backend.product
+ [1, "Product not selected yet"]
+ elsif !backend.registration.reg_code
+ [2, "Product not registered yet"]
+ else
+ connect_result(first_error_code: 3) do
+ backend.registration.deregister
+ end
+ end
+ end
+
+ REGISTRATION_INTERFACE = "org.opensuse.Agama1.Registration"
+ private_constant :REGISTRATION_INTERFACE
+
+ dbus_interface REGISTRATION_INTERFACE do
+ dbus_reader(:reg_code, "s")
+
+ dbus_reader(:email, "s")
+
+ dbus_reader(:requirement, "u")
+
+ dbus_method(:Register, "in reg_code:s, in options:a{sv}, out result:(us)") do |*args|
+ [register(args[0], email: args[1]["Email"])]
+ end
+
+ dbus_method(:Deregister, "out result:(us)") { [deregister] }
+ end
+
+ private
+
+ # @return [Agama::Software]
+ attr_reader :backend
+
+ # @return [Logger]
+ attr_reader :logger
+
+ # Registers callback to be called
+ def register_callbacks
+ # FIXME: Product issues might change after changing the registration. Nevertheless,
+ # #on_issues_change callbacks should be used for emitting issues signals, ensuring they
+ # are emitted every time the backend changes its issues. Currently, #on_issues_change
+ # cannot be used for product issues. Note that Software::Manager backend takes care of
+ # both software and product issues. And it already uses #on_issues_change callbacks for
+ # software related issues.
+ backend.registration.on_change { issues_properties_changed }
+ backend.registration.on_change { registration_properties_changed }
+ end
+
+ def registration_properties_changed
+ dbus_properties_changed(REGISTRATION_INTERFACE,
+ interfaces_and_properties[REGISTRATION_INTERFACE], [])
+ end
+
+ # Result from calling to SUSE connect.
+ #
+ # @raise [Exception] if an unexpected error is found.
+ #
+ # @return [Array(Integer, String)] List including a result code and a description
+ # (e.g., [1, "Connection to registration server failed (network error)"]).
+ def connect_result(first_error_code: 1, &block)
+ block.call
+ [0, ""]
+ rescue SocketError => e
+ connect_result_from_error(e, first_error_code, "network error")
+ rescue Timeout::Error => e
+ connect_result_from_error(e, first_error_code + 1, "timeout")
+ rescue SUSE::Connect::ApiError => e
+ connect_result_from_error(e, first_error_code + 2)
+ rescue SUSE::Connect::MissingSccCredentialsFile => e
+ connect_result_from_error(e, first_error_code + 3, "missing credentials")
+ rescue SUSE::Connect::MalformedSccCredentialsFile => e
+ connect_result_from_error(e, first_error_code + 4, "incorrect credentials")
+ rescue OpenSSL::SSL::SSLError => e
+ connect_result_from_error(e, first_error_code + 5, "invalid certificate")
+ rescue JSON::ParserError => e
+ connect_result_from_error(e, first_error_code + 6)
+ end
+
+ # Generates a result from a given error.
+ #
+ # @param error [Exception]
+ # @param error_code [Integer]
+ # @param details [String, nil]
+ #
+ # @return [Array(Integer, String)] List including an error code and a description.
+ def connect_result_from_error(error, error_code, details = nil)
+ logger.error("Error connecting to registration server: #{error}")
+
+ description = "Connection to registration server failed"
+ description += " (#{details})" if details
+
+ [error_code, description]
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/dbus/software_service.rb b/service/lib/agama/dbus/software_service.rb
index a2d7100ec9..1ce788a18e 100644
--- a/service/lib/agama/dbus/software_service.rb
+++ b/service/lib/agama/dbus/software_service.rb
@@ -83,6 +83,7 @@ def service
def dbus_objects
@dbus_objects ||= [
Agama::DBus::Software::Manager.new(@backend, logger),
+ Agama::DBus::Software::Product.new(@backend, logger),
Agama::DBus::Software::Proposal.new(logger)
]
end
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index ef1a1be3b6..e2d6167cea 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -43,7 +43,15 @@
module Agama
module Software
- # This class is responsible for software handling
+ # This class is responsible for software handling.
+ #
+ # FIXME: This class has too many responsibilities:
+ # * Address the software service workflow (probe, propose, install).
+ # * Manages repositories, packages, patterns, services.
+ # * Manages product selection.
+ # * Manages software and product related issues.
+ #
+ # It shoud be splitted in separate and smaller classes.
class Manager
include Helpers
include WithIssues
@@ -322,6 +330,17 @@ def remove_service(service)
true
end
+ # Issues associated to the product.
+ #
+ # These issues are not considered as software issues, see {#update_issues}.
+ #
+ # @return [Array]
+ def product_issues
+ issues = []
+ issues << missing_product_issue unless product
+ issues << missing_registration_issue if missing_registration?
+ issues
+ end
private
@@ -391,16 +410,16 @@ def selected_patterns_changed
@selected_patterns_change_callbacks.each(&:call)
end
- # Updates the list of issues.
+ # Updates the list of software issues.
def update_issues
self.issues = current_issues
end
- # List of current issues.
+ # List of current software issues.
#
# @return [Array]
def current_issues
- return [missing_product_issue] unless product
+ return [] unless product
issues = repos_issues
@@ -408,19 +427,9 @@ def current_issues
# packages. Those issues does not make any sense if there are no repositories to install
# from.
issues += proposal.issues if repositories.enabled.any?
- issues << missing_registration_issue if missing_registration?
issues
end
- # Issue when a product is missing
- #
- # @return [Agama::Issue]
- def missing_product_issue
- Issue.new("Product not selected yet",
- source: Issue::Source::CONFIG,
- severity: Issue::Severity::ERROR)
- end
-
# Issues related to the software proposal.
#
# Repositories that could not be probed are reported as errors.
@@ -434,6 +443,15 @@ def repos_issues
end
end
+ # Issue when a product is missing
+ #
+ # @return [Agama::Issue]
+ def missing_product_issue
+ Issue.new("Product not selected yet",
+ source: Issue::Source::CONFIG,
+ severity: Issue::Severity::ERROR)
+ end
+
# Issue when a product requires registration but it is not registered yet.
#
# @return [Agama::Issue]
From fe0d63b6c8f3f367d05028d11e96dd943de4cb53 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Thu, 19 Oct 2023 17:37:48 +0100
Subject: [PATCH 53/97] [web] Adapt software client
---
web/src/client/software.js | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/web/src/client/software.js b/web/src/client/software.js
index 081f1ff1fe..193f90575e 100644
--- a/web/src/client/software.js
+++ b/web/src/client/software.js
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022] SUSE LLC
+ * Copyright (c) [2022-2023] SUSE LLC
*
* All Rights Reserved.
*
@@ -27,6 +27,8 @@ import { WithIssues, WithStatus, WithProgress } from "./mixins";
const SOFTWARE_SERVICE = "org.opensuse.Agama.Software1";
const SOFTWARE_IFACE = "org.opensuse.Agama.Software1";
const SOFTWARE_PATH = "/org/opensuse/Agama/Software1";
+const PRODUCT_IFACE = "org.opensuse.Agama.Software1.Product";
+const PRODUCT_PATH = "/org/opensuse/Agama/Software1/Product";
/**
* @typedef {object} Product
@@ -64,7 +66,7 @@ class SoftwareBaseClient {
* @return {Promise>}
*/
async getProducts() {
- const proxy = await this.client.proxy(SOFTWARE_IFACE);
+ const proxy = await this.client.proxy(PRODUCT_IFACE);
return proxy.AvailableProducts.map(product => {
const [id, name, meta] = product;
return { id, name, description: meta.description?.v };
@@ -136,7 +138,7 @@ class SoftwareBaseClient {
*/
async getSelectedProduct() {
const products = await this.getProducts();
- const proxy = await this.client.proxy(SOFTWARE_IFACE);
+ const proxy = await this.client.proxy(PRODUCT_IFACE);
if (proxy.SelectedProduct === "") {
return null;
}
@@ -149,7 +151,7 @@ class SoftwareBaseClient {
* @param {string} id - product ID
*/
async selectProduct(id) {
- const proxy = await this.client.proxy(SOFTWARE_IFACE);
+ const proxy = await this.client.proxy(PRODUCT_IFACE);
return proxy.SelectProduct(id);
}
@@ -159,7 +161,7 @@ class SoftwareBaseClient {
* @param {(id: string) => void} handler - callback function
*/
onProductChange(handler) {
- return this.client.onObjectChanged(SOFTWARE_PATH, SOFTWARE_IFACE, changes => {
+ return this.client.onObjectChanged(PRODUCT_PATH, PRODUCT_IFACE, changes => {
if ("SelectedProduct" in changes) {
const selected = changes.SelectedProduct.v.toString();
handler(selected);
From 7b1cd3cc8c969f2bab4a01e684ec3797999fc743 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 20 Oct 2023 11:35:08 +0100
Subject: [PATCH 54/97] [service] Unit tests and minor fixes
---
service/lib/agama/dbus/clients/software.rb | 2 +-
service/lib/agama/dbus/software/product.rb | 8 +-
service/lib/agama/registration.rb | 21 +-
service/lib/agama/software/manager.rb | 3 -
.../test/agama/dbus/clients/software_test.rb | 34 +-
.../dbus/clients/with_issues_examples.rb | 61 ++-
.../test/agama/dbus/software/manager_test.rb | 329 ---------------
.../test/agama/dbus/software/product_test.rb | 388 ++++++++++++++++++
service/test/agama/software/manager_test.rb | 143 ++++---
9 files changed, 560 insertions(+), 429 deletions(-)
create mode 100644 service/test/agama/dbus/software/product_test.rb
diff --git a/service/lib/agama/dbus/clients/software.rb b/service/lib/agama/dbus/clients/software.rb
index 8f94786773..9ff10a4fc6 100644
--- a/service/lib/agama/dbus/clients/software.rb
+++ b/service/lib/agama/dbus/clients/software.rb
@@ -172,7 +172,7 @@ def remove_resolvables(unique_id, type, resolvables, optional: false)
#
# @param block [Proc] Callback to run when a product is selected
def on_product_selected(&block)
- on_properties_change(dbus_object) do |_, changes, _|
+ on_properties_change(dbus_product) do |_, changes, _|
product = changes["SelectedProduct"]
block.call(product) unless product.nil?
end
diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb
index 2029d9c2a7..475107f75b 100644
--- a/service/lib/agama/dbus/software/product.rb
+++ b/service/lib/agama/dbus/software/product.rb
@@ -28,15 +28,13 @@
module Agama
module DBus
module Software
- # D-Bus object to manage product configuration
+ # D-Bus object to manage product configuration.
class Product < BaseObject
include Interfaces::Issues
PATH = "/org/opensuse/Agama/Software1/Product"
private_constant :PATH
- # Constructor
- #
# @param backend [Agama::Software]
# @param logger [Logger]
def initialize(backend, logger)
@@ -46,7 +44,7 @@ def initialize(backend, logger)
register_callbacks
end
- # List of issues, see {DBus::Interfaces::Issues}
+ # List of issues, see {DBus::Interfaces::Issues}.
#
# @return [Array]
def issues
@@ -59,7 +57,7 @@ def available_products
end
end
- # Returns the selected base product
+ # Returns the selected base product.
#
# @return [String] Product ID or an empty string if no product is selected
def selected_product
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index 7e1d9369b6..64c64bcecc 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -53,6 +53,16 @@ def initialize(software_manager, logger)
@logger = logger
end
+ # Registers the selected product.
+ #
+ # @raise [
+ # SocketError|Timeout::Error|SUSE::Connect::ApiError|
+ # SUSE::Connect::MissingSccCredentialsFile|SUSE::Connect::MissingSccCredentialsFile|
+ # OpenSSL::SSL::SSLError|JSON::ParserError
+ # ]
+ #
+ # @param code [String] Registration code.
+ # @param email [String] Email for registering the product.
def register(code, email: "")
return unless product
@@ -66,7 +76,6 @@ def register(code, email: "")
# TODO: check if we can do it in memory for libzypp
SUSE::Connect::YaST.create_credentials_file(login, password)
- # TODO: fill it properly for scc
target_product = OpenStruct.new(
arch: Yast::Arch.rpm_arch,
identifier: product.id,
@@ -87,6 +96,15 @@ def register(code, email: "")
run_on_change_callbacks
end
+ # Deregisters the selected product.
+ #
+ # It uses the registration code and email passed to {#register}.
+ #
+ # @raise [
+ # SocketError|Timeout::Error|SUSE::Connect::ApiError|
+ # SUSE::Connect::MissingSccCredentialsFile|SUSE::Connect::MissingSccCredentialsFile|
+ # OpenSSL::SSL::SSLError|JSON::ParserError
+ # ]
def deregister
Y2Packager::NewRepositorySetup.instance.services.delete(@service.name)
@software.remove_service(@service)
@@ -103,7 +121,6 @@ def deregister
@credentials_file = nil
end
- # reset variables here
@reg_code = nil
@email = nil
run_on_change_callbacks
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index e2d6167cea..6f819f99dc 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -90,9 +90,6 @@ def initialize(config, logger)
# patterns selected by user
@user_patterns = []
@selected_patterns_change_callbacks = []
-
- update_issues
-
on_progress_change { logger.info(progress.to_s) }
end
diff --git a/service/test/agama/dbus/clients/software_test.rb b/service/test/agama/dbus/clients/software_test.rb
index c050e71ce7..c379920f5e 100644
--- a/service/test/agama/dbus/clients/software_test.rb
+++ b/service/test/agama/dbus/clients/software_test.rb
@@ -34,27 +34,32 @@
allow(bus).to receive(:service).with("org.opensuse.Agama.Software1").and_return(service)
allow(service).to receive(:[]).with("/org/opensuse/Agama/Software1")
.and_return(dbus_object)
- allow(dbus_object).to receive(:introspect)
+ allow(service).to receive(:[]).with("/org/opensuse/Agama/Software1/Product")
+ .and_return(dbus_product)
+ allow(service).to receive(:[]).with("/org/opensuse/Agama/Software1/Proposal")
+ .and_return(dbus_proposal)
allow(dbus_object).to receive(:[]).with("org.opensuse.Agama.Software1")
.and_return(software_iface)
- allow(dbus_object).to receive(:[]).with("org.freedesktop.DBus.Properties")
+ allow(dbus_product).to receive(:[]).with("org.opensuse.Agama.Software1.Product")
+ .and_return(product_iface)
+ allow(dbus_product).to receive(:[]).with("org.freedesktop.DBus.Properties")
.and_return(properties_iface)
- allow(service).to receive(:[]).with("/org/opensuse/Agama/Software1/Proposal")
- .and_return(dbus_proposal)
end
let(:bus) { instance_double(Agama::DBus::Bus) }
let(:service) { instance_double(::DBus::ProxyService) }
- let(:dbus_object) { instance_double(::DBus::ProxyObject) }
+ let(:dbus_object) { instance_double(::DBus::ProxyObject, introspect: nil) }
+ let(:dbus_product) { instance_double(::DBus::ProxyObject, introspect: nil) }
let(:dbus_proposal) { instance_double(::DBus::ProxyObject, introspect: nil) }
let(:software_iface) { instance_double(::DBus::ProxyObjectInterface) }
let(:properties_iface) { instance_double(::DBus::ProxyObjectInterface) }
+ let(:product_iface) { instance_double(::DBus::ProxyObjectInterface) }
subject { described_class.new }
describe "#available_products" do
before do
- allow(software_iface).to receive(:[]).with("AvailableProducts").and_return(
+ allow(product_iface).to receive(:[]).with("AvailableProducts").and_return(
[
["Tumbleweed", "openSUSE Tumbleweed", {}],
["Leap15.3", "openSUSE Leap 15.3", {}]
@@ -72,7 +77,7 @@
describe "#selected_product" do
before do
- allow(software_iface).to receive(:[]).with("SelectedProduct").and_return(product)
+ allow(product_iface).to receive(:[]).with("SelectedProduct").and_return(product)
end
context "when there is no selected product" do
@@ -94,17 +99,17 @@
describe "#select_product" do
# Using partial double because methods are dynamically added to the proxy object
- let(:dbus_object) { double(::DBus::ProxyObject) }
+ let(:dbus_product) { double(::DBus::ProxyObject, introspect: nil) }
it "selects the given product" do
- expect(dbus_object).to receive(:SelectProduct).with("Tumbleweed")
+ expect(dbus_product).to receive(:SelectProduct).with("Tumbleweed")
subject.select_product("Tumbleweed")
end
end
describe "#probe" do
- let(:dbus_object) { double(::DBus::ProxyObject, Probe: nil) }
+ let(:dbus_object) { double(::DBus::ProxyObject, introspect: nil, Probe: nil) }
it "calls the D-Bus Probe method" do
expect(dbus_object).to receive(:Probe)
@@ -125,7 +130,7 @@
end
describe "#provisions_selected" do
- let(:dbus_object) { double(::DBus::ProxyObject) }
+ let(:dbus_object) { double(::DBus::ProxyObject, introspect: nil) }
it "returns true/false for every tag given" do
expect(dbus_object).to receive(:ProvisionsSelected)
@@ -136,7 +141,10 @@
end
describe "#package_installed?" do
- let(:dbus_object) { double(::DBus::ProxyObject, IsPackageInstalled: installed?) }
+ let(:dbus_object) do
+ double(::DBus::ProxyObject, introspect: nil, IsPackageInstalled: installed?)
+ end
+
let(:package) { "NetworkManager" }
context "when the package is installed" do
@@ -158,7 +166,7 @@
describe "#on_product_selected" do
before do
- allow(dbus_object).to receive(:path).and_return("/org/opensuse/Agama/Test")
+ allow(dbus_product).to receive(:path).and_return("/org/opensuse/Agama/Test")
allow(properties_iface).to receive(:on_signal)
end
diff --git a/service/test/agama/dbus/clients/with_issues_examples.rb b/service/test/agama/dbus/clients/with_issues_examples.rb
index e599db742f..d409970358 100644
--- a/service/test/agama/dbus/clients/with_issues_examples.rb
+++ b/service/test/agama/dbus/clients/with_issues_examples.rb
@@ -24,19 +24,54 @@
shared_examples "issues" do
before do
- allow(dbus_object).to receive(:path).and_return("/org/opensuse/Agama/Test")
- allow(dbus_object).to receive(:[]).with("org.opensuse.Agama1.Issues")
- .and_return(issues_properties)
+ allow(service).to receive(:root).and_return(root_node)
+
+ allow(dbus_object1).to receive(:[]).with("org.opensuse.Agama1.Issues")
+ .and_return(issues_interface1)
+
+ allow(dbus_object3).to receive(:[]).with("org.opensuse.Agama1.Issues")
+ .and_return(issues_interface3)
+
+ allow(issues_interface1).to receive(:[]).with("All").and_return(issues1)
+ allow(issues_interface3).to receive(:[]).with("All").and_return(issues3)
+ end
+
+ let(:root_node) do
+ instance_double(::DBus::Node, descendant_objects: [dbus_object1, dbus_object2, dbus_object3])
+ end
+
+ let(:dbus_object1) do
+ instance_double(::DBus::ProxyObject,
+ interfaces: ["org.opensuse.Agama1.Test", "org.opensuse.Agama1.Issues"])
+ end
+
+ let(:dbus_object2) do
+ instance_double(::DBus::ProxyObject, interfaces: ["org.opensuse.Agama1.Test"])
+ end
+
+ let(:dbus_object3) do
+ instance_double(::DBus::ProxyObject, interfaces: ["org.opensuse.Agama1.Issues"])
end
- let(:issues_properties) { { "All" => issues } }
+ let(:issues_interface1) { instance_double(::DBus::ProxyObjectInterface) }
+
+ let(:issues_interface3) { instance_double(::DBus::ProxyObjectInterface) }
- let(:issues) { [issue1, issue2] }
- let(:issue1) { ["Issue 1", "Details 1", 1, 0] }
- let(:issue2) { ["Issue 2", "Details 2", 2, 1] }
+ let(:issues1) do
+ [
+ ["Issue 1", "Details 1", 1, 0],
+ ["Issue 2", "Details 2", 2, 1]
+ ]
+ end
+
+ let(:issues3) do
+ [
+ ["Issue 3", "Details 3", 1, 0]
+ ]
+ end
describe "#issues" do
- it "returns the list of issues" do
+ it "returns the list of issues from all objects" do
expect(subject.issues).to all(be_a(Agama::Issue))
expect(subject.issues).to contain_exactly(
@@ -51,6 +86,12 @@
details: "Details 2",
source: Agama::Issue::Source::CONFIG,
severity: Agama::Issue::Severity::ERROR
+ ),
+ an_object_having_attributes(
+ description: "Issue 3",
+ details: "Details 3",
+ source: Agama::Issue::Source::SYSTEM,
+ severity: Agama::Issue::Severity::WARN
)
)
end
@@ -58,15 +99,13 @@
describe "#errors?" do
context "if there is any error" do
- let(:issues) { [issue2] }
-
it "returns true" do
expect(subject.errors?).to eq(true)
end
end
context "if there is no error" do
- let(:issues) { [issue1] }
+ let(:issues1) { [] }
it "returns false" do
expect(subject.errors?).to eq(false)
diff --git a/service/test/agama/dbus/software/manager_test.rb b/service/test/agama/dbus/software/manager_test.rb
index a8f6d04e8d..edc0ae1946 100644
--- a/service/test/agama/dbus/software/manager_test.rb
+++ b/service/test/agama/dbus/software/manager_test.rb
@@ -87,41 +87,6 @@
backend.issues = []
end
- describe "select_product" do
- context "if the product is correctly selected" do
- it "returns result code 0 with empty description" do
- expect(subject.select_product("Tumbleweed")).to contain_exactly(0, "")
- end
- end
-
- context "if the given product is already selected" do
- before do
- subject.select_product("Tumbleweed")
- end
-
- it "returns result code 1 and description" do
- expect(subject.select_product("Tumbleweed")).to contain_exactly(1, /already selected/)
- end
- end
-
- context "if the current product is registered" do
- before do
- subject.select_product("Leap")
- allow(backend.registration).to receive(:reg_code).and_return("123XX432")
- end
-
- it "returns result code 2 and description" do
- expect(subject.select_product("Tumbleweed")).to contain_exactly(2, /must be deregistered/)
- end
- end
-
- context "if the product is unknown" do
- it "returns result code 3 and description" do
- expect(subject.select_product("Unknown")).to contain_exactly(3, /unknown product/i)
- end
- end
- end
-
describe "#probe" do
it "runs the probing, setting the service as busy meanwhile" do
expect(subject.service_status).to receive(:busy)
@@ -171,298 +136,4 @@
expect(installed).to eq(true)
end
end
-
- describe "#reg_code" do
- before do
- allow(backend.registration).to receive(:reg_code).and_return(reg_code)
- end
-
- context "if there is no registered product yet" do
- let(:reg_code) { nil }
-
- it "returns an empty string" do
- expect(subject.reg_code).to eq("")
- end
- end
-
- context "if there is a registered product" do
- let(:reg_code) { "123XX432" }
-
- it "returns the registration code" do
- expect(subject.reg_code).to eq("123XX432")
- end
- end
- end
-
- describe "#email" do
- before do
- allow(backend.registration).to receive(:email).and_return(email)
- end
-
- context "if there is no registered email" do
- let(:email) { nil }
-
- it "returns an empty string" do
- expect(subject.email).to eq("")
- end
- end
-
- context "if there is a registered email" do
- let(:email) { "test@suse.com" }
-
- it "returns the registered email" do
- expect(subject.email).to eq("test@suse.com")
- end
- end
- end
-
- describe "#requirement" do
- before do
- allow(backend.registration).to receive(:requirement).and_return(requirement)
- end
-
- context "if the registration is not required" do
- let(:requirement) { Agama::Registration::Requirement::NOT_REQUIRED }
-
- it "returns 0" do
- expect(subject.requirement).to eq(0)
- end
- end
-
- context "if the registration is optional" do
- let(:requirement) { Agama::Registration::Requirement::OPTIONAL }
-
- it "returns 1" do
- expect(subject.requirement).to eq(1)
- end
- end
-
- context "if the registration is mandatory" do
- let(:requirement) { Agama::Registration::Requirement::MANDATORY }
-
- it "returns 2" do
- expect(subject.requirement).to eq(2)
- end
- end
- end
-
- describe "#register" do
- before do
- allow(backend.registration).to receive(:reg_code).and_return(nil)
- end
-
- context "if there is no product selected yet" do
- it "returns result code 1 and description" do
- expect(subject.register("123XX432")).to contain_exactly(1, /product not selected/i)
- end
- end
-
- context "if there is a selected product" do
- before do
- backend.select_product("Tumbleweed")
- end
-
- context "if the product is already registered" do
- before do
- allow(backend.registration).to receive(:reg_code).and_return("123XX432")
- end
-
- it "returns result code 2 and description" do
- expect(subject.register("123XX432")).to contain_exactly(2, /product already registered/i)
- end
- end
-
- context "if there is a network error" do
- before do
- allow(backend.registration).to receive(:register).and_raise(SocketError)
- end
-
- it "returns result code 3 and description" do
- expect(subject.register("123XX432")).to contain_exactly(3, /network error/)
- end
- end
-
- context "if there is a timeout" do
- before do
- allow(backend.registration).to receive(:register).and_raise(Timeout::Error)
- end
-
- it "returns result code 4 and description" do
- expect(subject.register("123XX432")).to contain_exactly(4, /timeout/)
- end
- end
-
- context "if there is an API error" do
- before do
- allow(backend.registration).to receive(:register).and_raise(SUSE::Connect::ApiError, "")
- end
-
- it "returns result code 5 and description" do
- expect(subject.register("123XX432")).to contain_exactly(5, /registration server failed/)
- end
- end
-
- context "if there is a missing credials error" do
- before do
- allow(backend.registration)
- .to receive(:register).and_raise(SUSE::Connect::MissingSccCredentialsFile)
- end
-
- it "returns result code 6 and description" do
- expect(subject.register("123XX432")).to contain_exactly(6, /missing credentials/)
- end
- end
-
- context "if there is an incorrect credials error" do
- before do
- allow(backend.registration)
- .to receive(:register).and_raise(SUSE::Connect::MalformedSccCredentialsFile)
- end
-
- it "returns result code 7 and description" do
- expect(subject.register("123XX432")).to contain_exactly(7, /incorrect credentials/)
- end
- end
-
- context "if there is an invalid certificate error" do
- before do
- allow(backend.registration).to receive(:register).and_raise(OpenSSL::SSL::SSLError)
- end
-
- it "returns result code 8 and description" do
- expect(subject.register("123XX432")).to contain_exactly(8, /invalid certificate/)
- end
- end
-
- context "if there is an internal error" do
- before do
- allow(backend.registration).to receive(:register).and_raise(JSON::ParserError)
- end
-
- it "returns result code 9 and description" do
- expect(subject.register("123XX432")).to contain_exactly(9, /registration server failed/)
- end
- end
-
- context "if the registration is correctly done" do
- before do
- allow(backend.registration).to receive(:register)
- end
-
- it "returns result code 0 with empty description" do
- expect(subject.register("123XX432")).to contain_exactly(0, "")
- end
- end
- end
- end
-
- describe "#deregister" do
- before do
- allow(backend.registration).to receive(:reg_code).and_return("123XX432")
- end
-
- context "if there is no product selected yet" do
- it "returns result code 1 and description" do
- expect(subject.deregister).to contain_exactly(1, /product not selected/i)
- end
- end
-
- context "if there is a selected product" do
- before do
- backend.select_product("Tumbleweed")
- end
-
- context "if the product is not registered yet" do
- before do
- allow(backend.registration).to receive(:reg_code).and_return(nil)
- end
-
- it "returns result code 2 and description" do
- expect(subject.deregister).to contain_exactly(2, /product not registered/i)
- end
- end
-
- context "if there is a network error" do
- before do
- allow(backend.registration).to receive(:deregister).and_raise(SocketError)
- end
-
- it "returns result code 3 and description" do
- expect(subject.deregister).to contain_exactly(3, /network error/)
- end
- end
-
- context "if there is a timeout" do
- before do
- allow(backend.registration).to receive(:deregister).and_raise(Timeout::Error)
- end
-
- it "returns result code 4 and description" do
- expect(subject.deregister).to contain_exactly(4, /timeout/)
- end
- end
-
- context "if there is an API error" do
- before do
- allow(backend.registration).to receive(:deregister).and_raise(SUSE::Connect::ApiError, "")
- end
-
- it "returns result code 5 and description" do
- expect(subject.deregister).to contain_exactly(5, /registration server failed/)
- end
- end
-
- context "if there is a missing credials error" do
- before do
- allow(backend.registration)
- .to receive(:deregister).and_raise(SUSE::Connect::MissingSccCredentialsFile)
- end
-
- it "returns result code 6 and description" do
- expect(subject.deregister).to contain_exactly(6, /missing credentials/)
- end
- end
-
- context "if there is an incorrect credials error" do
- before do
- allow(backend.registration)
- .to receive(:deregister).and_raise(SUSE::Connect::MalformedSccCredentialsFile)
- end
-
- it "returns result code 7 and description" do
- expect(subject.deregister).to contain_exactly(7, /incorrect credentials/)
- end
- end
-
- context "if there is an invalid certificate error" do
- before do
- allow(backend.registration).to receive(:deregister).and_raise(OpenSSL::SSL::SSLError)
- end
-
- it "returns result code 8 and description" do
- expect(subject.deregister).to contain_exactly(8, /invalid certificate/)
- end
- end
-
- context "if there is an internal error" do
- before do
- allow(backend.registration).to receive(:deregister).and_raise(JSON::ParserError)
- end
-
- it "returns result code 9 and description" do
- expect(subject.deregister).to contain_exactly(9, /registration server failed/)
- end
- end
-
- context "if the deregistration is correctly done" do
- before do
- allow(backend.registration).to receive(:deregister)
- end
-
- it "returns result code 0 with empty description" do
- expect(subject.deregister).to contain_exactly(0, "")
- end
- end
- end
- end
end
diff --git a/service/test/agama/dbus/software/product_test.rb b/service/test/agama/dbus/software/product_test.rb
new file mode 100644
index 0000000000..475b2b2cc0
--- /dev/null
+++ b/service/test/agama/dbus/software/product_test.rb
@@ -0,0 +1,388 @@
+# frozen_string_literal: true
+
+# Copyright (c) [2023] SUSE LLC
+#
+# All Rights Reserved.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of version 2 of the GNU General Public License as published
+# by the Free Software Foundation.
+#
+# 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.
+
+require_relative "../../../test_helper"
+require "agama/dbus/software/product"
+require "agama/config"
+require "agama/registration"
+require "agama/software/manager"
+require "suse/connect"
+
+describe Agama::DBus::Software::Product do
+ subject { described_class.new(backend, logger) }
+
+ let(:logger) { Logger.new($stdout, level: :warn) }
+
+ let(:backend) { Agama::Software::Manager.new(config, logger) }
+
+ let(:config) do
+ Agama::Config.new(YAML.safe_load(File.read(config_path)))
+ end
+
+ let(:config_path) do
+ File.join(FIXTURES_PATH, "root_dir", "etc", "agama.yaml")
+ end
+
+ before do
+ allow(subject).to receive(:dbus_properties_changed)
+ end
+
+ it "defines Product D-Bus interface" do
+ expect(subject.intfs.keys).to include("org.opensuse.Agama.Software1.Product")
+ end
+
+ it "defines Registration D-Bus interface" do
+ expect(subject.intfs.keys).to include("org.opensuse.Agama1.Registration")
+ end
+
+ it "defines Issues D-Bus interface" do
+ expect(subject.intfs.keys).to include("org.opensuse.Agama1.Issues")
+ end
+
+ describe "select_product" do
+ context "if the product is correctly selected" do
+ it "returns result code 0 with empty description" do
+ expect(subject.select_product("Tumbleweed")).to contain_exactly(0, "")
+ end
+ end
+
+ context "if the given product is already selected" do
+ before do
+ subject.select_product("Tumbleweed")
+ end
+
+ it "returns result code 1 and description" do
+ expect(subject.select_product("Tumbleweed")).to contain_exactly(1, /already selected/)
+ end
+ end
+
+ context "if the current product is registered" do
+ before do
+ subject.select_product("Leap")
+ allow(backend.registration).to receive(:reg_code).and_return("123XX432")
+ end
+
+ it "returns result code 2 and description" do
+ expect(subject.select_product("Tumbleweed")).to contain_exactly(2, /must be deregistered/)
+ end
+ end
+
+ context "if the product is unknown" do
+ it "returns result code 3 and description" do
+ expect(subject.select_product("Unknown")).to contain_exactly(3, /unknown product/i)
+ end
+ end
+ end
+
+ describe "#reg_code" do
+ before do
+ allow(backend.registration).to receive(:reg_code).and_return(reg_code)
+ end
+
+ context "if there is no registered product yet" do
+ let(:reg_code) { nil }
+
+ it "returns an empty string" do
+ expect(subject.reg_code).to eq("")
+ end
+ end
+
+ context "if there is a registered product" do
+ let(:reg_code) { "123XX432" }
+
+ it "returns the registration code" do
+ expect(subject.reg_code).to eq("123XX432")
+ end
+ end
+ end
+
+ describe "#email" do
+ before do
+ allow(backend.registration).to receive(:email).and_return(email)
+ end
+
+ context "if there is no registered email" do
+ let(:email) { nil }
+
+ it "returns an empty string" do
+ expect(subject.email).to eq("")
+ end
+ end
+
+ context "if there is a registered email" do
+ let(:email) { "test@suse.com" }
+
+ it "returns the registered email" do
+ expect(subject.email).to eq("test@suse.com")
+ end
+ end
+ end
+
+ describe "#requirement" do
+ before do
+ allow(backend.registration).to receive(:requirement).and_return(requirement)
+ end
+
+ context "if the registration is not required" do
+ let(:requirement) { Agama::Registration::Requirement::NOT_REQUIRED }
+
+ it "returns 0" do
+ expect(subject.requirement).to eq(0)
+ end
+ end
+
+ context "if the registration is optional" do
+ let(:requirement) { Agama::Registration::Requirement::OPTIONAL }
+
+ it "returns 1" do
+ expect(subject.requirement).to eq(1)
+ end
+ end
+
+ context "if the registration is mandatory" do
+ let(:requirement) { Agama::Registration::Requirement::MANDATORY }
+
+ it "returns 2" do
+ expect(subject.requirement).to eq(2)
+ end
+ end
+ end
+
+ describe "#register" do
+ before do
+ allow(backend.registration).to receive(:reg_code).and_return(nil)
+ end
+
+ context "if there is no product selected yet" do
+ it "returns result code 1 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(1, /product not selected/i)
+ end
+ end
+
+ context "if there is a selected product" do
+ before do
+ backend.select_product("Tumbleweed")
+ end
+
+ context "if the product is already registered" do
+ before do
+ allow(backend.registration).to receive(:reg_code).and_return("123XX432")
+ end
+
+ it "returns result code 2 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(2, /product already registered/i)
+ end
+ end
+
+ context "if there is a network error" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(SocketError)
+ end
+
+ it "returns result code 3 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(3, /network error/)
+ end
+ end
+
+ context "if there is a timeout" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(Timeout::Error)
+ end
+
+ it "returns result code 4 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(4, /timeout/)
+ end
+ end
+
+ context "if there is an API error" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(SUSE::Connect::ApiError, "")
+ end
+
+ it "returns result code 5 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(5, /registration server failed/)
+ end
+ end
+
+ context "if there is a missing credials error" do
+ before do
+ allow(backend.registration)
+ .to receive(:register).and_raise(SUSE::Connect::MissingSccCredentialsFile)
+ end
+
+ it "returns result code 6 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(6, /missing credentials/)
+ end
+ end
+
+ context "if there is an incorrect credials error" do
+ before do
+ allow(backend.registration)
+ .to receive(:register).and_raise(SUSE::Connect::MalformedSccCredentialsFile)
+ end
+
+ it "returns result code 7 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(7, /incorrect credentials/)
+ end
+ end
+
+ context "if there is an invalid certificate error" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(OpenSSL::SSL::SSLError)
+ end
+
+ it "returns result code 8 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(8, /invalid certificate/)
+ end
+ end
+
+ context "if there is an internal error" do
+ before do
+ allow(backend.registration).to receive(:register).and_raise(JSON::ParserError)
+ end
+
+ it "returns result code 9 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(9, /registration server failed/)
+ end
+ end
+
+ context "if the registration is correctly done" do
+ before do
+ allow(backend.registration).to receive(:register)
+ end
+
+ it "returns result code 0 with empty description" do
+ expect(subject.register("123XX432")).to contain_exactly(0, "")
+ end
+ end
+ end
+ end
+
+ describe "#deregister" do
+ before do
+ allow(backend.registration).to receive(:reg_code).and_return("123XX432")
+ end
+
+ context "if there is no product selected yet" do
+ it "returns result code 1 and description" do
+ expect(subject.deregister).to contain_exactly(1, /product not selected/i)
+ end
+ end
+
+ context "if there is a selected product" do
+ before do
+ backend.select_product("Tumbleweed")
+ end
+
+ context "if the product is not registered yet" do
+ before do
+ allow(backend.registration).to receive(:reg_code).and_return(nil)
+ end
+
+ it "returns result code 2 and description" do
+ expect(subject.deregister).to contain_exactly(2, /product not registered/i)
+ end
+ end
+
+ context "if there is a network error" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(SocketError)
+ end
+
+ it "returns result code 3 and description" do
+ expect(subject.deregister).to contain_exactly(3, /network error/)
+ end
+ end
+
+ context "if there is a timeout" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(Timeout::Error)
+ end
+
+ it "returns result code 4 and description" do
+ expect(subject.deregister).to contain_exactly(4, /timeout/)
+ end
+ end
+
+ context "if there is an API error" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(SUSE::Connect::ApiError, "")
+ end
+
+ it "returns result code 5 and description" do
+ expect(subject.deregister).to contain_exactly(5, /registration server failed/)
+ end
+ end
+
+ context "if there is a missing credials error" do
+ before do
+ allow(backend.registration)
+ .to receive(:deregister).and_raise(SUSE::Connect::MissingSccCredentialsFile)
+ end
+
+ it "returns result code 6 and description" do
+ expect(subject.deregister).to contain_exactly(6, /missing credentials/)
+ end
+ end
+
+ context "if there is an incorrect credials error" do
+ before do
+ allow(backend.registration)
+ .to receive(:deregister).and_raise(SUSE::Connect::MalformedSccCredentialsFile)
+ end
+
+ it "returns result code 7 and description" do
+ expect(subject.deregister).to contain_exactly(7, /incorrect credentials/)
+ end
+ end
+
+ context "if there is an invalid certificate error" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(OpenSSL::SSL::SSLError)
+ end
+
+ it "returns result code 8 and description" do
+ expect(subject.deregister).to contain_exactly(8, /invalid certificate/)
+ end
+ end
+
+ context "if there is an internal error" do
+ before do
+ allow(backend.registration).to receive(:deregister).and_raise(JSON::ParserError)
+ end
+
+ it "returns result code 9 and description" do
+ expect(subject.deregister).to contain_exactly(9, /registration server failed/)
+ end
+ end
+
+ context "if the deregistration is correctly done" do
+ before do
+ allow(backend.registration).to receive(:deregister)
+ end
+
+ it "returns result code 0 with empty description" do
+ expect(subject.deregister).to contain_exactly(0, "")
+ end
+ end
+ end
+ end
+end
diff --git a/service/test/agama/software/manager_test.rb b/service/test/agama/software/manager_test.rb
index 984d0d89b3..73b05d3031 100644
--- a/service/test/agama/software/manager_test.rb
+++ b/service/test/agama/software/manager_test.rb
@@ -111,50 +111,18 @@
expect(manager.product).to be_nil
end
-
- it "adds a not selected product issue" do
- manager = described_class.new(config, logger)
-
- expect(manager.issues).to contain_exactly(an_object_having_attributes(
- description: /product not selected/i
- ))
- end
end
context "if there is only a product" do
let(:products) { [product] }
- let(:product) do
- Agama::Software::Product.new("test1").tap { |p| p.repositories = product_repositories }
- end
-
- let(:product_repositories) { [] }
+ let(:product) { Agama::Software::Product.new("test1") }
it "selects the product" do
manager = described_class.new(config, logger)
expect(manager.product.id).to eq("test1")
end
-
- context "if the product requires registration" do
- let(:product_repositories) { [] }
-
- it "adds a registration issue" do
- manager = described_class.new(config, logger)
-
- expect(manager.issues).to include(an_object_having_attributes(
- description: /product must be registered/i
- ))
- end
-
- context "if the product does not require registration" do
- let(:product_repositories) { ["https://test"] }
-
- it "does not add issues" do
- expect(subject.issues).to be_empty
- end
- end
- end
end
end
@@ -212,38 +180,6 @@
))
end
end
-
- context "if the product is not registered" do
- let(:reg_code) { nil }
-
- before do
- allow(subject.registration).to receive(:requirement).and_return(reg_requirement)
- end
-
- context "and registration is mandatory" do
- let(:reg_requirement) { Agama::Registration::Requirement::MANDATORY }
-
- it "adds registration issue" do
- subject.public_send(tested_method)
-
- expect(subject.issues).to include(an_object_having_attributes(
- description: /product must be registered/i
- ))
- end
- end
-
- context "and registration is not mandatory" do
- let(:reg_requirement) { Agama::Registration::Requirement::OPTIONAL }
-
- it "does not add registration issue" do
- subject.public_send(tested_method)
-
- expect(subject.issues).to_not include(an_object_having_attributes(
- description: /product must be registered/i
- ))
- end
- end
- end
end
describe "#probe" do
@@ -399,6 +335,83 @@
end
end
+ describe "#product_issues" do
+ before do
+ allow_any_instance_of(Agama::Software::ProductBuilder)
+ .to receive(:build).and_return([product1, product2])
+ end
+
+ let(:product1) do
+ Agama::Software::Product.new("test1").tap { |p| p.repositories = [] }
+ end
+
+ let(:product2) do
+ Agama::Software::Product.new("test2").tap { |p| p.repositories = ["http://test"] }
+ end
+
+ context "if no product is selected yet" do
+ it "contains a missing product issue" do
+ expect(subject.product_issues).to contain_exactly(
+ an_object_having_attributes(
+ description: /product not selected/i
+ )
+ )
+ end
+ end
+
+ context "if a product is already selected" do
+ before do
+ subject.select_product(product_id)
+ end
+
+ let(:product_id) { "test1" }
+
+ it "does not include a missing product issue" do
+ expect(subject.product_issues).to_not include(
+ an_object_having_attributes(
+ description: /product not selected/i
+ )
+ )
+ end
+
+ context "and the product does not require registration" do
+ let(:product_id) { "test2" }
+
+ it "does not contain issues" do
+ expect(subject.product_issues).to be_empty
+ end
+ end
+
+ context "and the product requires registration" do
+ let(:product_id) { "test1" }
+
+ before do
+ allow(subject.registration).to receive(:reg_code).and_return(reg_code)
+ end
+
+ context "and the product is not registered" do
+ let(:reg_code) { nil }
+
+ it "contains a missing registration issue" do
+ expect(subject.product_issues).to contain_exactly(
+ an_object_having_attributes(
+ description: /product must be registered/i
+ )
+ )
+ end
+ end
+
+ context "and the product is registered" do
+ let(:reg_code) { "1234XX5678" }
+
+ it "does not contain issues" do
+ expect(subject.product_issues).to be_empty
+ end
+ end
+ end
+ end
+ end
+
include_examples "issues"
include_examples "progress"
end
From 5c3809a803402d17a7189b151de5f1fec96e121a Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Fri, 20 Oct 2023 14:04:25 +0200
Subject: [PATCH 55/97] copy credentials in installation
---
service/lib/agama/registration.rb | 10 ++++++++++
service/lib/agama/software/manager.rb | 1 +
2 files changed, 11 insertions(+)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index 64c64bcecc..e19ed98e78 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -126,6 +126,16 @@ def deregister
run_on_change_callbacks
end
+ def finish
+ return unless reg_code
+
+ files = [@credentials_file, SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE]
+ files.each do |file|
+ dest = File.join(Yast::Installation.destdir, file)
+ FileUtils::cp(file, dest)
+ end
+ end
+
# Indicates whether the registration is optional, mandatory or not required.
#
# @return [Symbol] See {Requirement}.
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index 6f819f99dc..94bb65beb9 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -183,6 +183,7 @@ def finish
Yast::Pkg.SourceSaveAll
Yast::Pkg.TargetFinish
Yast::Pkg.SourceCacheCopyTo(Yast::Installation.destdir)
+ registration.finish
end
progress.step("Restoring original repositories") { restore_original_repos }
end
From 0b21ca7e5f70a47f4eb6c5a5507b3a2b4b5338a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 20 Oct 2023 15:10:44 +0100
Subject: [PATCH 56/97] [service] Registration tests and minor fixes
---
service/lib/agama/registration.rb | 6 +-
.../test/agama/dbus/software/manager_test.rb | 9 +-
.../test/agama/dbus/software/product_test.rb | 9 +-
service/test/agama/registration_test.rb | 375 ++++++++++++++++++
service/test/agama/software/manager_test.rb | 4 +-
5 files changed, 388 insertions(+), 15 deletions(-)
create mode 100644 service/test/agama/registration_test.rb
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index e19ed98e78..093d95db13 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -64,7 +64,7 @@ def initialize(software_manager, logger)
# @param code [String] Registration code.
# @param email [String] Email for registering the product.
def register(code, email: "")
- return unless product
+ return if product.nil? || reg_code
connect_params = {
token: code,
@@ -79,7 +79,7 @@ def register(code, email: "")
target_product = OpenStruct.new(
arch: Yast::Arch.rpm_arch,
identifier: product.id,
- version: product.version
+ version: product.version || "1.0"
)
activate_params = {}
@service = SUSE::Connect::YaST.activate_product(target_product, activate_params, email)
@@ -106,6 +106,8 @@ def register(code, email: "")
# OpenSSL::SSL::SSLError|JSON::ParserError
# ]
def deregister
+ return unless product && reg_code
+
Y2Packager::NewRepositorySetup.instance.services.delete(@service.name)
@software.remove_service(@service)
diff --git a/service/test/agama/dbus/software/manager_test.rb b/service/test/agama/dbus/software/manager_test.rb
index edc0ae1946..925218b9d5 100644
--- a/service/test/agama/dbus/software/manager_test.rb
+++ b/service/test/agama/dbus/software/manager_test.rb
@@ -36,12 +36,11 @@
let(:backend) { Agama::Software::Manager.new(config, logger) }
- let(:config) do
- Agama::Config.new(YAML.safe_load(File.read(config_path)))
- end
+ let(:config) { Agama::Config.new(config_data) }
- let(:config_path) do
- File.join(FIXTURES_PATH, "root_dir", "etc", "agama.yaml")
+ let(:config_data) do
+ path = File.join(FIXTURES_PATH, "root_dir/etc/agama.yaml")
+ YAML.safe_load(File.read(path))
end
let(:progress_interface) { Agama::DBus::Interfaces::Progress::PROGRESS_INTERFACE }
diff --git a/service/test/agama/dbus/software/product_test.rb b/service/test/agama/dbus/software/product_test.rb
index 475b2b2cc0..b7b89a875d 100644
--- a/service/test/agama/dbus/software/product_test.rb
+++ b/service/test/agama/dbus/software/product_test.rb
@@ -33,12 +33,11 @@
let(:backend) { Agama::Software::Manager.new(config, logger) }
- let(:config) do
- Agama::Config.new(YAML.safe_load(File.read(config_path)))
- end
+ let(:config) { Agama::Config.new(config_data) }
- let(:config_path) do
- File.join(FIXTURES_PATH, "root_dir", "etc", "agama.yaml")
+ let(:config_data) do
+ path = File.join(FIXTURES_PATH, "root_dir/etc/agama.yaml")
+ YAML.safe_load(File.read(path))
end
before do
diff --git a/service/test/agama/registration_test.rb b/service/test/agama/registration_test.rb
new file mode 100644
index 0000000000..a2dd6731b8
--- /dev/null
+++ b/service/test/agama/registration_test.rb
@@ -0,0 +1,375 @@
+# frozen_string_literal: true
+
+# Copyright (c) [2023] SUSE LLC
+#
+# All Rights Reserved.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of version 2 of the GNU General Public License as published
+# by the Free Software Foundation.
+#
+# 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.
+
+require_relative "../test_helper"
+require "agama/config"
+require "agama/registration"
+require "agama/software/manager"
+require "suse/connect"
+require "yast"
+require "y2packager/new_repository_setup"
+
+Yast.import("Arch")
+
+describe Agama::Registration do
+ subject { described_class.new(manager, logger) }
+
+ let(:manager) { instance_double(Agama::Software::Manager) }
+
+ let(:logger) { Logger.new($stdout, level: :warn) }
+
+ before do
+ allow(Yast::Arch).to receive(:rpm_arch).and_return("x86_64")
+
+ allow(manager).to receive(:product).and_return(product)
+ allow(manager).to receive(:add_service)
+ allow(manager).to receive(:remove_service)
+
+ allow(SUSE::Connect::YaST).to receive(:announce_system).and_return(["test-user", "12345"])
+ allow(SUSE::Connect::YaST).to receive(:deactivate_system)
+ allow(SUSE::Connect::YaST).to receive(:create_credentials_file)
+ allow(SUSE::Connect::YaST).to receive(:activate_product).and_return(service)
+ allow(Y2Packager::NewRepositorySetup.instance).to receive(:add_service)
+ end
+
+ let(:service) { OpenStruct.new(name: "test-service", url: nil) }
+
+ describe "#register" do
+ context "if there is no product selected yet" do
+ let(:product) { nil }
+
+ it "does not try to register" do
+ expect(SUSE::Connect::YaST).to_not receive(:announce_system)
+
+ subject.register("11112222", email: "test@test.com")
+ end
+ end
+
+ context "if there is a selected product" do
+ let(:product) { Agama::Software::Product.new("test").tap { |p| p.version = "5.0" } }
+
+ context "and the product is already registered" do
+ before do
+ subject.register("11112222", email: "test@test.com")
+ end
+
+ it "does not try to register" do
+ expect(SUSE::Connect::YaST).to_not receive(:announce_system)
+
+ subject.register("11112222", email: "test@test.com")
+ end
+ end
+
+ context "and the product is not registered yet" do
+ it "announces the system" do
+ expect(SUSE::Connect::YaST).to receive(:announce_system).with(
+ { token: "11112222", email: "test@test.com" },
+ "test-5-x86_64"
+ )
+
+ subject.register("11112222", email: "test@test.com")
+ end
+
+ it "creates credentials file" do
+ expect(SUSE::Connect::YaST).to receive(:create_credentials_file)
+ .with("test-user", "12345")
+
+ subject.register("11112222", email: "test@test.com")
+ end
+
+ it "activates the selected product" do
+ expect(SUSE::Connect::YaST).to receive(:activate_product).with(
+ an_object_having_attributes(
+ arch: "x86_64", identifier: "test", version: "5.0"
+ ), {}, "test@test.com"
+ )
+
+ subject.register("11112222", email: "test@test.com")
+ end
+
+ it "adds the service to software manager" do
+ expect(Y2Packager::NewRepositorySetup.instance)
+ .to receive(:add_service).with("test-service")
+
+ subject.register("11112222", email: "test@test.com")
+ end
+
+ context "if the service requires a creadentials file" do
+ let(:service) { OpenStruct.new(name: "test-service", url: "https://credentials/file") }
+
+ before do
+ allow(subject).to receive(:credentials_from_url)
+ .with("https://credentials/file").and_return("credentials")
+ 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")
+
+ subject.register("11112222", email: "test@test.com")
+ end
+ end
+
+ context "if the service does not require a creadentials file" do
+ let(:service) { OpenStruct.new(name: "test-service", url: nil) }
+
+ it "does not create the credentials file" do
+ expect(SUSE::Connect::YaST).to receive(:create_credentials_file)
+ expect(SUSE::Connect::YaST).to_not receive(:create_credentials_file)
+ .with("test-user", "12345", anything)
+
+ subject.register("11112222", email: "test@test.com")
+ end
+ end
+
+ context "if the product was correctly registered" do
+ before do
+ subject.on_change(&callback)
+ end
+
+ let(:callback) { proc {} }
+
+ it "runs the callbacks" do
+ expect(callback).to receive(:call)
+
+ subject.register("11112222", email: "test@test.com")
+ end
+
+ it "sets the registration code" do
+ subject.register("11112222", email: "test@test.com")
+
+ expect(subject.reg_code).to eq("11112222")
+ end
+
+ it "sets the email" do
+ subject.register("11112222", email: "test@test.com")
+
+ expect(subject.email).to eq("test@test.com")
+ end
+ end
+
+ context "if the product was not correctly registered" do
+ before do
+ allow(SUSE::Connect::YaST).to receive(:activate_product).and_raise(Timeout::Error)
+ subject.on_change(&callback)
+ end
+
+ let(:callback) { proc {} }
+
+ it "raises an error" do
+ expect { subject.register("11112222", email: "test@test.com") }
+ .to raise_error(Timeout::Error)
+ end
+
+ it "does not run the callbacks" do
+ expect(callback).to_not receive(:call)
+
+ expect { subject.register("11112222", email: "test@test.com") }
+ .to raise_error(Timeout::Error)
+ end
+
+ it "does not set the registration code" do
+ expect { subject.register("11112222", email: "test@test.com") }
+ .to raise_error(Timeout::Error)
+
+ expect(subject.reg_code).to be_nil
+ end
+
+ it "does not set the email" do
+ expect { subject.register("11112222", email: "test@test.com") }
+ .to raise_error(Timeout::Error)
+
+ expect(subject.email).to be_nil
+ end
+ end
+ end
+ end
+ end
+
+ describe "#deregister" do
+ before do
+ allow(FileUtils).to receive(:rm)
+ end
+
+ context "if there is no product selected yet" do
+ let(:product) { nil }
+
+ it "does not try to deregister" do
+ expect(SUSE::Connect::YaST).to_not receive(:deactivate_system)
+
+ subject.deregister
+ end
+ end
+
+ context "if there is a selected product" do
+ let(:product) { Agama::Software::Product.new("test").tap { |p| p.version = "5.0" } }
+
+ context "and the product is not registered yet" do
+ it "does not try to deregister" do
+ expect(SUSE::Connect::YaST).to_not receive(:deactivate_system)
+
+ subject.deregister
+ end
+ end
+
+ context "and the product is registered" do
+ before do
+ allow(subject).to receive(:credentials_from_url)
+ allow(subject).to receive(:credentials_from_url)
+ .with("https://credentials/file").and_return("credentials")
+
+ subject.register("11112222", email: "test@test.com")
+ end
+
+ it "deletes the service from the software config" do
+ expect(manager).to receive(:remove_service).with(service)
+
+ subject.deregister
+ end
+
+ it "deactivates the system" do
+ expect(SUSE::Connect::YaST).to receive(:deactivate_system).with(
+ { token: "11112222", email: "test@test.com" }
+ )
+
+ subject.deregister
+ end
+
+ it "removes the credentials file" do
+ expect(FileUtils).to receive(:rm).with(/SCCcredentials/)
+
+ subject.deregister
+ end
+
+ context "if the service has a credentials files" do
+ let(:service) { OpenStruct.new(name: "test-service", url: "https://credentials/file") }
+
+ it "removes the credentials file" do
+ expect(FileUtils).to receive(:rm)
+ expect(FileUtils).to receive(:rm).with(/\/credentials$/)
+
+ subject.deregister
+ end
+ end
+
+ context "if the product has no credentials file" do
+ let(:service) { OpenStruct.new(name: "test-service", url: nil) }
+
+ it "does not try to remove the credentials file" do
+ expect(FileUtils).to_not receive(:rm).with(/\/credentials$/)
+
+ subject.deregister
+ end
+ end
+
+ context "if the product was correctly deregistered" do
+ before do
+ subject.on_change(&callback)
+ end
+
+ let(:callback) { proc {} }
+
+ it "runs the callbacks" do
+ expect(callback).to receive(:call)
+
+ subject.deregister
+ end
+
+ it "removes the registration code" do
+ subject.deregister
+
+ expect(subject.reg_code).to be_nil
+ end
+
+ it "removes the email" do
+ subject.deregister
+
+ expect(subject.email).to be_nil
+ end
+ end
+
+ context "if the product was not correctly deregistered" do
+ before do
+ allow(SUSE::Connect::YaST).to receive(:deactivate_system).and_raise(Timeout::Error)
+ subject.on_change(&callback)
+ end
+
+ let(:callback) { proc {} }
+
+ it "raises an error" do
+ expect { subject.deregister }.to raise_error(Timeout::Error)
+ end
+
+ it "does not run the callbacks" do
+ expect(callback).to_not receive(:call)
+
+ expect { subject.deregister }.to raise_error(Timeout::Error)
+ end
+
+ it "does not remove the registration code" do
+ expect { subject.deregister }.to raise_error(Timeout::Error)
+
+ expect(subject.reg_code).to eq("11112222")
+ end
+
+ it "does not remove the email" do
+ expect { subject.deregister }.to raise_error(Timeout::Error)
+
+ expect(subject.email).to eq("test@test.com")
+ end
+ end
+ end
+ end
+ end
+
+ describe "#requirement" do
+ context "if there is not product selected yet" do
+ let(:product) { nil }
+
+ it "returns not required" do
+ expect(subject.requirement).to eq(Agama::Registration::Requirement::NOT_REQUIRED)
+ end
+ end
+
+ context "if there is a selected product" do
+ let(:product) do
+ Agama::Software::Product.new("test").tap { |p| p.repositories = repositories }
+ end
+
+ context "and the product has repositories" do
+ let(:repositories) { ["https://repo"] }
+
+ it "returns not required" do
+ expect(subject.requirement).to eq(Agama::Registration::Requirement::NOT_REQUIRED)
+ end
+ end
+
+ context "and the product has no repositories" do
+ let(:repositories) { [] }
+
+ it "returns mandatory" do
+ expect(subject.requirement).to eq(Agama::Registration::Requirement::MANDATORY)
+ end
+ end
+ end
+ end
+end
diff --git a/service/test/agama/software/manager_test.rb b/service/test/agama/software/manager_test.rb
index 73b05d3031..9760358c73 100644
--- a/service/test/agama/software/manager_test.rb
+++ b/service/test/agama/software/manager_test.rb
@@ -22,9 +22,7 @@
require_relative "../../test_helper"
require_relative "../with_issues_examples"
require_relative "../with_progress_examples"
-require_relative File.join(
- SRC_PATH, "agama", "dbus", "y2dir", "software", "modules", "PackageCallbacks.rb"
-)
+require_relative File.join(SRC_PATH, "agama/dbus/y2dir/software/modules/PackageCallbacks.rb")
require "agama/config"
require "agama/issue"
require "agama/registration"
From 08144604cba2798489bb3eab880858a85bb4233f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 20 Oct 2023 15:29:22 +0100
Subject: [PATCH 57/97] [service] Add missing check
---
service/lib/agama/dbus/software/product.rb | 19 +++++----
service/lib/agama/registration.rb | 5 ++-
.../test/agama/dbus/software/product_test.rb | 40 ++++++++++++-------
3 files changed, 40 insertions(+), 24 deletions(-)
diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb
index 475107f75b..83a4c4a703 100644
--- a/service/lib/agama/dbus/software/product.rb
+++ b/service/lib/agama/dbus/software/product.rb
@@ -150,20 +150,23 @@ def requirement
# 0: success
# 1: missing product
# 2: already registered
- # 3: network error
- # 4: timeout error
- # 5: api error
- # 6: missing credentials
- # 7: incorrect credentials
- # 8: invalid certificate
- # 9: internal error (e.g., parsing json data)
+ # 3: registration not required
+ # 4: network error
+ # 5: timeout error
+ # 6: api error
+ # 7: missing credentials
+ # 8: incorrect credentials
+ # 9: invalid certificate
+ # 10: internal error (e.g., parsing json data)
def register(reg_code, email: nil)
if !backend.product
[1, "Product not selected yet"]
elsif backend.registration.reg_code
[2, "Product already registered"]
+ elsif backend.registration.requirement == Agama::Registration::Requirement::NOT_REQUIRED
+ [3, "Product does not require registration"]
else
- connect_result(first_error_code: 3) do
+ connect_result(first_error_code: 4) do
backend.registration.register(reg_code, email: email)
end
end
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index 093d95db13..56944cedbf 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -106,7 +106,7 @@ def register(code, email: "")
# OpenSSL::SSL::SSLError|JSON::ParserError
# ]
def deregister
- return unless product && reg_code
+ return unless reg_code
Y2Packager::NewRepositorySetup.instance.services.delete(@service.name)
@software.remove_service(@service)
@@ -128,13 +128,14 @@ def deregister
run_on_change_callbacks
end
+ # Copies credentials files to the target system.
def finish
return unless reg_code
files = [@credentials_file, SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE]
files.each do |file|
dest = File.join(Yast::Installation.destdir, file)
- FileUtils::cp(file, dest)
+ FileUtils.cp(file, dest)
end
end
diff --git a/service/test/agama/dbus/software/product_test.rb b/service/test/agama/dbus/software/product_test.rb
index b7b89a875d..57f2e442f8 100644
--- a/service/test/agama/dbus/software/product_test.rb
+++ b/service/test/agama/dbus/software/product_test.rb
@@ -179,8 +179,12 @@
context "if there is a selected product" do
before do
backend.select_product("Tumbleweed")
+
+ allow(backend.product).to receive(:repositories).and_return(repositories)
end
+ let(:repositories) { [] }
+
context "if the product is already registered" do
before do
allow(backend.registration).to receive(:reg_code).and_return("123XX432")
@@ -191,13 +195,21 @@
end
end
+ context "if the product does not require registration" do
+ let(:repositories) { ["https://repo"] }
+
+ it "returns result code 3 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(3, /not require registration/i)
+ end
+ end
+
context "if there is a network error" do
before do
allow(backend.registration).to receive(:register).and_raise(SocketError)
end
- it "returns result code 3 and description" do
- expect(subject.register("123XX432")).to contain_exactly(3, /network error/)
+ it "returns result code 4 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(4, /network error/)
end
end
@@ -206,8 +218,8 @@
allow(backend.registration).to receive(:register).and_raise(Timeout::Error)
end
- it "returns result code 4 and description" do
- expect(subject.register("123XX432")).to contain_exactly(4, /timeout/)
+ it "returns result code 5 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(5, /timeout/)
end
end
@@ -216,8 +228,8 @@
allow(backend.registration).to receive(:register).and_raise(SUSE::Connect::ApiError, "")
end
- it "returns result code 5 and description" do
- expect(subject.register("123XX432")).to contain_exactly(5, /registration server failed/)
+ it "returns result code 6 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(6, /registration server failed/)
end
end
@@ -227,8 +239,8 @@
.to receive(:register).and_raise(SUSE::Connect::MissingSccCredentialsFile)
end
- it "returns result code 6 and description" do
- expect(subject.register("123XX432")).to contain_exactly(6, /missing credentials/)
+ it "returns result code 7 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(7, /missing credentials/)
end
end
@@ -238,8 +250,8 @@
.to receive(:register).and_raise(SUSE::Connect::MalformedSccCredentialsFile)
end
- it "returns result code 7 and description" do
- expect(subject.register("123XX432")).to contain_exactly(7, /incorrect credentials/)
+ it "returns result code 8 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(8, /incorrect credentials/)
end
end
@@ -248,8 +260,8 @@
allow(backend.registration).to receive(:register).and_raise(OpenSSL::SSL::SSLError)
end
- it "returns result code 8 and description" do
- expect(subject.register("123XX432")).to contain_exactly(8, /invalid certificate/)
+ it "returns result code 9 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(9, /invalid certificate/)
end
end
@@ -258,8 +270,8 @@
allow(backend.registration).to receive(:register).and_raise(JSON::ParserError)
end
- it "returns result code 9 and description" do
- expect(subject.register("123XX432")).to contain_exactly(9, /registration server failed/)
+ it "returns result code 10 and description" do
+ expect(subject.register("123XX432")).to contain_exactly(10, /registration server failed/)
end
end
From 691f71c26ebd5ecd48d20927719724bf75db8f79 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 20 Oct 2023 15:38:00 +0100
Subject: [PATCH 58/97] [service] Use ruby-dbus 0.23.1
---
service/Gemfile.lock | 4 ++--
service/agama.gemspec | 2 +-
service/lib/agama/dbus/software/manager.rb | 5 +----
3 files changed, 4 insertions(+), 7 deletions(-)
diff --git a/service/Gemfile.lock b/service/Gemfile.lock
index c114df5bb3..89fdf2541e 100644
--- a/service/Gemfile.lock
+++ b/service/Gemfile.lock
@@ -9,7 +9,7 @@ PATH
fast_gettext (~> 2.2.0)
nokogiri (~> 1.13.1)
rexml (~> 3.2.5)
- ruby-dbus (>= 0.23.0.beta2, < 1.0)
+ ruby-dbus (>= 0.23.1, < 1.0)
GEM
remote: https://rubygems.org/
@@ -49,7 +49,7 @@ GEM
rspec-support (~> 3.11.0)
rspec-support (3.11.0)
ruby-augeas (0.5.0)
- ruby-dbus (0.23.0.beta2)
+ ruby-dbus (0.23.1)
rexml
simplecov (0.21.2)
docile (~> 1.1)
diff --git a/service/agama.gemspec b/service/agama.gemspec
index bb2b561d4a..97df966e22 100644
--- a/service/agama.gemspec
+++ b/service/agama.gemspec
@@ -58,5 +58,5 @@ Gem::Specification.new do |spec|
spec.add_dependency "fast_gettext", "~> 2.2.0"
spec.add_dependency "nokogiri", "~> 1.13.1"
spec.add_dependency "rexml", "~> 3.2.5"
- spec.add_dependency "ruby-dbus", ">= 0.23.0.beta2", "< 1.0"
+ spec.add_dependency "ruby-dbus", ">= 0.23.1", "< 1.0"
end
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index 06df099ee3..aad196e3cb 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -83,12 +83,9 @@ def issues
]
end
- # documented way to be able to write to patterns and trigger signal
- attr_writer :selected_patterns
-
# selected patterns is hash with pattern name as id and 0 for user selected and
# 1 for auto selected. Can be extended in future e.g. for mandatory patterns
- dbus_attr_reader :selected_patterns, "a{sy}"
+ dbus_reader_attr_accessor :selected_patterns, "a{sy}"
dbus_method(:AddPattern, "in id:s") { |p| backend.add_pattern(p) }
dbus_method(:RemovePattern, "in id:s") { |p| backend.remove_pattern(p) }
From a2c702b9c4e3562e2b3f43a58ba74c71c6274ae9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 20 Oct 2023 16:08:28 +0100
Subject: [PATCH 59/97] [service] Update D-Bus documentation
---
...g.opensuse.Agama.Software1.Product.bus.xml | 53 +++++++++++++++++++
.../bus/org.opensuse.Agama.Software1.bus.xml | 20 +------
.../org.opensuse.Agama1.Registration.bus.xml | 2 +-
...g.opensuse.Agama.Software1.Product.doc.xml | 38 +++++++++++++
doc/dbus/org.opensuse.Agama.Software1.doc.xml | 34 +-----------
doc/dbus/org.opensuse.Agama1.Progress.doc.xml | 4 +-
.../org.opensuse.Agama1.Registration.doc.xml | 17 +++---
7 files changed, 105 insertions(+), 63 deletions(-)
create mode 100644 doc/dbus/bus/org.opensuse.Agama.Software1.Product.bus.xml
create mode 100644 doc/dbus/org.opensuse.Agama.Software1.Product.doc.xml
diff --git a/doc/dbus/bus/org.opensuse.Agama.Software1.Product.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Software1.Product.bus.xml
new file mode 100644
index 0000000000..7bd3ab733f
--- /dev/null
+++ b/doc/dbus/bus/org.opensuse.Agama.Software1.Product.bus.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml
index 44b5873a17..9535bb3c0f 100644
--- a/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml
+++ b/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml
@@ -1,5 +1,6 @@
+
@@ -28,10 +29,6 @@
-
-
-
-
@@ -64,8 +61,6 @@
-
-
@@ -76,19 +71,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/doc/dbus/bus/org.opensuse.Agama1.Registration.bus.xml b/doc/dbus/bus/org.opensuse.Agama1.Registration.bus.xml
index d0feb248d1..9cfd11ce93 120000
--- a/doc/dbus/bus/org.opensuse.Agama1.Registration.bus.xml
+++ b/doc/dbus/bus/org.opensuse.Agama1.Registration.bus.xml
@@ -1 +1 @@
-org.opensuse.Agama.Software1.bus.xml
\ No newline at end of file
+org.opensuse.Agama.Software1.Product.bus.xml
\ No newline at end of file
diff --git a/doc/dbus/org.opensuse.Agama.Software1.Product.doc.xml b/doc/dbus/org.opensuse.Agama.Software1.Product.doc.xml
new file mode 100644
index 0000000000..165a67094b
--- /dev/null
+++ b/doc/dbus/org.opensuse.Agama.Software1.Product.doc.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/dbus/org.opensuse.Agama.Software1.doc.xml b/doc/dbus/org.opensuse.Agama.Software1.doc.xml
index 82bc7e5208..bacbe87d94 100644
--- a/doc/dbus/org.opensuse.Agama.Software1.doc.xml
+++ b/doc/dbus/org.opensuse.Agama.Software1.doc.xml
@@ -1,27 +1,9 @@
+
-
-
-
-
-
-
-
@@ -54,20 +36,6 @@
-
-
-
-
diff --git a/doc/dbus/org.opensuse.Agama1.Progress.doc.xml b/doc/dbus/org.opensuse.Agama1.Progress.doc.xml
index 7c2cc909fd..1a1a3ed0e0 100644
--- a/doc/dbus/org.opensuse.Agama1.Progress.doc.xml
+++ b/doc/dbus/org.opensuse.Agama1.Progress.doc.xml
@@ -1,7 +1,9 @@
-
+
+
+
@@ -25,13 +23,14 @@
0: success
1: missing product
2: already registered
- 3: network error
- 4: timeout error
- 5: api error
- 6: missing credentials
- 7: incorrect credentials
+ 3: registration not required
+ 4: network error
+ 5: timeout error
+ 6: api error
+ 7: missing credentials
+ 8: incorrect credentials
8: invalid certificate
- 9: internal error (e.g., parsing json data)
+ 10: internal error (e.g., parsing json data)
-->
From 7317e611d10ad1d35afd9ac017cf636c54fd2b09 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 20 Oct 2023 16:18:32 +0100
Subject: [PATCH 60/97] [web] Fix tests
---
web/src/client/software.test.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/web/src/client/software.test.js b/web/src/client/software.test.js
index 5e12a2ff54..65fd0e2a9a 100644
--- a/web/src/client/software.test.js
+++ b/web/src/client/software.test.js
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022] SUSE LLC
+ * Copyright (c) [2022-2023] SUSE LLC
*
* All Rights Reserved.
*
@@ -26,9 +26,9 @@ import { SoftwareClient } from "./software";
jest.mock("./dbus");
-const SOFTWARE_IFACE = "org.opensuse.Agama.Software1";
+const PRODUCT_IFACE = "org.opensuse.Agama.Software1.Product";
-const softProxy = {
+const productProxy = {
wait: jest.fn(),
AvailableProducts: [
["MicroOS", "openSUSE MicroOS", {}],
@@ -42,7 +42,7 @@ beforeEach(() => {
DBusClient.mockImplementation(() => {
return {
proxy: (iface) => {
- if (iface === SOFTWARE_IFACE) return softProxy;
+ if (iface === PRODUCT_IFACE) return productProxy;
}
};
});
From c99a98e21d97c2577736257bc7e8c6fb62e0cc51 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Fri, 20 Oct 2023 17:37:50 +0200
Subject: [PATCH 61/97] run probe and propose after registration/deregistration
---
service/lib/agama/software/manager.rb | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index 94bb65beb9..d72ffc94cf 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -278,7 +278,14 @@ def used_disk_space
end
def registration
- @registration ||= Registration.new(self, @logger)
+ return @registration if @registration
+
+ @registration = Registration.new(self, @logger)
+ @registration.on_change do
+ # reprobe and repropose when system is register or deregistered
+ probe
+ proposal
+ end
end
# code is based on https://github.com/yast/yast-registration/blob/master/src/lib/registration/sw_mgmt.rb#L365
From 133acc889f716fa3a6d90591c2705c0dcf52ca83 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Fri, 20 Oct 2023 21:57:38 +0200
Subject: [PATCH 62/97] fix return statement
---
service/lib/agama/software/manager.rb | 2 ++
1 file changed, 2 insertions(+)
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index d72ffc94cf..c0dea816fe 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -286,6 +286,8 @@ def registration
probe
proposal
end
+
+ @registration
end
# code is based on https://github.com/yast/yast-registration/blob/master/src/lib/registration/sw_mgmt.rb#L365
From aa57609ac10057aace093088ffc1983dd567183a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Mon, 23 Oct 2023 11:29:59 +0100
Subject: [PATCH 63/97] [rust] Adapt software proxies and client
---
rust/agama-lib/src/software/client.rs | 12 ++++++------
rust/agama-lib/src/software/proxies.rs | 21 ++++++++++++++-------
2 files changed, 20 insertions(+), 13 deletions(-)
diff --git a/rust/agama-lib/src/software/client.rs b/rust/agama-lib/src/software/client.rs
index 8e90b5a474..2d6ae30c39 100644
--- a/rust/agama-lib/src/software/client.rs
+++ b/rust/agama-lib/src/software/client.rs
@@ -1,4 +1,4 @@
-use super::proxies::Software1Proxy;
+use super::proxies::SoftwareProductProxy;
use crate::error::ServiceError;
use serde::Serialize;
use zbus::Connection;
@@ -16,20 +16,20 @@ pub struct Product {
/// D-Bus client for the software service
pub struct SoftwareClient<'a> {
- software_proxy: Software1Proxy<'a>,
+ product_proxy: SoftwareProductProxy<'a>,
}
impl<'a> SoftwareClient<'a> {
pub async fn new(connection: Connection) -> Result, ServiceError> {
Ok(Self {
- software_proxy: Software1Proxy::new(&connection).await?,
+ product_proxy: SoftwareProductProxy::new(&connection).await?,
})
}
/// Returns the available products
pub async fn products(&self) -> Result, ServiceError> {
let products: Vec = self
- .software_proxy
+ .product_proxy
.available_products()
.await?
.into_iter()
@@ -50,12 +50,12 @@ impl<'a> SoftwareClient<'a> {
/// Returns the selected product to install
pub async fn product(&self) -> Result {
- Ok(self.software_proxy.selected_product().await?)
+ Ok(self.product_proxy.selected_product().await?)
}
/// Selects the product to install
pub async fn select_product(&self, product_id: &str) -> Result<(), ServiceError> {
- let result = self.software_proxy.select_product(product_id).await?;
+ let result = self.product_proxy.select_product(product_id).await?;
match result {
(0, _) => Ok(()),
diff --git a/rust/agama-lib/src/software/proxies.rs b/rust/agama-lib/src/software/proxies.rs
index b9721bbf4a..8fc081b3c6 100644
--- a/rust/agama-lib/src/software/proxies.rs
+++ b/rust/agama-lib/src/software/proxies.rs
@@ -39,15 +39,26 @@ trait Software1 {
/// RemovePattern method
fn remove_pattern(&self, id: &str) -> zbus::Result<()>;
- /// SelectProduct method
- fn select_product(&self, id: &str) -> zbus::Result<(u32, String)>;
-
/// SetUserPatterns method
fn set_user_patterns(&self, ids: &[&str]) -> zbus::Result<()>;
/// UsedDiskSpace method
fn used_disk_space(&self) -> zbus::Result;
+ /// SelectedPatterns property
+ #[dbus_proxy(property)]
+ fn selected_patterns(&self) -> zbus::Result>;
+}
+
+#[dbus_proxy(
+ interface = "org.opensuse.Agama.Software1.Product",
+ default_service = "org.opensuse.Agama.Software1",
+ default_path = "/org/opensuse/Agama/Software1/Product"
+)]
+trait SoftwareProduct {
+ /// SelectProduct method
+ fn select_product(&self, id: &str) -> zbus::Result<(u32, String)>;
+
/// AvailableProducts property
#[dbus_proxy(property)]
fn available_products(
@@ -60,10 +71,6 @@ trait Software1 {
)>,
>;
- /// SelectedPatterns property
- #[dbus_proxy(property)]
- fn selected_patterns(&self) -> zbus::Result>;
-
/// SelectedProduct property
#[dbus_proxy(property)]
fn selected_product(&self) -> zbus::Result;
From 847952e103ac70fb1fc4e2496e6e5af2c96f9472 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Mon, 23 Oct 2023 11:32:09 +0100
Subject: [PATCH 64/97] [service] Rubocop
---
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 c0dea816fe..d536ce2bc5 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -52,7 +52,7 @@ module Software
# * Manages software and product related issues.
#
# It shoud be splitted in separate and smaller classes.
- class Manager
+ class Manager # rubocop:disable Metrics/ClassLength
include Helpers
include WithIssues
include WithProgress
From a78702b7af653ddcaf2a61ebe6b6993501ff08a4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Mon, 23 Oct 2023 13:39:47 +0100
Subject: [PATCH 65/97] Add missing package to setup script
---
setup-service.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup-service.sh b/setup-service.sh
index 4760be186f..97c3dcd8af 100755
--- a/setup-service.sh
+++ b/setup-service.sh
@@ -39,7 +39,7 @@ test -f /etc/zypp/repos.d/d_l_python.repo || \
$SUDO zypper --non-interactive \
addrepo https://download.opensuse.org/repositories/devel:/languages:/python/openSUSE_Tumbleweed/ d_l_python
$SUDO zypper --non-interactive --gpg-auto-import-keys install gcc gcc-c++ make openssl-devel ruby-devel \
- python-langtable-data git augeas-devel jemalloc-devel awk || exit 1
+ python-langtable-data git augeas-devel jemalloc-devel awk suseconnect-ruby-bindings || exit 1
# only install cargo if it is not available (avoid conflicts with rustup)
which cargo || $SUDO zypper --non-interactive install cargo
From aa32a970e7de5cd640c1bb460f2fa70422ad32c0 Mon Sep 17 00:00:00 2001
From: Josef Reidinger
Date: Mon, 23 Oct 2023 18:41:12 +0200
Subject: [PATCH 66/97] fix writting credentials to target system
---
service/lib/agama/registration.rb | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb
index 56944cedbf..3466147215 100644
--- a/service/lib/agama/registration.rb
+++ b/service/lib/agama/registration.rb
@@ -118,8 +118,7 @@ def deregister
SUSE::Connect::YaST.deactivate_system(connect_params)
FileUtils.rm(SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE) # connect does not remove it itself
if @credentials_file
- path = File.join(SUSE::Connect::YaST::DEFAULT_CREDENTIALS_DIR, @credentials_file)
- FileUtils.rm(path)
+ FileUtils.rm(credentials_path(@credentials_file))
@credentials_file = nil
end
@@ -132,7 +131,7 @@ def deregister
def finish
return unless reg_code
- files = [@credentials_file, SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE]
+ files = [credentials_path(@credentials_file), SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE]
files.each do |file|
dest = File.join(Yast::Installation.destdir, file)
FileUtils.cp(file, dest)
@@ -189,5 +188,9 @@ def credentials_from_url(url)
# if something goes wrong try to continue like if there is no credentials param
nil
end
+
+ def credentials_path(file)
+ File.join(SUSE::Connect::YaST::DEFAULT_CREDENTIALS_DIR, file)
+ end
end
end
From ba7f48295640a79c0696dbf7d17046374db0267e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 3 Nov 2023 15:10:01 +0000
Subject: [PATCH 67/97] [service] Adapt to changes in master
---
service/lib/agama/config.rb | 5 +-
service/lib/agama/dbus/software/product.rb | 2 +-
service/lib/agama/software/manager.rb | 4 +-
service/lib/agama/software/product_builder.rb | 4 +-
service/test/agama/config_test.rb | 45 +++++++++------
.../test/agama/dbus/software/product_test.rb | 14 ++---
service/test/agama/software/manager_test.rb | 7 +--
.../agama/software/product_builder_test.rb | 56 ++++++++++---------
8 files changed, 76 insertions(+), 61 deletions(-)
diff --git a/service/lib/agama/config.rb b/service/lib/agama/config.rb
index c49721d63f..0e524d648c 100644
--- a/service/lib/agama/config.rb
+++ b/service/lib/agama/config.rb
@@ -142,7 +142,8 @@ def merge(config)
# Elements that match the current arch.
#
# @example
- # config.pure_data = {
+ # config.products #=>
+ # {
# "ALP-Dolomite" => {
# "software" => {
# "installation_repositories" => [
@@ -171,7 +172,7 @@ def merge(config)
# @return [Array]
def arch_elements_from(*keys, property: nil)
keys.map!(&:to_s)
- elements = pure_data.dig(*keys)
+ elements = products.dig(*keys)
return [] unless elements.is_a?(Array)
elements.map do |element|
diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb
index 83a4c4a703..3abfd98825 100644
--- a/service/lib/agama/dbus/software/product.rb
+++ b/service/lib/agama/dbus/software/product.rb
@@ -35,7 +35,7 @@ class Product < BaseObject
PATH = "/org/opensuse/Agama/Software1/Product"
private_constant :PATH
- # @param backend [Agama::Software]
+ # @param backend [Agama::Software::Manager]
# @param logger [Logger]
def initialize(backend, logger)
super(PATH, logger: logger)
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index c0a8ac951a..cb398b00dc 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -101,8 +101,9 @@ def initialize(config, logger)
# @raise {ArgumentError} If id is unknown.
#
# @param id [String]
+ # @return [Boolean] true on success.
def select_product(id)
- return if id == product&.id
+ return false if id == product&.id
new_product = @products.find { |p| p.id == id }
@@ -111,6 +112,7 @@ def select_product(id)
@product = new_product
repositories.delete_all
update_issues
+ true
end
def probe
diff --git a/service/lib/agama/software/product_builder.rb b/service/lib/agama/software/product_builder.rb
index 7982c37ade..0cd70a7473 100644
--- a/service/lib/agama/software/product_builder.rb
+++ b/service/lib/agama/software/product_builder.rb
@@ -62,8 +62,8 @@ def build
# @return [Hash]
def product_data_from_config(id)
{
- name: config.pure_data.dig(id, "software", "base_product"),
- version: config.pure_data.dig(id, "software", "version"),
+ name: config.products.dig(id, "software", "base_product"),
+ version: config.products.dig(id, "software", "version"),
repositories: config.arch_elements_from(
id, "software", "installation_repositories", property: :url
),
diff --git a/service/test/agama/config_test.rb b/service/test/agama/config_test.rb
index b9dbb241d5..f6e6c4dc58 100644
--- a/service/test/agama/config_test.rb
+++ b/service/test/agama/config_test.rb
@@ -22,6 +22,7 @@
require_relative "../test_helper"
require "yast"
require "agama/config"
+require "agama/product_reader"
Yast.import "Arch"
@@ -133,15 +134,22 @@
end
describe "#arch_elements_from" do
- subject { described_class.new(data) }
+ subject { described_class.new }
+
+ before do
+ allow(Agama::ProductReader).to receive(:new).and_return(reader)
+ end
+
+ let(:reader) { instance_double(Agama::ProductReader, load_products: products) }
context "when the given set of keys does not match any data" do
- let(:data) do
- {
- "Product1" => {
+ let(:products) do
+ [
+ {
+ "id" => "Product1",
"name" => "Test product 1"
}
- }
+ ]
end
it "returns an empty array" do
@@ -150,12 +158,13 @@
end
context "when the given set of keys does not contain a collection" do
- let(:data) do
- {
- "Product1" => {
+ let(:products) do
+ [
+ {
+ "id" => "Product1",
"name" => "Test product 1"
}
- }
+ ]
end
it "returns an empty array" do
@@ -164,9 +173,10 @@
end
context "when the given set of keys contains a collection" do
- let(:data) do
- {
- "Product1" => {
+ let(:products) do
+ [
+ {
+ "id" => "Product1",
"some" => {
"collection" => [
"element1",
@@ -188,7 +198,7 @@
]
}
}
- }
+ ]
end
before do
@@ -209,9 +219,10 @@
end
context "and there are no elements matching the current arch" do
- let(:data) do
- {
- "Product1" => {
+ let(:products) do
+ [
+ {
+ "id" => "Product1",
"some" => {
"collection" => [
{
@@ -225,7 +236,7 @@
]
}
}
- }
+ ]
end
it "returns an empty list" do
diff --git a/service/test/agama/dbus/software/product_test.rb b/service/test/agama/dbus/software/product_test.rb
index 57f2e442f8..e25c36e661 100644
--- a/service/test/agama/dbus/software/product_test.rb
+++ b/service/test/agama/dbus/software/product_test.rb
@@ -33,17 +33,17 @@
let(:backend) { Agama::Software::Manager.new(config, logger) }
- let(:config) { Agama::Config.new(config_data) }
-
- let(:config_data) do
- path = File.join(FIXTURES_PATH, "root_dir/etc/agama.yaml")
- YAML.safe_load(File.read(path))
- end
+ let(:config) { Agama::Config.new }
before do
+ allow(config).to receive(:products).and_return(products)
allow(subject).to receive(:dbus_properties_changed)
end
+ let(:products) do
+ { "Tumbleweed" => {}, "ALP-Dolomite" => {} }
+ end
+
it "defines Product D-Bus interface" do
expect(subject.intfs.keys).to include("org.opensuse.Agama.Software1.Product")
end
@@ -75,7 +75,7 @@
context "if the current product is registered" do
before do
- subject.select_product("Leap")
+ subject.select_product("Leap16")
allow(backend.registration).to receive(:reg_code).and_return("123XX432")
end
diff --git a/service/test/agama/software/manager_test.rb b/service/test/agama/software/manager_test.rb
index 1439b02b45..77bec63e2a 100644
--- a/service/test/agama/software/manager_test.rb
+++ b/service/test/agama/software/manager_test.rb
@@ -129,7 +129,6 @@
shared_examples "software issues" do |tested_method|
before do
- subject.select_product("Tumbleweed")
allow(subject.registration).to receive(:reg_code).and_return(reg_code)
end
@@ -192,7 +191,7 @@
stub_const("Agama::Software::Manager::REPOS_DIR", repos_dir)
stub_const("Agama::Software::Manager::REPOS_BACKUP", backup_repos_dir)
FileUtils.mkdir_p(repos_dir)
- subject.select_product("Tumbleweed")
+ subject.select_product("ALP-Dolomite")
end
after do
@@ -233,9 +232,9 @@
expect(products.size).to eq(3)
expect(products).to all(be_a(Agama::Software::Product))
expect(products).to contain_exactly(
+ an_object_having_attributes(id: "ALP-Dolomite"),
an_object_having_attributes(id: "Tumbleweed"),
- an_object_having_attributes(id: "Leap Micro"),
- an_object_having_attributes(id: "Leap")
+ an_object_having_attributes(id: "Leap16")
)
end
end
diff --git a/service/test/agama/software/product_builder_test.rb b/service/test/agama/software/product_builder_test.rb
index e3dd6a4345..f7bf6b48c2 100644
--- a/service/test/agama/software/product_builder_test.rb
+++ b/service/test/agama/software/product_builder_test.rb
@@ -22,36 +22,26 @@
require_relative "../../test_helper"
require "yast"
require "agama/config"
+require "agama/product_reader"
require "agama/software/product"
require "agama/software/product_builder"
Yast.import "Arch"
describe Agama::Software::ProductBuilder do
- subject { described_class.new(config) }
+ before do
+ allow(Agama::ProductReader).to receive(:new).and_return(reader)
+ end
- let(:config) { Agama::Config.new(data) }
+ let(:reader) { instance_double(Agama::ProductReader, load_products: products) }
- let(:data) do
- {
- "products" => {
- "Test1" => {
- "name" => "Product Test 1",
- "description" => "This is a test product named Test 1"
- },
- "Test2" => {
- "name" => "Product Test 2",
- "description" => "This is a test product named Test 2",
- "archs" => "x86_64,aarch64"
- },
- "Test3" => {
- "name" => "Product Test 3",
- "description" => "This is a test product named Test 3",
- "archs" => "ppc64,aarch64"
- }
- },
- "Test1" => {
- "software" => {
+ let(:products) do
+ [
+ {
+ "id" => "Test1",
+ "name" => "Product Test 1",
+ "description" => "This is a test product named Test 1",
+ "software" => {
"installation_repositories" => [
{
"url" => "https://repos/test1/x86_64/product/",
@@ -92,15 +82,23 @@
"version" => "1.0"
}
},
- "Test2" => {
- "software" => {
+ {
+ "id" => "Test2",
+ "name" => "Product Test 2",
+ "description" => "This is a test product named Test 2",
+ "archs" => "x86_64,aarch64",
+ "software" => {
"mandatory_patterns" => ["pattern2-1"],
"base_product" => "Test2",
"version" => "2.0"
}
},
- "Test3" => {
- "software" => {
+ {
+ "id" => "Test3",
+ "name" => "Product Test 3",
+ "description" => "This is a test product named Test 3",
+ "archs" => "ppc64,aarch64",
+ "software" => {
"installation_repositories" => ["https://repos/test3/product/"],
"optional_patterns" => [
{
@@ -111,9 +109,13 @@
"base_product" => "Test3"
}
}
- }
+ ]
end
+ subject { described_class.new(config) }
+
+ let(:config) { Agama::Config.new }
+
describe "#build" do
context "for x86_64" do
before do
From 6709506f0bf87e5c28afbf9d25ea5ae9b0f27761 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Wed, 15 Nov 2023 09:48:06 +0000
Subject: [PATCH 68/97] [service] Adapt product translations to changes in
registration
---
service/lib/agama/dbus/software/manager.rb | 25 --------
service/lib/agama/dbus/software/product.rb | 2 +-
service/lib/agama/software/product.rb | 49 ++++++++++++--
service/lib/agama/software/product_builder.rb | 1 +
.../test/agama/dbus/software/manager_test.rb | 64 -------------------
.../agama/software/product_builder_test.rb | 40 ++++++++----
service/test/agama/software/product_test.rb | 64 +++++++++++++++++++
7 files changed, 138 insertions(+), 107 deletions(-)
create mode 100644 service/test/agama/software/product_test.rb
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index 7217d90811..aad196e3cb 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -149,31 +149,6 @@ def register_callbacks
backend.on_issues_change { issues_properties_changed }
end
- # find translated product description if available
- # @param data [Hash] product configuration from the YAML file
- # @return [String,nil] Translated product description (if available)
- # or the untranslated description, nil if not found
- def localized_description(data)
- translations = data["translations"]&.[]("description")
- lang = ENV["LANG"] || ""
-
- # no translations or language not set, return untranslated value
- return data["description"] if !translations.is_a?(Hash) || lang.empty?
-
- # remove the character encoding if present
- lang = lang.split(".").first
- # full matching (language + country)
- return translations[lang] if translations[lang]
-
- # remove the country part
- lang = lang.split("_").first
- # partial match (just the language)
- return translations[lang] if translations[lang]
-
- # fallback to original untranslated description
- data["description"]
- end
-
USER_SELECTED_PATTERN = 0
AUTO_SELECTED_PATTERN = 1
def compute_patterns
diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb
index 3abfd98825..5085e42444 100644
--- a/service/lib/agama/dbus/software/product.rb
+++ b/service/lib/agama/dbus/software/product.rb
@@ -53,7 +53,7 @@ def issues
def available_products
backend.products.map do |product|
- [product.id, product.display_name, { "description" => product.description }]
+ [product.id, product.display_name, { "description" => product.localized_description }]
end
end
diff --git a/service/lib/agama/software/product.rb b/service/lib/agama/software/product.rb
index 71f1ade8ac..24bb8c0b2b 100644
--- a/service/lib/agama/software/product.rb
+++ b/service/lib/agama/software/product.rb
@@ -30,22 +30,22 @@ class Product
# Name of the product to be display.
#
- # @return [String]
+ # @return [String, nil]
attr_accessor :display_name
# Description of the product.
#
- # @return [String]
+ # @return [String, nil]
attr_accessor :description
# Internal name of the product. This is relevant for registering the product.
#
- # @return [String]
+ # @return [String, nil]
attr_accessor :name
# Version of the product. This is relevant for registering the product.
#
- # @return [String] E.g., "1.0".
+ # @return [String, nil] E.g., "1.0".
attr_accessor :version
# List of repositories.
@@ -73,6 +73,19 @@ class Product
# @return [Array]
attr_accessor :optional_patterns
+ # Product translations.
+ #
+ # @example
+ # product.translations #=>
+ # {
+ # "description" => {
+ # "cs" => "Czech translation",
+ # "es" => "Spanish translation"
+ # }
+ #
+ # @return [Hash>]
+ attr_accessor :translations
+
# @param id [string] Product id.
def initialize(id)
@id = id
@@ -81,6 +94,34 @@ def initialize(id)
@optional_packages = []
@mandatory_patterns = []
@optional_patterns = []
+ @translations = {}
+ end
+
+ # Localized product description.
+ #
+ # If there is no translation for the current language, then the untranslated description is
+ # used.
+ #
+ # @return [String, nil]
+ def localized_description
+ translations = self.translations["description"]
+ lang = ENV["LANG"]
+
+ # No translations or language not set, return untranslated value.
+ return description unless translations && lang
+
+ # Remove the character encoding if present.
+ lang = lang.split(".").first
+ # Full matching (language + country)
+ return translations[lang] if translations[lang]
+
+ # Remove the country part.
+ lang = lang.split("_").first
+ # Partial match (just the language).
+ return translations[lang] if translations[lang]
+
+ # Fallback to original untranslated description.
+ description
end
end
end
diff --git a/service/lib/agama/software/product_builder.rb b/service/lib/agama/software/product_builder.rb
index 0cd70a7473..b4ecfeddf8 100644
--- a/service/lib/agama/software/product_builder.rb
+++ b/service/lib/agama/software/product_builder.rb
@@ -47,6 +47,7 @@ def build
product.optional_packages = data[:optional_packages]
product.mandatory_patterns = data[:mandatory_patterns]
product.optional_patterns = data[:optional_patterns]
+ product.translations = attrs["translations"] || {}
end
end
end
diff --git a/service/test/agama/dbus/software/manager_test.rb b/service/test/agama/dbus/software/manager_test.rb
index ea94877819..925218b9d5 100644
--- a/service/test/agama/dbus/software/manager_test.rb
+++ b/service/test/agama/dbus/software/manager_test.rb
@@ -135,68 +135,4 @@
expect(installed).to eq(true)
end
end
-
- describe "#available_base_products" do
- # testing product with translations
- products = {
- "Tumbleweed" => {
- "name" => "openSUSE Tumbleweed",
- "description" => "Original description",
- "translations" => {
- "description" => {
- "cs" => "Czech translation",
- "es" => "Spanish translation"
- }
- }
- }
- }
-
- it "returns product ID and name" do
- expect(backend).to receive(:products).and_return(products)
-
- product = subject.available_base_products.first
- expect(product[0]).to eq("Tumbleweed")
- expect(product[1]).to eq("openSUSE Tumbleweed")
- end
-
- it "returns untranslated description when the language is not set" do
- allow(ENV).to receive(:[]).with("LANG").and_return(nil)
- expect(backend).to receive(:products).and_return(products)
-
- product = subject.available_base_products.first
- expect(product[2]["description"]).to eq("Original description")
- end
-
- it "returns Czech translation if locale is \"cs_CZ.UTF-8\"" do
- allow(ENV).to receive(:[]).with("LANG").and_return("cs_CZ.UTF-8")
- expect(backend).to receive(:products).and_return(products)
-
- product = subject.available_base_products.first
- expect(product[2]["description"]).to eq("Czech translation")
- end
-
- it "returns Czech translation if locale is \"cs\"" do
- allow(ENV).to receive(:[]).with("LANG").and_return("cs")
- expect(backend).to receive(:products).and_return(products)
-
- product = subject.available_base_products.first
- expect(product[2]["description"]).to eq("Czech translation")
- end
-
- it "return untranslated description when translation is not available" do
- allow(ENV).to receive(:[]).with("LANG").and_return("cs_CZ.UTF-8")
-
- # testing product without translations
- untranslated = {
- "Tumbleweed" => {
- "name" => "openSUSE Tumbleweed",
- "description" => "Original description"
- }
- }
- expect(backend).to receive(:products).and_return(untranslated)
-
- product = subject.available_base_products.first
- expect(product[2]["description"]).to eq("Original description")
- end
- end
end
diff --git a/service/test/agama/software/product_builder_test.rb b/service/test/agama/software/product_builder_test.rb
index f7bf6b48c2..a48bc87ceb 100644
--- a/service/test/agama/software/product_builder_test.rb
+++ b/service/test/agama/software/product_builder_test.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Copyright (c) [2022-2023] SUSE LLC
+# Copyright (c) [2023] SUSE LLC
#
# All Rights Reserved.
#
@@ -38,10 +38,16 @@
let(:products) do
[
{
- "id" => "Test1",
- "name" => "Product Test 1",
- "description" => "This is a test product named Test 1",
- "software" => {
+ "id" => "Test1",
+ "name" => "Product Test 1",
+ "description" => "This is a test product named Test 1",
+ "translations" => {
+ "description" => {
+ "cs" => "Czech",
+ "es" => "Spanish"
+ }
+ },
+ "software" => {
"installation_repositories" => [
{
"url" => "https://repos/test1/x86_64/product/",
@@ -141,7 +147,8 @@
mandatory_patterns: ["pattern1-1", "pattern1-2"],
optional_patterns: ["pattern1-3"],
mandatory_packages: ["package1-1", "package1-2", "package1-3"],
- optional_packages: ["package1-5"]
+ optional_packages: ["package1-5"],
+ translations: { "description" => { "cs" => "Czech", "es" => "Spanish" } }
),
an_object_having_attributes(
id: "Test2",
@@ -153,7 +160,8 @@
mandatory_patterns: ["pattern2-1"],
optional_patterns: [],
mandatory_packages: [],
- optional_packages: []
+ optional_packages: [],
+ translations: {}
)
)
end
@@ -183,7 +191,8 @@
mandatory_patterns: ["pattern1-1", "pattern1-2"],
optional_patterns: ["pattern1-4"],
mandatory_packages: ["package1-1", "package1-2", "package1-3"],
- optional_packages: ["package1-5"]
+ optional_packages: ["package1-5"],
+ translations: { "description" => { "cs" => "Czech", "es" => "Spanish" } }
),
an_object_having_attributes(
id: "Test2",
@@ -195,7 +204,8 @@
mandatory_patterns: ["pattern2-1"],
optional_patterns: [],
mandatory_packages: [],
- optional_packages: []
+ optional_packages: [],
+ translations: {}
),
an_object_having_attributes(
id: "Test3",
@@ -207,7 +217,8 @@
mandatory_patterns: [],
optional_patterns: ["pattern3-1"],
mandatory_packages: [],
- optional_packages: []
+ optional_packages: [],
+ translations: {}
)
)
end
@@ -237,7 +248,8 @@
mandatory_patterns: ["pattern1-1", "pattern1-2"],
optional_patterns: [],
mandatory_packages: ["package1-1", "package1-2", "package1-4"],
- optional_packages: ["package1-5"]
+ optional_packages: ["package1-5"],
+ translations: { "description" => { "cs" => "Czech", "es" => "Spanish" } }
),
an_object_having_attributes(
id: "Test3",
@@ -249,7 +261,8 @@
mandatory_patterns: [],
optional_patterns: [],
mandatory_packages: [],
- optional_packages: []
+ optional_packages: [],
+ translations: {}
)
)
end
@@ -279,7 +292,8 @@
mandatory_patterns: ["pattern1-1", "pattern1-2"],
optional_patterns: [],
mandatory_packages: ["package1-1", "package1-2"],
- optional_packages: ["package1-5"]
+ optional_packages: ["package1-5"],
+ translations: { "description" => { "cs" => "Czech", "es" => "Spanish" } }
)
)
end
diff --git a/service/test/agama/software/product_test.rb b/service/test/agama/software/product_test.rb
new file mode 100644
index 0000000000..aab6a8397e
--- /dev/null
+++ b/service/test/agama/software/product_test.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+# Copyright (c) [2023] SUSE LLC
+#
+# All Rights Reserved.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of version 2 of the GNU General Public License as published
+# by the Free Software Foundation.
+#
+# 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.
+
+require_relative "../../test_helper"
+require "agama/software/product"
+
+describe Agama::Software::Product do
+ subject { described_class.new("Test") }
+
+ describe "#localized_description" do
+ before do
+ subject.description = "Original description"
+ subject.translations = {
+ "description" => {
+ "cs" => "Czech translation",
+ "es" => "Spanish translation"
+ }
+ }
+ end
+
+ it "returns untranslated description when the language is not set" do
+ allow(ENV).to receive(:[]).with("LANG").and_return(nil)
+
+ expect(subject.localized_description).to eq("Original description")
+ end
+
+ it "returns Czech translation if locale is \"cs_CZ.UTF-8\"" do
+ allow(ENV).to receive(:[]).with("LANG").and_return("cs_CZ.UTF-8")
+
+ expect(subject.localized_description).to eq("Czech translation")
+ end
+
+ it "returns Czech translation if locale is \"cs\"" do
+ allow(ENV).to receive(:[]).with("LANG").and_return("cs")
+
+ expect(subject.localized_description).to eq("Czech translation")
+ end
+
+ it "return untranslated description when translation is not available" do
+ allow(ENV).to receive(:[]).with("LANG").and_return("cs_CZ.UTF-8")
+ subject.translations = {}
+
+ expect(subject.localized_description).to eq("Original description")
+ end
+ end
+end
From 1d27be92895129200504144f35aefdf62798a74e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Tue, 24 Oct 2023 13:16:48 +0100
Subject: [PATCH 69/97] [web] Add product manager to software client
---
web/src/client/software.js | 123 ++++++++++--------
web/src/client/software.test.js | 34 ++---
.../software/ProductSelectionPage.jsx | 10 +-
.../software/ProductSelectionPage.test.jsx | 14 +-
web/src/context/software.jsx | 7 +-
5 files changed, 106 insertions(+), 82 deletions(-)
diff --git a/web/src/client/software.js b/web/src/client/software.js
index 193f90575e..9db954ff8b 100644
--- a/web/src/client/software.js
+++ b/web/src/client/software.js
@@ -37,6 +37,75 @@ const PRODUCT_PATH = "/org/opensuse/Agama/Software1/Product";
* @property {string} description - Product description
*/
+/**
+ * Product manager.
+ * @ignore
+ */
+class BaseProductManager {
+ /**
+ * @param {DBusClient} client
+ */
+ constructor(client) {
+ this.client = client;
+ }
+
+ /**
+ * Returns the list of available products.
+ *
+ * @return {Promise>}
+ */
+ async getAll() {
+ const proxy = await this.client.proxy(PRODUCT_IFACE);
+ return proxy.AvailableProducts.map(product => {
+ const [id, name, meta] = product;
+ return { id, name, description: meta.description?.v };
+ });
+ }
+
+ /**
+ * Returns the selected product.
+ *
+ * @return {Promise}
+ */
+ async getSelected() {
+ const products = await this.getAll();
+ const proxy = await this.client.proxy(PRODUCT_IFACE);
+ if (proxy.SelectedProduct === "") {
+ return null;
+ }
+ return products.find(product => product.id === proxy.SelectedProduct);
+ }
+
+ /**
+ * Selects a product for installation.
+ *
+ * @param {string} id - Product ID.
+ */
+ async select(id) {
+ const proxy = await this.client.proxy(PRODUCT_IFACE);
+ return proxy.SelectProduct(id);
+ }
+
+ /**
+ * Registers a callback to run when properties in the Product object change.
+ *
+ * @param {(id: string) => void} handler - Callback function.
+ */
+ onChange(handler) {
+ return this.client.onObjectChanged(PRODUCT_PATH, PRODUCT_IFACE, changes => {
+ if ("SelectedProduct" in changes) {
+ const selected = changes.SelectedProduct.v.toString();
+ handler(selected);
+ }
+ });
+ }
+}
+
+/**
+ * Manages product selection.
+ */
+class ProductManager extends WithIssues(BaseProductManager, PRODUCT_PATH) { }
+
/**
* Software client
*
@@ -48,6 +117,7 @@ class SoftwareBaseClient {
*/
constructor(address = undefined) {
this.client = new DBusClient(SOFTWARE_SERVICE, address);
+ this.product = new ProductManager(this.client);
}
/**
@@ -60,19 +130,6 @@ class SoftwareBaseClient {
return proxy.Probe();
}
- /**
- * Returns the list of available products
- *
- * @return {Promise>}
- */
- async getProducts() {
- const proxy = await this.client.proxy(PRODUCT_IFACE);
- return proxy.AvailableProducts.map(product => {
- const [id, name, meta] = product;
- return { id, name, description: meta.description?.v };
- });
- }
-
/**
* Returns how much space installation takes on disk
*
@@ -130,48 +187,10 @@ class SoftwareBaseClient {
const proxy = await this.client.proxy(SOFTWARE_IFACE);
return proxy.RemovePattern(name);
}
-
- /**
- * Returns the selected product
- *
- * @return {Promise}
- */
- async getSelectedProduct() {
- const products = await this.getProducts();
- const proxy = await this.client.proxy(PRODUCT_IFACE);
- if (proxy.SelectedProduct === "") {
- return null;
- }
- return products.find(product => product.id === proxy.SelectedProduct);
- }
-
- /**
- * Selects a product for installation
- *
- * @param {string} id - product ID
- */
- async selectProduct(id) {
- const proxy = await this.client.proxy(PRODUCT_IFACE);
- return proxy.SelectProduct(id);
- }
-
- /**
- * Registers a callback to run when properties in the Software object change
- *
- * @param {(id: string) => void} handler - callback function
- */
- onProductChange(handler) {
- return this.client.onObjectChanged(PRODUCT_PATH, PRODUCT_IFACE, changes => {
- if ("SelectedProduct" in changes) {
- const selected = changes.SelectedProduct.v.toString();
- handler(selected);
- }
- });
- }
}
/**
- * Allows getting the list the available products and selecting one for installation.
+ * Manages software and product configuration.
*/
class SoftwareClient extends WithIssues(
WithProgress(
diff --git a/web/src/client/software.test.js b/web/src/client/software.test.js
index 65fd0e2a9a..6baa381b8f 100644
--- a/web/src/client/software.test.js
+++ b/web/src/client/software.test.js
@@ -48,23 +48,25 @@ beforeEach(() => {
});
});
-describe("#getProducts", () => {
- it("returns the list of available products", async () => {
- const client = new SoftwareClient();
- const availableProducts = await client.getProducts();
- expect(availableProducts).toEqual([
- { id: "MicroOS", name: "openSUSE MicroOS" },
- { id: "Tumbleweed", name: "openSUSE Tumbleweed" }
- ]);
+describe("#product", () => {
+ describe("#getAll", () => {
+ it("returns the list of available products", async () => {
+ const client = new SoftwareClient();
+ const availableProducts = await client.product.getAll();
+ expect(availableProducts).toEqual([
+ { id: "MicroOS", name: "openSUSE MicroOS" },
+ { id: "Tumbleweed", name: "openSUSE Tumbleweed" }
+ ]);
+ });
});
-});
-describe('#getSelectedProduct', () => {
- it("returns the selected product", async () => {
- const client = new SoftwareClient();
- const selectedProduct = await client.getSelectedProduct();
- expect(selectedProduct).toEqual(
- { id: "MicroOS", name: "openSUSE MicroOS" }
- );
+ describe('#getSelected', () => {
+ it("returns the selected product", async () => {
+ const client = new SoftwareClient();
+ const selectedProduct = await client.product.getSelected();
+ expect(selectedProduct).toEqual(
+ { id: "MicroOS", name: "openSUSE MicroOS" }
+ );
+ });
});
});
diff --git a/web/src/components/software/ProductSelectionPage.jsx b/web/src/components/software/ProductSelectionPage.jsx
index 86b8d622ef..c4cfb3121f 100644
--- a/web/src/components/software/ProductSelectionPage.jsx
+++ b/web/src/components/software/ProductSelectionPage.jsx
@@ -38,7 +38,7 @@ import { Icon, Loading } from "~/components/layout";
import { Title, PageIcon, MainActions } from "~/components/layout/Layout";
function ProductSelectionPage() {
- const client = useInstallerClient();
+ const { software, manager } = useInstallerClient();
const navigate = useNavigate();
const { products, selectedProduct } = useSoftware();
const previous = selectedProduct?.id;
@@ -47,8 +47,8 @@ function ProductSelectionPage() {
useEffect(() => {
// TODO: display a notification in the UI to emphasizes that
// selected product has changed
- return client.software.onProductChange(() => navigate("/"));
- }, [client.software, navigate]);
+ return software.product.onChange(() => navigate("/"));
+ }, [software, navigate]);
const isSelected = p => p.id === selected;
@@ -60,8 +60,8 @@ function ProductSelectionPage() {
}
// TODO: handle errors
- await client.software.selectProduct(selected);
- client.manager.startProbing();
+ await software.product.select(selected);
+ manager.startProbing();
navigate("/");
};
diff --git a/web/src/components/software/ProductSelectionPage.test.jsx b/web/src/components/software/ProductSelectionPage.test.jsx
index 197e33cb27..ecf6c981ed 100644
--- a/web/src/components/software/ProductSelectionPage.test.jsx
+++ b/web/src/components/software/ProductSelectionPage.test.jsx
@@ -54,10 +54,12 @@ const managerMock = {
};
const softwareMock = {
- getProducts: () => Promise.resolve(products),
- getSelectedProduct: jest.fn(() => Promise.resolve(products[0])),
- selectProduct: jest.fn().mockResolvedValue(),
- onProductChange: jest.fn()
+ product: {
+ getAll: () => Promise.resolve(products),
+ getSelected: jest.fn(() => Promise.resolve(products[0])),
+ select: jest.fn().mockResolvedValue(),
+ onChange: jest.fn()
+ }
};
beforeEach(() => {
@@ -76,7 +78,7 @@ describe("when the user chooses a product", () => {
await user.click(radio);
const button = await screen.findByRole("button", { name: "Select" });
await user.click(button);
- expect(softwareMock.selectProduct).toHaveBeenCalledWith("MicroOS");
+ expect(softwareMock.product.select).toHaveBeenCalledWith("MicroOS");
expect(managerMock.startProbing).toHaveBeenCalled();
expect(mockNavigateFn).toHaveBeenCalledWith("/");
});
@@ -88,7 +90,7 @@ describe("when the user chooses does not change the product", () => {
await screen.findByText("openSUSE Tumbleweed");
const button = await screen.findByRole("button", { name: "Select" });
await user.click(button);
- expect(softwareMock.selectProduct).not.toHaveBeenCalled();
+ expect(softwareMock.product.select).not.toHaveBeenCalled();
expect(managerMock.startProbing).not.toHaveBeenCalled();
expect(mockNavigateFn).toHaveBeenCalledWith("/");
});
diff --git a/web/src/context/software.jsx b/web/src/context/software.jsx
index 8e53f91405..0d018ab752 100644
--- a/web/src/context/software.jsx
+++ b/web/src/context/software.jsx
@@ -33,8 +33,9 @@ function SoftwareProvider({ children }) {
useEffect(() => {
const loadProducts = async () => {
- const available = await cancellablePromise(client.software.getProducts());
- const selected = await cancellablePromise(client.software.getSelectedProduct());
+ const productManager = client.software.product;
+ const available = await cancellablePromise(productManager.getAll());
+ const selected = await cancellablePromise(productManager.getSelected());
setProducts(available);
setSelectedId(selected?.id || null);
};
@@ -47,7 +48,7 @@ function SoftwareProvider({ children }) {
useEffect(() => {
if (!client) return;
- return client.software.onProductChange(setSelectedId);
+ return client.software.product.onChange(setSelectedId);
}, [client, setSelectedId]);
const value = [products, selectedId];
From 4038cbe840fdc09226ec0356eb73a53cb365d83c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Tue, 24 Oct 2023 17:09:12 +0100
Subject: [PATCH 70/97] [web] Add registration to software client
---
web/src/client/software.js | 97 ++++++++++++++++++++++++++++++
web/src/client/software.test.js | 102 +++++++++++++++++++++++++++++++-
2 files changed, 198 insertions(+), 1 deletion(-)
diff --git a/web/src/client/software.js b/web/src/client/software.js
index 9db954ff8b..87d2ae3850 100644
--- a/web/src/client/software.js
+++ b/web/src/client/software.js
@@ -29,6 +29,7 @@ const SOFTWARE_IFACE = "org.opensuse.Agama.Software1";
const SOFTWARE_PATH = "/org/opensuse/Agama/Software1";
const PRODUCT_IFACE = "org.opensuse.Agama.Software1.Product";
const PRODUCT_PATH = "/org/opensuse/Agama/Software1/Product";
+const REGISTRATION_IFACE = "org.opensuse.Agama1.Registration";
/**
* @typedef {object} Product
@@ -37,6 +38,20 @@ const PRODUCT_PATH = "/org/opensuse/Agama/Software1/Product";
* @property {string} description - Product description
*/
+/**
+ * @typedef {object} Registration
+ * @property {string} code - Registration code.
+ * @property {string} email - Registration email.
+ * @property {string} requirement - Registration requirement (i.e., "not-required, "optional",
+ * "mandatory").
+ */
+
+/**
+ * @typedef {object} ActionResult
+ * @property {boolean} success - Whether the action was successfuly done.
+ * @property {string} message - Result message.
+ */
+
/**
* Product manager.
* @ignore
@@ -99,6 +114,88 @@ class BaseProductManager {
}
});
}
+
+ /**
+ * Returns the registration of the selected product.
+ *
+ * @return {Promise}
+ */
+ async getRegistration() {
+ const proxy = await this.client.proxy(REGISTRATION_IFACE, PRODUCT_PATH);
+ const code = proxy.RegCode;
+ const email = proxy.Email;
+ const requirement = this.registrationRequirement(proxy.Requirement);
+
+ return (code.length === 0 ? null : { code, email, requirement });
+ }
+
+ /**
+ * Tries to register the selected product.
+ *
+ * @param {string} code
+ * @param {string} [email]
+ * @returns {Promise}
+ */
+ async register(code, email = "") {
+ const proxy = await this.client.proxy(REGISTRATION_IFACE, PRODUCT_PATH);
+ const result = await proxy.Register(code, { email });
+
+ return {
+ success: result[0] === 0,
+ message: result[1]
+ };
+ }
+
+ /**
+ * Tries to deregister the selected product.
+ *
+ * @returns {Promise}
+ */
+ async deregister() {
+ const proxy = await this.client.proxy(REGISTRATION_IFACE, PRODUCT_PATH);
+ const result = await proxy.Deregister();
+
+ return {
+ success: result[0] === 0,
+ message: result[1]
+ };
+ }
+
+ /**
+ * Registers a callback to run when the registration changes.
+ *
+ * @param {(registration: Registration) => void} handler - Callback function.
+ */
+ onRegistrationChange(handler) {
+ return this.client.onObjectChanged(PRODUCT_PATH, REGISTRATION_IFACE, () => {
+ this.getRegistration().then(handler);
+ });
+ }
+
+ /**
+ * Helper method to generate the requirement representation.
+ * @private
+ *
+ * @param {number} value - D-Bus registration value.
+ * @returns {string}
+ */
+ registrationRequirement(value) {
+ let requirement;
+
+ switch (value) {
+ case 0:
+ requirement = "not-required";
+ break;
+ case 1:
+ requirement = "optional";
+ break;
+ case 2:
+ requirement = "mandatory";
+ break;
+ }
+
+ return requirement;
+ }
}
/**
diff --git a/web/src/client/software.test.js b/web/src/client/software.test.js
index 6baa381b8f..b04d34206b 100644
--- a/web/src/client/software.test.js
+++ b/web/src/client/software.test.js
@@ -27,6 +27,7 @@ import { SoftwareClient } from "./software";
jest.mock("./dbus");
const PRODUCT_IFACE = "org.opensuse.Agama.Software1.Product";
+const REGISTRATION_IFACE = "org.opensuse.Agama1.Registration";
const productProxy = {
wait: jest.fn(),
@@ -37,12 +38,15 @@ const productProxy = {
SelectedProduct: "MicroOS"
};
+const registrationProxy = {};
+
beforeEach(() => {
// @ts-ignore
DBusClient.mockImplementation(() => {
return {
proxy: (iface) => {
if (iface === PRODUCT_IFACE) return productProxy;
+ if (iface === REGISTRATION_IFACE) return registrationProxy;
}
};
});
@@ -60,7 +64,7 @@ describe("#product", () => {
});
});
- describe('#getSelected', () => {
+ describe("#getSelected", () => {
it("returns the selected product", async () => {
const client = new SoftwareClient();
const selectedProduct = await client.product.getSelected();
@@ -69,4 +73,100 @@ describe("#product", () => {
);
});
});
+
+ describe("#getRegistration", () => {
+ describe("if there is no registration code", () => {
+ beforeEach(() => {
+ registrationProxy.RegCode = "";
+ });
+
+ it("returns null", async () => {
+ const client = new SoftwareClient();
+ const registration = await client.product.getRegistration();
+ expect(registration).toBeNull();
+ });
+ });
+
+ describe("if there is registration code", () => {
+ beforeEach(() => {
+ registrationProxy.RegCode = "111222";
+ registrationProxy.Email = "test@test.com";
+ registrationProxy.Requirement = 2;
+ });
+
+ it("returns the registration", async () => {
+ const client = new SoftwareClient();
+ const registration = await client.product.getRegistration();
+ expect(registration).toStrictEqual({
+ code: "111222",
+ email: "test@test.com",
+ requirement: "mandatory"
+ });
+ });
+ });
+ });
+
+ describe("#register", () => {
+ describe("when the action is correctly done", () => {
+ beforeEach(() => {
+ registrationProxy.Register = jest.fn().mockResolvedValue([0, ""]);
+ });
+
+ it("returns a successful result", async () => {
+ const client = new SoftwareClient();
+ const result = await client.product.register("111222", "test@test.com");
+ expect(result).toStrictEqual({
+ success: true,
+ message: ""
+ });
+ });
+ });
+
+ describe("when the action fails", () => {
+ beforeEach(() => {
+ registrationProxy.Register = jest.fn().mockResolvedValue([1, "error message"]);
+ });
+
+ it("returns an unsuccessful result", async () => {
+ const client = new SoftwareClient();
+ const result = await client.product.register("111222", "test@test.com");
+ expect(result).toStrictEqual({
+ success: false,
+ message: "error message"
+ });
+ });
+ });
+ });
+
+ describe("#deregister", () => {
+ describe("when the action is correctly done", () => {
+ beforeEach(() => {
+ registrationProxy.Deregister = jest.fn().mockResolvedValue([0, ""]);
+ });
+
+ it("returns a successful result", async () => {
+ const client = new SoftwareClient();
+ const result = await client.product.deregister();
+ expect(result).toStrictEqual({
+ success: true,
+ message: ""
+ });
+ });
+ });
+
+ describe("when the action fails", () => {
+ beforeEach(() => {
+ registrationProxy.Deregister = jest.fn().mockResolvedValue([1, "error message"]);
+ });
+
+ it("returns an unsuccessful result", async () => {
+ const client = new SoftwareClient();
+ const result = await client.product.deregister();
+ expect(result).toStrictEqual({
+ success: false,
+ message: "error message"
+ });
+ });
+ });
+ });
});
From 911bcdc438cf516beaf439edd7ddcd1f88061d25 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Wed, 25 Oct 2023 09:27:49 +0100
Subject: [PATCH 71/97] [web] Add product provider
---
web/src/components/overview/Overview.jsx | 4 +-
web/src/components/overview/Overview.test.jsx | 6 +--
.../components/software/ChangeProductLink.jsx | 4 +-
.../software/ChangeProductLink.test.jsx | 10 ++--
.../software/ProductSelectionPage.jsx | 4 +-
.../software/ProductSelectionPage.test.jsx | 6 +--
web/src/context/agama.jsx | 6 +--
web/src/context/{software.jsx => product.jsx} | 52 ++++++++++---------
8 files changed, 47 insertions(+), 45 deletions(-)
rename web/src/context/{software.jsx => product.jsx} (53%)
diff --git a/web/src/components/overview/Overview.jsx b/web/src/components/overview/Overview.jsx
index ed13813746..8d6850dce5 100644
--- a/web/src/components/overview/Overview.jsx
+++ b/web/src/components/overview/Overview.jsx
@@ -20,7 +20,7 @@
*/
import React, { useState } from "react";
-import { useSoftware } from "~/context/software";
+import { useProduct } from "~/context/product";
import { Navigate } from "react-router-dom";
import { Page, InstallButton } from "~/components/core";
@@ -33,7 +33,7 @@ import {
} from "~/components/overview";
function Overview() {
- const { selectedProduct } = useSoftware();
+ const { selectedProduct } = useProduct();
const [showErrors, setShowErrors] = useState(false);
if (selectedProduct === null) {
diff --git a/web/src/components/overview/Overview.test.jsx b/web/src/components/overview/Overview.test.jsx
index 05c12e0f9e..b3462eb20c 100644
--- a/web/src/components/overview/Overview.test.jsx
+++ b/web/src/components/overview/Overview.test.jsx
@@ -34,9 +34,9 @@ const startInstallationFn = jest.fn();
jest.mock("~/client");
-jest.mock("~/context/software", () => ({
- ...jest.requireActual("~/context/software"),
- useSoftware: () => {
+jest.mock("~/context/product", () => ({
+ ...jest.requireActual("~/context/product"),
+ useProduct: () => {
return {
products: mockProducts,
selectedProduct: mockProduct
diff --git a/web/src/components/software/ChangeProductLink.jsx b/web/src/components/software/ChangeProductLink.jsx
index 8dc6f07001..3eb872231a 100644
--- a/web/src/components/software/ChangeProductLink.jsx
+++ b/web/src/components/software/ChangeProductLink.jsx
@@ -21,12 +21,12 @@
import React from "react";
import { Link } from "react-router-dom";
-import { useSoftware } from "~/context/software";
+import { useProduct } from "~/context/product";
import { Icon } from "~/components/layout";
import { _ } from "~/i18n";
export default function ChangeProductLink() {
- const { products } = useSoftware();
+ const { products } = useProduct();
if (products?.length === 1) return null;
diff --git a/web/src/components/software/ChangeProductLink.test.jsx b/web/src/components/software/ChangeProductLink.test.jsx
index c89a62222a..c5a41e3309 100644
--- a/web/src/components/software/ChangeProductLink.test.jsx
+++ b/web/src/components/software/ChangeProductLink.test.jsx
@@ -28,9 +28,9 @@ import { ChangeProductLink } from "~/components/software";
let mockProducts;
jest.mock("~/client");
-jest.mock("~/context/software", () => ({
- ...jest.requireActual("~/context/software"),
- useSoftware: () => {
+jest.mock("~/context/product", () => ({
+ ...jest.requireActual("~/context/product"),
+ useProduct: () => {
return {
products: mockProducts,
};
@@ -40,8 +40,8 @@ jest.mock("~/context/software", () => ({
beforeEach(() => {
createClient.mockImplementation(() => {
return {
- software: {
- onProductChange: jest.fn()
+ product: {
+ onChange: jest.fn()
},
};
});
diff --git a/web/src/components/software/ProductSelectionPage.jsx b/web/src/components/software/ProductSelectionPage.jsx
index c4cfb3121f..4b4663aaa6 100644
--- a/web/src/components/software/ProductSelectionPage.jsx
+++ b/web/src/components/software/ProductSelectionPage.jsx
@@ -22,7 +22,7 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useInstallerClient } from "~/context/installer";
-import { useSoftware } from "~/context/software";
+import { useProduct } from "~/context/product";
import { _ } from "~/i18n";
import {
@@ -40,7 +40,7 @@ import { Title, PageIcon, MainActions } from "~/components/layout/Layout";
function ProductSelectionPage() {
const { software, manager } = useInstallerClient();
const navigate = useNavigate();
- const { products, selectedProduct } = useSoftware();
+ const { products, selectedProduct } = useProduct();
const previous = selectedProduct?.id;
const [selected, setSelected] = useState(selectedProduct?.id);
diff --git a/web/src/components/software/ProductSelectionPage.test.jsx b/web/src/components/software/ProductSelectionPage.test.jsx
index ecf6c981ed..b3abb5916a 100644
--- a/web/src/components/software/ProductSelectionPage.test.jsx
+++ b/web/src/components/software/ProductSelectionPage.test.jsx
@@ -39,9 +39,9 @@ const products = [
];
jest.mock("~/client");
-jest.mock("~/context/software", () => ({
- ...jest.requireActual("~/context/software"),
- useSoftware: () => {
+jest.mock("~/context/product", () => ({
+ ...jest.requireActual("~/context/product"),
+ useProduct: () => {
return {
products,
selectedProduct: products[0]
diff --git a/web/src/context/agama.jsx b/web/src/context/agama.jsx
index 6306c1ba8c..0aa16e811b 100644
--- a/web/src/context/agama.jsx
+++ b/web/src/context/agama.jsx
@@ -24,7 +24,7 @@
import React from "react";
import { InstallerClientProvider } from "./installer";
import { L10nProvider } from "./l10n";
-import { SoftwareProvider } from "./software";
+import { ProductProvider } from "./product";
import { NotificationProvider } from "./notification";
/**
@@ -37,11 +37,11 @@ function AgamaProviders({ children }) {
return (
-
+
{children}
-
+
);
diff --git a/web/src/context/software.jsx b/web/src/context/product.jsx
similarity index 53%
rename from web/src/context/software.jsx
rename to web/src/context/product.jsx
index 0d018ab752..1d79959fdd 100644
--- a/web/src/context/software.jsx
+++ b/web/src/context/product.jsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022] SUSE LLC
+ * Copyright (c) [2023] SUSE LLC
*
* All Rights Reserved.
*
@@ -19,31 +19,34 @@
* find current contact information at www.suse.com.
*/
-import React, { useEffect } from "react";
+import React, { useContext, useEffect, useState } from "react";
import { useCancellablePromise } from "~/utils";
import { useInstallerClient } from "./installer";
-const SoftwareContext = React.createContext([]);
+const ProductContext = React.createContext([]);
-function SoftwareProvider({ children }) {
+function ProductProvider({ children }) {
const client = useInstallerClient();
const { cancellablePromise } = useCancellablePromise();
- const [products, setProducts] = React.useState(undefined);
- const [selectedId, setSelectedId] = React.useState(undefined);
+ const [products, setProducts] = useState(undefined);
+ const [selectedId, setSelectedId] = useState(undefined);
+ const [registration, setRegistration] = useState(undefined);
useEffect(() => {
- const loadProducts = async () => {
+ const load = async () => {
const productManager = client.software.product;
const available = await cancellablePromise(productManager.getAll());
const selected = await cancellablePromise(productManager.getSelected());
+ const registration = await cancellablePromise(productManager.getRegistration());
setProducts(available);
setSelectedId(selected?.id || null);
+ setRegistration(registration);
};
if (client) {
- loadProducts().catch(console.error);
+ load().catch(console.error);
}
- }, [client, setProducts, setSelectedId, cancellablePromise]);
+ }, [client, setProducts, setSelectedId, setRegistration, cancellablePromise]);
useEffect(() => {
if (!client) return;
@@ -51,28 +54,27 @@ function SoftwareProvider({ children }) {
return client.software.product.onChange(setSelectedId);
}, [client, setSelectedId]);
- const value = [products, selectedId];
- return {children} ;
+ useEffect(() => {
+ if (!client) return;
+
+ return client.software.product.onRegistrationChange(setRegistration);
+ }, [client, setRegistration]);
+
+ const value = { products, selectedId, registration };
+ return {children} ;
}
-function useSoftware() {
- const context = React.useContext(SoftwareContext);
+function useProduct() {
+ const context = useContext(ProductContext);
if (!context) {
- throw new Error("useSoftware must be used within a SoftwareProvider");
+ throw new Error("useProduct must be used within a ProductProvider");
}
- const [products, selectedId] = context;
-
- let selectedProduct = selectedId;
- if (selectedId && products) {
- selectedProduct = products.find(p => p.id === selectedId) || null;
- }
+ const { products = [], selectedId } = context;
+ const selectedProduct = products.find(p => p.id === selectedId);
- return {
- products,
- selectedProduct
- };
+ return { ...context, selectedProduct };
}
-export { SoftwareProvider, useSoftware };
+export { ProductProvider, useProduct };
From f88225d374cdc6b0ec2a10e0f91d3042ab9c53b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Wed, 25 Oct 2023 17:47:01 +0100
Subject: [PATCH 72/97] [web] Allow to configure the rows of SectionSkeleton
---
web/src/components/core/SectionSkeleton.jsx | 36 +++++++++++++--------
1 file changed, 22 insertions(+), 14 deletions(-)
diff --git a/web/src/components/core/SectionSkeleton.jsx b/web/src/components/core/SectionSkeleton.jsx
index b8e0fa8d70..bc6cf20cda 100644
--- a/web/src/components/core/SectionSkeleton.jsx
+++ b/web/src/components/core/SectionSkeleton.jsx
@@ -23,19 +23,27 @@ import React from "react";
import { Skeleton } from "@patternfly/react-core";
import { _ } from "~/i18n";
-const SectionSkeleton = () => (
- <>
-
-
- >
-);
+const SectionSkeleton = ({ numRows = 2 }) => {
+ const WaitingSkeleton = ({ width }) => {
+ return (
+
+ );
+ };
+
+ return (
+ <>
+ {
+ Array.from({ length: numRows }, (_, i) => {
+ const width = i % 2 === 0 ? "50%" : "25%";
+ return ;
+ })
+ }
+ >
+ );
+};
export default SectionSkeleton;
From 9ca8e76d3261761b9ec8a373f95361d2350524be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Wed, 25 Oct 2023 17:48:22 +0100
Subject: [PATCH 73/97] [web] Add product section to overview
---
web/src/components/overview/L10nSection.jsx | 2 +-
web/src/components/overview/Overview.jsx | 2 +
web/src/components/overview/Overview.test.jsx | 2 +
.../components/overview/ProductSection.jsx | 72 ++++++++++++++
.../overview/ProductSection.test.jsx | 93 +++++++++++++++++++
web/src/components/overview/index.js | 3 +-
.../software/ChangeProductLink.test.jsx | 12 ---
7 files changed, 172 insertions(+), 14 deletions(-)
create mode 100644 web/src/components/overview/ProductSection.jsx
create mode 100644 web/src/components/overview/ProductSection.test.jsx
diff --git a/web/src/components/overview/L10nSection.jsx b/web/src/components/overview/L10nSection.jsx
index ce8c47c111..1429b7f1a8 100644
--- a/web/src/components/overview/L10nSection.jsx
+++ b/web/src/components/overview/L10nSection.jsx
@@ -61,7 +61,7 @@ export default function L10nSection({ showErrors }) {
const SectionContent = () => {
const { busy, languages, language } = state;
- if (busy) return ;
+ if (busy) return ;
const selected = languages.find(lang => lang.id === language);
diff --git a/web/src/components/overview/Overview.jsx b/web/src/components/overview/Overview.jsx
index 8d6850dce5..0cc9e896dc 100644
--- a/web/src/components/overview/Overview.jsx
+++ b/web/src/components/overview/Overview.jsx
@@ -27,6 +27,7 @@ import { Page, InstallButton } from "~/components/core";
import {
L10nSection,
NetworkSection,
+ ProductSection,
SoftwareSection,
StorageSection,
UsersSection
@@ -46,6 +47,7 @@ function Overview() {
icon="inventory_2"
action={ setShowErrors(true)} />}
>
+
diff --git a/web/src/components/overview/Overview.test.jsx b/web/src/components/overview/Overview.test.jsx
index b3462eb20c..89a0a053bd 100644
--- a/web/src/components/overview/Overview.test.jsx
+++ b/web/src/components/overview/Overview.test.jsx
@@ -44,6 +44,7 @@ jest.mock("~/context/product", () => ({
}
}));
+jest.mock("~/components/overview/ProductSection", () => () => Product Section
);
jest.mock("~/components/overview/L10nSection", () => () => Localization Section
);
jest.mock("~/components/overview/StorageSection", () => () => Storage Section
);
jest.mock("~/components/overview/NetworkSection", () => () => Network Section
);
@@ -71,6 +72,7 @@ describe("when product is selected", () => {
const title = screen.getByText(/openSUSE Tumbleweed/i);
expect(title).toBeInTheDocument();
+ await screen.findByText("Product Section");
await screen.findByText("Localization Section");
await screen.findByText("Network Section");
await screen.findByText("Storage Section");
diff --git a/web/src/components/overview/ProductSection.jsx b/web/src/components/overview/ProductSection.jsx
new file mode 100644
index 0000000000..8b95d792af
--- /dev/null
+++ b/web/src/components/overview/ProductSection.jsx
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) [2023] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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, { useEffect, useState } from "react";
+import { Text } from "@patternfly/react-core";
+import { useCancellablePromise } from "~/utils";
+import { useInstallerClient } from "~/context/installer";
+import { useProduct } from "~/context/product";
+import { Section, SectionSkeleton } from "~/components/core";
+import { _ } from "~/i18n";
+
+const errorsFrom = (issues) => {
+ const errors = issues.filter(i => i.severity === "error");
+ return errors.map(e => ({ message: e.description }));
+};
+
+export default function ProductSection() {
+ const { software } = useInstallerClient();
+ const [issues, setIssues] = useState([]);
+ const { selectedProduct } = useProduct();
+ const { cancellablePromise } = useCancellablePromise();
+
+ useEffect(() => {
+ cancellablePromise(software.product.getIssues()).then(setIssues);
+ return software.product.onIssuesChange(setIssues);
+ }, [cancellablePromise, setIssues, software]);
+
+ const Content = ({ isLoading = false }) => {
+ if (isLoading) return ;
+
+ return (
+
+ {selectedProduct?.name}
+
+ );
+ };
+
+ const isLoading = !selectedProduct;
+ const errors = isLoading ? [] : errorsFrom(issues);
+
+ return (
+
+ );
+}
diff --git a/web/src/components/overview/ProductSection.test.jsx b/web/src/components/overview/ProductSection.test.jsx
new file mode 100644
index 0000000000..66fa1c2f17
--- /dev/null
+++ b/web/src/components/overview/ProductSection.test.jsx
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) [2023] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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, waitFor } from "@testing-library/react";
+import { installerRender } from "~/test-utils";
+import { createClient } from "~/client";
+import { ProductSection } from "~/components/overview";
+
+let mockProduct;
+
+const mockIssue = { severity: "error", description: "Fake issue" };
+
+jest.mock("~/client");
+
+jest.mock("~/components/core/SectionSkeleton", () => () => Loading
);
+
+jest.mock("~/context/product", () => ({
+ ...jest.requireActual("~/context/product"),
+ useProduct: () => ({ selectedProduct: mockProduct })
+}));
+
+beforeEach(() => {
+ const issues = [mockIssue];
+ mockProduct = { name: "Test Product" };
+
+ createClient.mockImplementation(() => {
+ return {
+ software: {
+ product: {
+ getIssues: jest.fn().mockResolvedValue(issues),
+ onIssuesChange: jest.fn()
+ }
+ }
+ };
+ });
+});
+
+it("shows the product name", async () => {
+ installerRender( );
+
+ await screen.findByText("Test Product");
+});
+
+it("shows the error", async () => {
+ installerRender( );
+
+ await screen.findByText("Fake issue");
+});
+
+it("does not show warnings", async () => {
+ mockIssue.severity = "warning";
+
+ installerRender( );
+
+ await waitFor(() => expect(screen.queryByText("Fake issue")).not.toBeInTheDocument());
+});
+
+describe("when no product is selected", () => {
+ beforeEach(() => {
+ mockProduct = undefined;
+ });
+
+ it("shows the skeleton", async () => {
+ installerRender( );
+
+ await screen.findByText("Loading");
+ });
+
+ it("does not show errors", async () => {
+ installerRender( );
+
+ await waitFor(() => expect(screen.queryByText("Fake issue")).not.toBeInTheDocument());
+ });
+});
diff --git a/web/src/components/overview/index.js b/web/src/components/overview/index.js
index bfab42832b..c46f96127a 100644
--- a/web/src/components/overview/index.js
+++ b/web/src/components/overview/index.js
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022] SUSE LLC
+ * Copyright (c) [2022-2023] SUSE LLC
*
* All Rights Reserved.
*
@@ -22,6 +22,7 @@
export { default as Overview } from "./Overview";
export { default as L10nSection } from "./L10nSection";
export { default as NetworkSection } from "./NetworkSection";
+export { default as ProductSection } from "./ProductSection";
export { default as SoftwareSection } from "./SoftwareSection";
export { default as StorageSection } from "./StorageSection";
export { default as UsersSection } from "./UsersSection";
diff --git a/web/src/components/software/ChangeProductLink.test.jsx b/web/src/components/software/ChangeProductLink.test.jsx
index c5a41e3309..55afa5e24b 100644
--- a/web/src/components/software/ChangeProductLink.test.jsx
+++ b/web/src/components/software/ChangeProductLink.test.jsx
@@ -22,12 +22,10 @@
import React from "react";
import { screen, waitFor } from "@testing-library/react";
import { installerRender } from "~/test-utils";
-import { createClient } from "~/client";
import { ChangeProductLink } from "~/components/software";
let mockProducts;
-jest.mock("~/client");
jest.mock("~/context/product", () => ({
...jest.requireActual("~/context/product"),
useProduct: () => {
@@ -37,16 +35,6 @@ jest.mock("~/context/product", () => ({
}
}));
-beforeEach(() => {
- createClient.mockImplementation(() => {
- return {
- product: {
- onChange: jest.fn()
- },
- };
- });
-});
-
describe("ChangeProductLink", () => {
describe("when there is only a single product", () => {
beforeEach(() => {
From 4334e9a3f245b3a862a5df225d9ea578c13d9a8f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Wed, 25 Oct 2023 18:12:58 +0100
Subject: [PATCH 74/97] [web] Adapt title of overview page
---
web/src/components/layout/Icon.jsx | 2 ++
web/src/components/overview/Overview.jsx | 7 ++++---
web/src/components/overview/Overview.test.jsx | 2 +-
3 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/web/src/components/layout/Icon.jsx b/web/src/components/layout/Icon.jsx
index f3866cccf3..57ff9259dd 100644
--- a/web/src/components/layout/Icon.jsx
+++ b/web/src/components/layout/Icon.jsx
@@ -57,6 +57,7 @@ import SettingsEthernet from "@icons/settings_ethernet.svg?component";
import SettingsFill from "@icons/settings-fill.svg?component";
import SignalCellularAlt from "@icons/signal_cellular_alt.svg?component";
import Storage from "@icons/storage.svg?component";
+import Summarize from "@icons/summarize.svg?component";
import TaskAlt from "@icons/task_alt.svg?component";
import Terminal from "@icons/terminal.svg?component";
import Translate from "@icons/translate.svg?component";
@@ -110,6 +111,7 @@ const icons = {
settings_ethernet: SettingsEthernet,
signal_cellular_alt: SignalCellularAlt,
storage: Storage,
+ summarize: Summarize,
task_alt: TaskAlt,
terminal: Terminal,
translate: Translate,
diff --git a/web/src/components/overview/Overview.jsx b/web/src/components/overview/Overview.jsx
index 0cc9e896dc..205f82e576 100644
--- a/web/src/components/overview/Overview.jsx
+++ b/web/src/components/overview/Overview.jsx
@@ -22,7 +22,6 @@
import React, { useState } from "react";
import { useProduct } from "~/context/product";
import { Navigate } from "react-router-dom";
-
import { Page, InstallButton } from "~/components/core";
import {
L10nSection,
@@ -32,6 +31,7 @@ import {
StorageSection,
UsersSection
} from "~/components/overview";
+import { _ } from "~/i18n";
function Overview() {
const { selectedProduct } = useProduct();
@@ -43,8 +43,9 @@ function Overview() {
return (
setShowErrors(true)} />}
>
diff --git a/web/src/components/overview/Overview.test.jsx b/web/src/components/overview/Overview.test.jsx
index 89a0a053bd..dcb0733bd5 100644
--- a/web/src/components/overview/Overview.test.jsx
+++ b/web/src/components/overview/Overview.test.jsx
@@ -69,7 +69,7 @@ beforeEach(() => {
describe("when product is selected", () => {
it("renders the Overview and the Install button", async () => {
installerRender( );
- const title = screen.getByText(/openSUSE Tumbleweed/i);
+ const title = screen.getByText(/installation summary/i);
expect(title).toBeInTheDocument();
await screen.findByText("Product Section");
From b8524bd7b547faff66d2d7f54f3402f3d90b9fa8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Thu, 26 Oct 2023 16:34:42 +0100
Subject: [PATCH 75/97] [web] Move issues management to client
---
web/src/client/index.js | 50 ++++++++++++-
web/src/client/issues.js | 78 --------------------
web/src/client/issues.test.js | 94 ------------------------
web/src/components/core/Sidebar.test.jsx | 20 +++--
web/src/context/notification.jsx | 5 +-
5 files changed, 63 insertions(+), 184 deletions(-)
delete mode 100644 web/src/client/issues.js
delete mode 100644 web/src/client/issues.test.js
diff --git a/web/src/client/index.js b/web/src/client/index.js
index b4d56f18e0..96079edcc3 100644
--- a/web/src/client/index.js
+++ b/web/src/client/index.js
@@ -30,7 +30,6 @@ import { UsersClient } from "./users";
import phase from "./phase";
import { QuestionsClient } from "./questions";
import { NetworkClient } from "./network";
-import { IssuesClient } from "./issues";
import cockpit from "../lib/cockpit";
const BUS_ADDRESS_FILE = "/run/agama/bus.address";
@@ -46,13 +45,26 @@ const MANAGER_SERVICE = "org.opensuse.Agama.Manager1";
* @property {StorageClient} storage - storage client
* @property {UsersClient} users - users client
* @property {QuestionsClient} questions - questions client
- * @property {IssuesClient} issues - issues client
+ * @property {() => Promise} issues - issues from all contexts
+ * @property {(handler: IssuesHandler) => (() => void)} onIssuesChange - registers a handler to run
+ * when issues from any context change. It returns a function to deregister the handler.
* @property {() => Promise} isConnected - determines whether the client is connected
* @property {(handler: () => void) => (() => void)} onDisconnect - registers a handler to run
* when the connection is lost. It returns a function to deregister the
* handler.
*/
+/**
+ * @typedef {import ("~/client/mixins").Issue} Issue
+ *
+ * @typedef {object} Issues
+ * @property {Issue[]} [product] - Issues from product.
+ * @property {Issue[]} [storage] - Issues from storage.
+ * @property {Issue[]} [software] - Issues from software.
+ *
+ * @typedef {(issues: Issues) => void} IssuesHandler
+*/
+
/**
* Creates the Agama client
*
@@ -67,7 +79,38 @@ const createClient = (address = "unix:path=/run/agama/bus") => {
const storage = new StorageClient(address);
const users = new UsersClient(address);
const questions = new QuestionsClient(address);
- const issues = new IssuesClient({ storage });
+
+ /**
+ * Gets all issues, grouping them by context.
+ *
+ * TODO: issues are requested by several components (e.g., overview sections, notifications
+ * provider, issues page, storage page, etc). There should be an issues provider.
+ *
+ * @returns {Promise}
+ */
+ const issues = async () => {
+ return {
+ product: await software.product.getIssues(),
+ storage: await storage.getIssues(),
+ software: await software.getIssues()
+ };
+ };
+
+ /**
+ * Registers a callback to be executed when issues change.
+ *
+ * @param {IssuesHandler} handler - Callback function.
+ * @return {() => void} - Function to deregister the callback.
+ */
+ const onIssuesChange = (handler) => {
+ const unsubscribeCallbacks = [];
+
+ unsubscribeCallbacks.push(software.product.onIssuesChange(i => handler({ product: i })));
+ unsubscribeCallbacks.push(storage.onIssuesChange(i => handler({ storage: i })));
+ unsubscribeCallbacks.push(software.onIssuesChange(i => handler({ software: i })));
+
+ return () => { unsubscribeCallbacks.forEach(cb => cb()) };
+ };
const isConnected = async () => {
try {
@@ -88,6 +131,7 @@ const createClient = (address = "unix:path=/run/agama/bus") => {
users,
questions,
issues,
+ onIssuesChange,
isConnected,
onDisconnect: (handler) => monitor.onDisconnect(handler)
};
diff --git a/web/src/client/issues.js b/web/src/client/issues.js
deleted file mode 100644
index f6a38d4e26..0000000000
--- a/web/src/client/issues.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (c) [2023] SUSE LLC
- *
- * All Rights Reserved.
- *
- * This program is free software; you can redistribute it and/or modify it
- * under the terms of version 2 of the GNU General Public License as published
- * by the Free Software Foundation.
- *
- * 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.
- */
-
-// @ts-check
-
-/**
- * @typedef {object} ClientsIssues
- * @property {import ("~/client/mixins").Issue[]} storage - Issues from storage client
- */
-
-/**
- * Client for managing all issues, independently on the service owning the issues
- */
-class IssuesClient {
- /**
- * @param {object} clients - Clients managing issues
- * @param {import ("~/client/storage").StorageClient} clients.storage
- */
- constructor(clients) {
- this.clients = clients;
- }
-
- /**
- * Get issues from all clients managing issues
- *
- * @returns {Promise}
- */
- async getAll() {
- const storage = await this.clients.storage.getIssues();
-
- return { storage };
- }
-
- /**
- * Checks whether there is some error
- *
- * @returns {Promise}
- */
- async any() {
- const clientsIssues = await this.getAll();
- const issues = Object.values(clientsIssues).flat();
-
- return issues.length > 0;
- }
-
- /**
- * Registers a callback for each service to be executed when its issues change
- *
- * @param {import ("~/client/mixins").IssuesHandler} handler - callback function
- * @return {import ("./dbus").RemoveFn} function to disable the callback
- */
- onIssuesChange(handler) {
- const unsubscribeCallbacks = [];
- unsubscribeCallbacks.push(this.clients.storage.onIssuesChange(handler));
-
- return () => { unsubscribeCallbacks.forEach(cb => cb()) };
- }
-}
-
-export { IssuesClient };
diff --git a/web/src/client/issues.test.js b/web/src/client/issues.test.js
deleted file mode 100644
index 2eec15df3f..0000000000
--- a/web/src/client/issues.test.js
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (c) [2023] SUSE LLC
- *
- * All Rights Reserved.
- *
- * This program is free software; you can redistribute it and/or modify it
- * under the terms of version 2 of the GNU General Public License as published
- * by the Free Software Foundation.
- *
- * 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.
- */
-
-// @ts-check
-
-import { IssuesClient } from "./issues";
-import { StorageClient } from "./storage";
-
-const storageIssues = [
- { description: "Storage issue 1", severity: "error", details: "", source: "" },
- { description: "Storage issue 2", severity: "warn", details: "", source: "" },
- { description: "Storage issue 3", severity: "error", details: "", source: "" }
-];
-
-const issues = {
- storage: []
-};
-
-jest.spyOn(StorageClient.prototype, 'getIssues').mockImplementation(async () => issues.storage);
-jest.spyOn(StorageClient.prototype, 'onIssuesChange');
-
-const clientsWithIssues = {
- storage: new StorageClient()
-};
-
-describe("#getAll", () => {
- beforeEach(() => {
- issues.storage = storageIssues;
- });
-
- it("returns all the storage issues", async () => {
- const client = new IssuesClient(clientsWithIssues);
-
- const { storage } = await client.getAll();
- expect(storage).toEqual(expect.arrayContaining(storageIssues));
- });
-});
-
-describe("#any", () => {
- describe("if there are storage issues", () => {
- beforeEach(() => {
- issues.storage = storageIssues;
- });
-
- it("returns true", async () => {
- const client = new IssuesClient(clientsWithIssues);
-
- const result = await client.any();
- expect(result).toEqual(true);
- });
- });
-
- describe("if there are no issues", () => {
- beforeEach(() => {
- issues.storage = [];
- });
-
- it("returns false", async () => {
- const client = new IssuesClient(clientsWithIssues);
-
- const result = await client.any();
- expect(result).toEqual(false);
- });
- });
-});
-
-describe("#onIssuesChange", () => {
- it("subscribes to changes in storage issues", () => {
- const client = new IssuesClient(clientsWithIssues);
-
- const handler = jest.fn();
- client.onIssuesChange(handler);
-
- expect(clientsWithIssues.storage.onIssuesChange).toHaveBeenCalledWith(handler);
- });
-});
diff --git a/web/src/components/core/Sidebar.test.jsx b/web/src/components/core/Sidebar.test.jsx
index cf20df357d..44ef3b4357 100644
--- a/web/src/components/core/Sidebar.test.jsx
+++ b/web/src/components/core/Sidebar.test.jsx
@@ -29,17 +29,17 @@ import { createClient } from "~/client";
jest.mock("~/components/core/LogsButton", () => () => LogsButton Mock
);
jest.mock("~/components/software/ChangeProductLink", () => () => ChangeProductLink Mock
);
-let hasIssues = false;
+let mockIssues;
jest.mock("~/client");
beforeEach(() => {
+ mockIssues = [];
+
createClient.mockImplementation(() => {
return {
- issues: {
- any: () => Promise.resolve(hasIssues),
- onIssuesChange: jest.fn()
- }
+ issues: jest.fn().mockResolvedValue(mockIssues),
+ onIssuesChange: jest.fn()
};
});
});
@@ -137,7 +137,13 @@ describe("onClick bubbling", () => {
describe("if there are issues", () => {
beforeEach(() => {
- hasIssues = true;
+ mockIssues = {
+ software: [
+ {
+ description: "software issue 1", details: "Details 1", source: "system", severity: "warn"
+ }
+ ]
+ };
});
it("includes a notification mark", async () => {
@@ -149,7 +155,7 @@ describe("if there are issues", () => {
describe("if there are not issues", () => {
beforeEach(() => {
- hasIssues = false;
+ mockIssues = [];
});
it("does not include a notification mark", async () => {
diff --git a/web/src/context/notification.jsx b/web/src/context/notification.jsx
index 38446dada1..d04df31486 100644
--- a/web/src/context/notification.jsx
+++ b/web/src/context/notification.jsx
@@ -37,7 +37,8 @@ function NotificationProvider({ children }) {
const load = useCallback(async () => {
if (!client) return;
- const hasIssues = await cancellablePromise(client.issues.any());
+ const issues = await cancellablePromise(client.issues());
+ const hasIssues = Object.values(issues).flat().length > 0;
update({ issues: hasIssues });
}, [client, cancellablePromise, update]);
@@ -45,7 +46,7 @@ function NotificationProvider({ children }) {
if (!client) return;
load();
- return client.issues.onIssuesChange(load);
+ return client.onIssuesChange(load);
}, [client, load]);
const value = [state, update];
From bd54a8d318161f5e0372457e28e3471dd29636ef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Thu, 26 Oct 2023 17:13:54 +0100
Subject: [PATCH 76/97] [web] Show product and software issues
---
web/src/client/software.js | 2 +-
web/src/components/core/IssuesPage.jsx | 94 ++++++++++++---------
web/src/components/core/IssuesPage.test.jsx | 65 ++++++++++----
3 files changed, 104 insertions(+), 57 deletions(-)
diff --git a/web/src/client/software.js b/web/src/client/software.js
index 87d2ae3850..bb3157fdc6 100644
--- a/web/src/client/software.js
+++ b/web/src/client/software.js
@@ -48,7 +48,7 @@ const REGISTRATION_IFACE = "org.opensuse.Agama1.Registration";
/**
* @typedef {object} ActionResult
- * @property {boolean} success - Whether the action was successfuly done.
+ * @property {boolean} success - Whether the action was successfully done.
* @property {string} message - Result message.
*/
diff --git a/web/src/components/core/IssuesPage.jsx b/web/src/components/core/IssuesPage.jsx
index 7d137c5fca..f076c6aece 100644
--- a/web/src/components/core/IssuesPage.jsx
+++ b/web/src/components/core/IssuesPage.jsx
@@ -21,17 +21,17 @@
import React, { useCallback, useEffect, useState } from "react";
-import { HelperText, HelperTextItem, Skeleton } from "@patternfly/react-core";
+import { HelperText, HelperTextItem } from "@patternfly/react-core";
import { partition, useCancellablePromise } from "~/utils";
-import { If, Page, Section } from "~/components/core";
+import { If, Page, Section, SectionSkeleton } from "~/components/core";
import { Icon } from "~/components/layout";
import { useInstallerClient } from "~/context/installer";
import { useNotification } from "~/context/notification";
import { _ } from "~/i18n";
/**
- * Renders an issue
+ * Item representing an issue.
* @component
*
* @param {object} props
@@ -58,54 +58,68 @@ const IssueItem = ({ issue }) => {
};
/**
- * Generates a specific section with issues
+ * Generates issue items sorted by severity.
* @component
*
* @param {object} props
* @param {import ("~/client/mixins").Issue[]} props.issues
- * @param {object} props.props
*/
-const IssuesSection = ({ issues, ...props }) => {
- if (issues.length === 0) return null;
-
+const IssueItems = ({ issues = [] }) => {
const sortedIssues = partition(issues, i => i.severity === "error").flat();
- const issueItems = sortedIssues.map((issue, index) => {
+ return sortedIssues.map((issue, index) => {
return ;
});
-
- return (
-
- );
};
/**
- * Generates the sections with issues
+ * Generates the sections with issues.
* @component
*
* @param {object} props
* @param {import ("~/client/issues").ClientsIssues} props.issues
*/
const IssuesSections = ({ issues }) => {
+ const productIssues = issues.product || [];
+ const storageIssues = issues.storage || [];
+ const softwareIssues = issues.software || [];
+
return (
-
+ <>
+ 0}
+ then={
+
+ }
+ />
+ 0}
+ then={
+
+ }
+ />
+ 0}
+ then={
+
+ }
+ />
+ >
);
};
/**
- * Generates the content for each section with issues. If there are no issues, then a success
- * message is shown.
+ * Generates sections with issues. If there are no issues, then a success message is shown.
* @component
*
* @param {object} props
- * @param {import ("~/client/issues").ClientsIssues} props.issues
+ * @param {import ("~/client").Issues} props.issues
*/
const IssuesContent = ({ issues }) => {
const NoIssues = () => {
@@ -130,28 +144,32 @@ const IssuesContent = ({ issues }) => {
};
/**
- * Page to show all issues per section
+ * Page to show all issues.
* @component
*/
export default function IssuesPage() {
const [isLoading, setIsLoading] = useState(true);
- const [issues, setIssues] = useState({});
- const { issues: client } = useInstallerClient();
+ const [issues, setIssues] = useState();
+ const client = useInstallerClient();
const { cancellablePromise } = useCancellablePromise();
- const [, updateNotification] = useNotification();
+ const [notification, updateNotification] = useNotification();
- const loadIssues = useCallback(async () => {
+ const load = useCallback(async () => {
setIsLoading(true);
- const allIssues = await cancellablePromise(client.getAll());
- setIssues(allIssues);
+ const issues = await cancellablePromise(client.issues());
setIsLoading(false);
- updateNotification({ issues: false });
- }, [client, cancellablePromise, setIssues, setIsLoading, updateNotification]);
+ return issues;
+ }, [client, cancellablePromise, setIsLoading]);
+
+ const update = useCallback((issues) => {
+ setIssues(current => ({ ...current, ...issues }));
+ if (notification.issues) updateNotification({ issues: false });
+ }, [notification, setIssues, updateNotification]);
useEffect(() => {
- loadIssues();
- return client.onIssuesChange(loadIssues);
- }, [client, loadIssues]);
+ load().then(update);
+ return client.onIssuesChange(update);
+ }, [client, load, update]);
return (
}
+ then={ }
else={ }
/>
diff --git a/web/src/components/core/IssuesPage.test.jsx b/web/src/components/core/IssuesPage.test.jsx
index 061357e68c..97badf900d 100644
--- a/web/src/components/core/IssuesPage.test.jsx
+++ b/web/src/components/core/IssuesPage.test.jsx
@@ -20,37 +20,43 @@
*/
import React from "react";
-import { screen, within } from "@testing-library/react";
-import { installerRender, withNotificationProvider } from "~/test-utils";
+import { act, screen, waitFor, within } from "@testing-library/react";
+import { installerRender, createCallbackMock, withNotificationProvider } from "~/test-utils";
import { createClient } from "~/client";
import { IssuesPage } from "~/components/core";
jest.mock("~/client");
jest.mock("@patternfly/react-core", () => {
- const original = jest.requireActual("@patternfly/react-core");
-
return {
- ...original,
+ ...jest.requireActual("@patternfly/react-core"),
Skeleton: () => PFSkeleton
};
});
-let issues = {
+const issues = {
+ product: [],
storage: [
- { description: "Issue 1", details: "Details 1", source: "system", severity: "warn" },
- { description: "Issue 2", details: "Details 2", source: "config", severity: "error" }
+ { description: "storage issue 1", details: "Details 1", source: "system", severity: "warn" },
+ { description: "storage issue 2", details: "Details 2", source: "config", severity: "error" }
+ ],
+ software: [
+ { description: "software issue 1", details: "Details 1", source: "system", severity: "warn" }
]
};
+let mockIssues;
+
+let mockOnIssuesChange;
+
beforeEach(() => {
+ mockIssues = { ...issues };
+ mockOnIssuesChange = jest.fn();
+
createClient.mockImplementation(() => {
return {
- issues: {
- any: () => Promise.resolve(true),
- getAll: () => Promise.resolve(issues),
- onIssuesChange: jest.fn()
- }
+ issues: jest.fn().mockResolvedValue(mockIssues),
+ onIssuesChange: mockOnIssuesChange
};
});
});
@@ -59,20 +65,25 @@ it("loads the issues", async () => {
installerRender(withNotificationProvider( ));
screen.getAllByText(/PFSkeleton/);
- await screen.findByText(/Issue 1/);
+ await screen.findByText(/storage issue 1/);
});
it("renders sections with issues", async () => {
installerRender(withNotificationProvider( ));
- const section = await screen.findByRole("region", { name: "Storage" });
- within(section).findByText(/Issue 1/);
- within(section).findByText(/Issue 2/);
+ await waitFor(() => expect(screen.queryByText("Product")).not.toBeInTheDocument());
+
+ const storageSection = await screen.findByText(/Storage/);
+ within(storageSection).findByText(/storage issue 1/);
+ within(storageSection).findByText(/storage issue 2/);
+
+ const softwareSection = await screen.findByText(/Software/);
+ within(softwareSection).findByText(/software issue 1/);
});
describe("if there are not issues", () => {
beforeEach(() => {
- issues = { storage: [] };
+ mockIssues = { product: [], storage: [], software: [] };
});
it("renders a success message", async () => {
@@ -81,3 +92,21 @@ describe("if there are not issues", () => {
await screen.findByText(/No issues found/);
});
});
+
+describe("if the issues change", () => {
+ it("shows the new issues", async () => {
+ const [mockFunction, callbacks] = createCallbackMock();
+ mockOnIssuesChange = mockFunction;
+
+ installerRender(withNotificationProvider( ));
+
+ await screen.findByText("Storage");
+
+ mockIssues.storage = [];
+ act(() => callbacks.forEach(c => c({ storage: mockIssues.storage })));
+
+ await waitFor(() => expect(screen.queryByText("Storage")).not.toBeInTheDocument());
+ const softwareSection = await screen.findByText(/Software/);
+ within(softwareSection).findByText(/software issue 1/);
+ });
+});
From 59ca28e3ca4fd26e41156d8dab941400a440598e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 27 Oct 2023 10:20:05 +0100
Subject: [PATCH 77/97] [web] Adapt to changes in master
---
web/src/App.jsx | 4 ++--
web/src/App.test.jsx | 6 +++---
web/src/context/product.jsx | 2 +-
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/web/src/App.jsx b/web/src/App.jsx
index c02367ddd4..0e162ca045 100644
--- a/web/src/App.jsx
+++ b/web/src/App.jsx
@@ -24,7 +24,7 @@ import { Outlet } from "react-router-dom";
import { _ } from "~/i18n";
import { useInstallerClient, useInstallerClientStatus } from "~/context/installer";
-import { useSoftware } from "./context/software";
+import { useProduct } from "./context/product";
import { STARTUP, INSTALL } from "~/client/phase";
import { BUSY } from "~/client/status";
@@ -57,7 +57,7 @@ const ATTEMPTS = 3;
function App() {
const client = useInstallerClient();
const { attempt } = useInstallerClientStatus();
- const { products } = useSoftware();
+ const { products } = useProduct();
const { language } = useL10n();
const [status, setStatus] = useState(undefined);
const [phase, setPhase] = useState(undefined);
diff --git a/web/src/App.test.jsx b/web/src/App.test.jsx
index 84092633b6..59b0753521 100644
--- a/web/src/App.test.jsx
+++ b/web/src/App.test.jsx
@@ -31,9 +31,9 @@ jest.mock("~/client");
// list of available products
let mockProducts;
-jest.mock("~/context/software", () => ({
- ...jest.requireActual("~/context/software"),
- useSoftware: () => {
+jest.mock("~/context/product", () => ({
+ ...jest.requireActual("~/context/product"),
+ useProduct: () => {
return {
products: mockProducts,
selectedProduct: null
diff --git a/web/src/context/product.jsx b/web/src/context/product.jsx
index 1d79959fdd..4f476f710a 100644
--- a/web/src/context/product.jsx
+++ b/web/src/context/product.jsx
@@ -72,7 +72,7 @@ function useProduct() {
}
const { products = [], selectedId } = context;
- const selectedProduct = products.find(p => p.id === selectedId);
+ const selectedProduct = products.find(p => p.id === selectedId) || null;
return { ...context, selectedProduct };
}
From 7735acee6361189712009d1a9c929d6a3b6ffa00 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 27 Oct 2023 14:25:59 +0100
Subject: [PATCH 78/97] [web] Extract product selection form to separate
component
---
.../product/ProductSelectionForm.jsx | 73 +++++++++++++++
.../product/ProductSelectionForm.test.jsx | 91 +++++++++++++++++++
web/src/components/product/index.js | 22 +++++
.../software/ProductSelectionPage.jsx | 66 +++-----------
4 files changed, 200 insertions(+), 52 deletions(-)
create mode 100644 web/src/components/product/ProductSelectionForm.jsx
create mode 100644 web/src/components/product/ProductSelectionForm.test.jsx
create mode 100644 web/src/components/product/index.js
diff --git a/web/src/components/product/ProductSelectionForm.jsx b/web/src/components/product/ProductSelectionForm.jsx
new file mode 100644
index 0000000000..7e7da0b557
--- /dev/null
+++ b/web/src/components/product/ProductSelectionForm.jsx
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) [2023] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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 { Card, CardBody, Form, FormGroup, Radio } from "@patternfly/react-core";
+
+import { _ } from "~/i18n";
+import { If } from "~/components/core";
+import { noop } from "~/utils";
+import { useProduct } from "~/context/product";
+
+const ProductOptions = ({ value, onOptionClick = noop }) => {
+ const { products } = useProduct();
+
+ const isSelected = (product) => product.id === value;
+
+ const options = products.map((p) => (
+
+
+ onOptionClick(p.id)}
+ />
+
+
+ ));
+
+ return options;
+};
+
+export default function ProductSelectionForm({ id, onSubmit: onSubmitProp = noop }) {
+ const { products, selectedProduct } = useProduct();
+ const [selected, setSelected] = useState(selectedProduct?.id);
+
+ const onSubmit = async (e) => {
+ e.preventDefault();
+ onSubmitProp(selected);
+ };
+
+ return (
+
}
+ else={ }
+ />
+
+
+ );
+}
diff --git a/web/src/components/product/ProductSelectionForm.test.jsx b/web/src/components/product/ProductSelectionForm.test.jsx
new file mode 100644
index 0000000000..8f10946ca4
--- /dev/null
+++ b/web/src/components/product/ProductSelectionForm.test.jsx
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) [2023] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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 { ProductSelectionForm } from "~/components/product";
+import { createClient } from "~/client";
+
+let mockProducts;
+let mockSelectedProduct;
+
+jest.mock("~/client");
+
+jest.mock("~/context/product", () => ({
+ ...jest.requireActual("~/context/product"),
+ useProduct: () => {
+ return {
+ products: mockProducts,
+ selectedProduct: mockSelectedProduct
+ };
+ }
+}));
+
+const products = [
+ {
+ id: "ALP-Dolomite",
+ name: "ALP Dolomite",
+ description: "ALP Dolomite description"
+ },
+ {
+ id: "Tumbleweed",
+ name: "openSUSE Tumbleweed",
+ description: "Tumbleweed description..."
+ },
+ {
+ id: "MicroOS",
+ name: "openSUSE MicroOS",
+ description: "MicroOS description"
+ }
+];
+
+beforeEach(() => {
+ mockProducts = products;
+ mockSelectedProduct = products[0];
+
+ createClient.mockImplementation(() => ({}));
+});
+
+it("shows an option for each product", async () => {
+ installerRender( );
+ await screen.findByRole("radio", { name: "ALP Dolomite" });
+ await screen.findByRole("radio", { name: "openSUSE Tumbleweed" });
+ await screen.findByRole("radio", { name: "openSUSE MicroOS" });
+});
+
+it("selects the current product by default", async () => {
+ installerRender( );
+ await screen.findByRole("radio", { name: "ALP Dolomite", checked: true });
+});
+
+it("selects the clicked product", async () => {
+ const { user } = installerRender( );
+ const radio = await screen.findByRole("radio", { name: "openSUSE Tumbleweed" });
+ await user.click(radio);
+ await screen.findByRole("radio", { name: "openSUSE Tumbleweed", clicked: true });
+});
+
+it("shows a message if there is no product for selection", async () => {
+ mockProducts = [];
+ installerRender( );
+ await screen.findByText(/no products found/i);
+});
diff --git a/web/src/components/product/index.js b/web/src/components/product/index.js
new file mode 100644
index 0000000000..fe40d93538
--- /dev/null
+++ b/web/src/components/product/index.js
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) [2023] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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.
+ */
+
+export { default as ProductSelectionForm } from "./ProductSelectionForm";
diff --git a/web/src/components/software/ProductSelectionPage.jsx b/web/src/components/software/ProductSelectionPage.jsx
index 4b4663aaa6..2d9c3fc724 100644
--- a/web/src/components/software/ProductSelectionPage.jsx
+++ b/web/src/components/software/ProductSelectionPage.jsx
@@ -19,30 +19,21 @@
* find current contact information at www.suse.com.
*/
-import React, { useEffect, useState } from "react";
+import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
-import { useInstallerClient } from "~/context/installer";
-import { useProduct } from "~/context/product";
-import { _ } from "~/i18n";
-
-import {
- Button,
- Card,
- CardBody,
- Form,
- FormGroup,
- Radio
-} from "@patternfly/react-core";
+import { Button } from "@patternfly/react-core";
+import { _ } from "~/i18n";
import { Icon, Loading } from "~/components/layout";
+import { ProductSelectionForm } from "~/components/product";
import { Title, PageIcon, MainActions } from "~/components/layout/Layout";
+import { useInstallerClient } from "~/context/installer";
+import { useProduct } from "~/context/product";
function ProductSelectionPage() {
- const { software, manager } = useInstallerClient();
+ const { manager, software } = useInstallerClient();
const navigate = useNavigate();
- const { products, selectedProduct } = useProduct();
- const previous = selectedProduct?.id;
- const [selected, setSelected] = useState(selectedProduct?.id);
+ const { selectedProduct, products } = useProduct();
useEffect(() => {
// TODO: display a notification in the UI to emphasizes that
@@ -50,18 +41,13 @@ function ProductSelectionPage() {
return software.product.onChange(() => navigate("/"));
}, [software, navigate]);
- const isSelected = p => p.id === selected;
-
- const accept = async (e) => {
- e.preventDefault();
- if (selected === previous) {
- navigate("/");
- return;
+ const onSubmit = async (id) => {
+ if (id !== selectedProduct?.id) {
+ // TODO: handle errors
+ await software.product.select(id);
+ manager.startProbing();
}
- // TODO: handle errors
- await software.product.select(selected);
- manager.startProbing();
navigate("/");
};
@@ -69,25 +55,6 @@ function ProductSelectionPage() {
);
- const buildOptions = () => {
- const options = products.map((p) => (
-
-
- setSelected(p.id)}
- />
-
-
- ));
-
- return options;
- };
-
return (
<>
{/* TRANSLATORS: page header */}
@@ -99,12 +66,7 @@ function ProductSelectionPage() {
{_("Select")}
-
-
+
>
);
}
From 825b5e3ee65948162ce16bd5cb00bb69182c2e8a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 27 Oct 2023 15:17:09 +0100
Subject: [PATCH 79/97] [web] Move product selection to products components
---
.../components/{software => product}/ProductSelectionPage.jsx | 0
.../{software => product}/ProductSelectionPage.test.jsx | 2 +-
web/src/components/product/index.js | 1 +
web/src/components/software/index.js | 1 -
web/src/index.js | 3 ++-
5 files changed, 4 insertions(+), 3 deletions(-)
rename web/src/components/{software => product}/ProductSelectionPage.jsx (100%)
rename web/src/components/{software => product}/ProductSelectionPage.test.jsx (97%)
diff --git a/web/src/components/software/ProductSelectionPage.jsx b/web/src/components/product/ProductSelectionPage.jsx
similarity index 100%
rename from web/src/components/software/ProductSelectionPage.jsx
rename to web/src/components/product/ProductSelectionPage.jsx
diff --git a/web/src/components/software/ProductSelectionPage.test.jsx b/web/src/components/product/ProductSelectionPage.test.jsx
similarity index 97%
rename from web/src/components/software/ProductSelectionPage.test.jsx
rename to web/src/components/product/ProductSelectionPage.test.jsx
index b3abb5916a..89a943a16e 100644
--- a/web/src/components/software/ProductSelectionPage.test.jsx
+++ b/web/src/components/product/ProductSelectionPage.test.jsx
@@ -22,7 +22,7 @@
import React from "react";
import { screen } from "@testing-library/react";
import { installerRender, mockNavigateFn } from "~/test-utils";
-import { ProductSelectionPage } from "~/components/software";
+import { ProductSelectionPage } from "~/components/product";
import { createClient } from "~/client";
const products = [
diff --git a/web/src/components/product/index.js b/web/src/components/product/index.js
index fe40d93538..e3bd2cbde7 100644
--- a/web/src/components/product/index.js
+++ b/web/src/components/product/index.js
@@ -20,3 +20,4 @@
*/
export { default as ProductSelectionForm } from "./ProductSelectionForm";
+export { default as ProductSelectionPage } from "./ProductSelectionPage";
diff --git a/web/src/components/software/index.js b/web/src/components/software/index.js
index b2c6fef467..84f7a58f05 100644
--- a/web/src/components/software/index.js
+++ b/web/src/components/software/index.js
@@ -19,7 +19,6 @@
* find current contact information at www.suse.com.
*/
-export { default as ProductSelectionPage } from "./ProductSelectionPage";
export { default as ChangeProductLink } from "./ChangeProductLink";
export { default as PatternSelector } from "./PatternSelector";
export { default as UsedSize } from "./UsedSize";
diff --git a/web/src/index.js b/web/src/index.js
index 3b7b6815f3..f47f5a76b5 100644
--- a/web/src/index.js
+++ b/web/src/index.js
@@ -37,7 +37,8 @@ import App from "~/App";
import Main from "~/Main";
import DevServerWrapper from "~/DevServerWrapper";
import { Overview } from "~/components/overview";
-import { ProductSelectionPage, SoftwarePage } from "~/components/software";
+import { ProductSelectionPage } from "~/components/product";
+import { SoftwarePage } from "~/components/software";
import { ProposalPage as StoragePage, ISCSIPage, DASDPage, ZFCPPage } from "~/components/storage";
import { UsersPage } from "~/components/users";
import { L10nPage } from "~/components/l10n";
From e3122a4c9ddcfac73a5acde9440aa705e65f2211 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Mon, 30 Oct 2023 12:50:37 +0000
Subject: [PATCH 80/97] [web] Create ProductSelector instead of form
---
.../product/ProductSelectionForm.jsx | 73 -------------------
.../product/ProductSelectionPage.jsx | 25 ++++---
.../components/product/ProductSelector.jsx | 54 ++++++++++++++
...Form.test.jsx => ProductSelector.test.jsx} | 38 +++-------
web/src/components/product/index.js | 2 +-
5 files changed, 82 insertions(+), 110 deletions(-)
delete mode 100644 web/src/components/product/ProductSelectionForm.jsx
create mode 100644 web/src/components/product/ProductSelector.jsx
rename web/src/components/product/{ProductSelectionForm.test.jsx => ProductSelector.test.jsx} (69%)
diff --git a/web/src/components/product/ProductSelectionForm.jsx b/web/src/components/product/ProductSelectionForm.jsx
deleted file mode 100644
index 7e7da0b557..0000000000
--- a/web/src/components/product/ProductSelectionForm.jsx
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (c) [2023] SUSE LLC
- *
- * All Rights Reserved.
- *
- * This program is free software; you can redistribute it and/or modify it
- * under the terms of version 2 of the GNU General Public License as published
- * by the Free Software Foundation.
- *
- * 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 { Card, CardBody, Form, FormGroup, Radio } from "@patternfly/react-core";
-
-import { _ } from "~/i18n";
-import { If } from "~/components/core";
-import { noop } from "~/utils";
-import { useProduct } from "~/context/product";
-
-const ProductOptions = ({ value, onOptionClick = noop }) => {
- const { products } = useProduct();
-
- const isSelected = (product) => product.id === value;
-
- const options = products.map((p) => (
-
-
- onOptionClick(p.id)}
- />
-
-
- ));
-
- return options;
-};
-
-export default function ProductSelectionForm({ id, onSubmit: onSubmitProp = noop }) {
- const { products, selectedProduct } = useProduct();
- const [selected, setSelected] = useState(selectedProduct?.id);
-
- const onSubmit = async (e) => {
- e.preventDefault();
- onSubmitProp(selected);
- };
-
- return (
-
- );
-}
diff --git a/web/src/components/product/ProductSelectionPage.jsx b/web/src/components/product/ProductSelectionPage.jsx
index 2d9c3fc724..585747a9db 100644
--- a/web/src/components/product/ProductSelectionPage.jsx
+++ b/web/src/components/product/ProductSelectionPage.jsx
@@ -19,13 +19,13 @@
* find current contact information at www.suse.com.
*/
-import React, { useEffect } from "react";
+import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
-import { Button } from "@patternfly/react-core";
+import { Button, Form, FormGroup } from "@patternfly/react-core";
import { _ } from "~/i18n";
import { Icon, Loading } from "~/components/layout";
-import { ProductSelectionForm } from "~/components/product";
+import { ProductSelector } from "~/components/product";
import { Title, PageIcon, MainActions } from "~/components/layout/Layout";
import { useInstallerClient } from "~/context/installer";
import { useProduct } from "~/context/product";
@@ -33,7 +33,8 @@ import { useProduct } from "~/context/product";
function ProductSelectionPage() {
const { manager, software } = useInstallerClient();
const navigate = useNavigate();
- const { selectedProduct, products } = useProduct();
+ const { products, selectedProduct } = useProduct();
+ const [newProductId, setNewProductId] = useState(selectedProduct?.id);
useEffect(() => {
// TODO: display a notification in the UI to emphasizes that
@@ -41,10 +42,12 @@ function ProductSelectionPage() {
return software.product.onChange(() => navigate("/"));
}, [software, navigate]);
- const onSubmit = async (id) => {
- if (id !== selectedProduct?.id) {
+ const onSubmit = async (e) => {
+ e.preventDefault();
+
+ if (newProductId !== selectedProduct?.id) {
// TODO: handle errors
- await software.product.select(id);
+ await software.product.select(newProductId);
manager.startProbing();
}
@@ -61,12 +64,16 @@ function ProductSelectionPage() {
{_("Product selection")}
-
+
{/* TRANSLATORS: button label */}
{_("Select")}
-
+
>
);
}
diff --git a/web/src/components/product/ProductSelector.jsx b/web/src/components/product/ProductSelector.jsx
new file mode 100644
index 0000000000..05b2842bf6
--- /dev/null
+++ b/web/src/components/product/ProductSelector.jsx
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) [2023] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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 { Card, CardBody, Radio } from "@patternfly/react-core";
+
+import { _ } from "~/i18n";
+import { If } from "~/components/core";
+import { noop } from "~/utils";
+
+export default function ProductSelector({ value, products = [], onChange = noop }) {
+ const isSelected = (product) => product.id === value;
+
+ return (
+ {_("No products available for selection")}}
+ else={
+ products.map((p) => (
+
+
+ onChange(p.id)}
+ />
+
+
+ ))
+ }
+ />
+ );
+}
diff --git a/web/src/components/product/ProductSelectionForm.test.jsx b/web/src/components/product/ProductSelector.test.jsx
similarity index 69%
rename from web/src/components/product/ProductSelectionForm.test.jsx
rename to web/src/components/product/ProductSelector.test.jsx
index 8f10946ca4..b40af415a0 100644
--- a/web/src/components/product/ProductSelectionForm.test.jsx
+++ b/web/src/components/product/ProductSelector.test.jsx
@@ -22,24 +22,11 @@
import React from "react";
import { screen } from "@testing-library/react";
import { installerRender } from "~/test-utils";
-import { ProductSelectionForm } from "~/components/product";
+import { ProductSelector } from "~/components/product";
import { createClient } from "~/client";
-let mockProducts;
-let mockSelectedProduct;
-
jest.mock("~/client");
-jest.mock("~/context/product", () => ({
- ...jest.requireActual("~/context/product"),
- useProduct: () => {
- return {
- products: mockProducts,
- selectedProduct: mockSelectedProduct
- };
- }
-}));
-
const products = [
{
id: "ALP-Dolomite",
@@ -59,33 +46,30 @@ const products = [
];
beforeEach(() => {
- mockProducts = products;
- mockSelectedProduct = products[0];
-
createClient.mockImplementation(() => ({}));
});
it("shows an option for each product", async () => {
- installerRender( );
+ installerRender( );
await screen.findByRole("radio", { name: "ALP Dolomite" });
await screen.findByRole("radio", { name: "openSUSE Tumbleweed" });
await screen.findByRole("radio", { name: "openSUSE MicroOS" });
});
-it("selects the current product by default", async () => {
- installerRender( );
- await screen.findByRole("radio", { name: "ALP Dolomite", checked: true });
+it("selects the given value", async () => {
+ installerRender( );
+ await screen.findByRole("radio", { name: "openSUSE Tumbleweed", clicked: true });
});
-it("selects the clicked product", async () => {
- const { user } = installerRender( );
+it("calls onChange if a new option is clicked", async () => {
+ const onChangeFn = jest.fn();
+ const { user } = installerRender( );
const radio = await screen.findByRole("radio", { name: "openSUSE Tumbleweed" });
await user.click(radio);
- await screen.findByRole("radio", { name: "openSUSE Tumbleweed", clicked: true });
+ expect(onChangeFn).toHaveBeenCalledWith("Tumbleweed");
});
it("shows a message if there is no product for selection", async () => {
- mockProducts = [];
- installerRender( );
- await screen.findByText(/no products found/i);
+ installerRender( );
+ await screen.findByText(/no products available/i);
});
diff --git a/web/src/components/product/index.js b/web/src/components/product/index.js
index e3bd2cbde7..12e0a814b0 100644
--- a/web/src/components/product/index.js
+++ b/web/src/components/product/index.js
@@ -19,5 +19,5 @@
* find current contact information at www.suse.com.
*/
-export { default as ProductSelectionForm } from "./ProductSelectionForm";
export { default as ProductSelectionPage } from "./ProductSelectionPage";
+export { default as ProductSelector } from "./ProductSelector";
From a483046cf72acaa0449f6d7422c0578cf5dd9f3e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Mon, 30 Oct 2023 12:52:07 +0000
Subject: [PATCH 81/97] [web] Change icon for summary
---
web/src/components/layout/Icon.jsx | 4 ++--
web/src/components/overview/Overview.jsx | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/web/src/components/layout/Icon.jsx b/web/src/components/layout/Icon.jsx
index 57ff9259dd..42c32748af 100644
--- a/web/src/components/layout/Icon.jsx
+++ b/web/src/components/layout/Icon.jsx
@@ -44,6 +44,7 @@ import HomeStorage from "@icons/home_storage.svg?component";
import Info from "@icons/info.svg?component";
import Inventory from "@icons/inventory_2.svg?component";
import Lan from "@icons/lan.svg?component";
+import ListAlt from "@icons/list_alt.svg?component";
import Lock from "@icons/lock.svg?component";
import ManageAccounts from "@icons/manage_accounts.svg?component";
import Menu from "@icons/menu.svg?component";
@@ -57,7 +58,6 @@ import SettingsEthernet from "@icons/settings_ethernet.svg?component";
import SettingsFill from "@icons/settings-fill.svg?component";
import SignalCellularAlt from "@icons/signal_cellular_alt.svg?component";
import Storage from "@icons/storage.svg?component";
-import Summarize from "@icons/summarize.svg?component";
import TaskAlt from "@icons/task_alt.svg?component";
import Terminal from "@icons/terminal.svg?component";
import Translate from "@icons/translate.svg?component";
@@ -98,6 +98,7 @@ const icons = {
inventory_2: Inventory,
lan: Lan,
loading: Loading,
+ list_alt: ListAlt,
lock: Lock,
manage_accounts: ManageAccounts,
menu: Menu,
@@ -111,7 +112,6 @@ const icons = {
settings_ethernet: SettingsEthernet,
signal_cellular_alt: SignalCellularAlt,
storage: Storage,
- summarize: Summarize,
task_alt: TaskAlt,
terminal: Terminal,
translate: Translate,
diff --git a/web/src/components/overview/Overview.jsx b/web/src/components/overview/Overview.jsx
index 205f82e576..eebce05b48 100644
--- a/web/src/components/overview/Overview.jsx
+++ b/web/src/components/overview/Overview.jsx
@@ -45,7 +45,7 @@ function Overview() {
setShowErrors(true)} />}
>
From 3bf7a232e5633c5bf74ed69e53615996ebe330a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Thu, 2 Nov 2023 10:30:21 +0000
Subject: [PATCH 82/97] [web] Fix method for getting the registration
---
web/src/client/software.js | 16 ++++++++++------
web/src/client/software.test.js | 29 ++++++++++++++++++++++++-----
2 files changed, 34 insertions(+), 11 deletions(-)
diff --git a/web/src/client/software.js b/web/src/client/software.js
index bb3157fdc6..2bb678446c 100644
--- a/web/src/client/software.js
+++ b/web/src/client/software.js
@@ -40,10 +40,10 @@ const REGISTRATION_IFACE = "org.opensuse.Agama1.Registration";
/**
* @typedef {object} Registration
- * @property {string} code - Registration code.
- * @property {string} email - Registration email.
* @property {string} requirement - Registration requirement (i.e., "not-required, "optional",
* "mandatory").
+ * @property {string|null} code - Registration code, if any.
+ * @property {string|null} email - Registration email, if any.
*/
/**
@@ -118,15 +118,19 @@ class BaseProductManager {
/**
* Returns the registration of the selected product.
*
- * @return {Promise}
+ * @return {Promise}
*/
async getRegistration() {
const proxy = await this.client.proxy(REGISTRATION_IFACE, PRODUCT_PATH);
+ const requirement = this.registrationRequirement(proxy.Requirement);
const code = proxy.RegCode;
const email = proxy.Email;
- const requirement = this.registrationRequirement(proxy.Requirement);
- return (code.length === 0 ? null : { code, email, requirement });
+ const registration = { requirement, code, email };
+ if (code.length === 0) registration.code = null;
+ if (email.length === 0) registration.email = null;
+
+ return registration;
}
/**
@@ -138,7 +142,7 @@ class BaseProductManager {
*/
async register(code, email = "") {
const proxy = await this.client.proxy(REGISTRATION_IFACE, PRODUCT_PATH);
- const result = await proxy.Register(code, { email });
+ const result = await proxy.Register(code, { Email: { t: "s", v: email } });
return {
success: result[0] === 0,
diff --git a/web/src/client/software.test.js b/web/src/client/software.test.js
index b04d34206b..6c68a69b4e 100644
--- a/web/src/client/software.test.js
+++ b/web/src/client/software.test.js
@@ -75,26 +75,32 @@ describe("#product", () => {
});
describe("#getRegistration", () => {
- describe("if there is no registration code", () => {
+ describe("if there the product is not registered yet", () => {
beforeEach(() => {
registrationProxy.RegCode = "";
+ registrationProxy.Email = "";
+ registrationProxy.Requirement = 1;
});
- it("returns null", async () => {
+ it("returns the expected registration", async () => {
const client = new SoftwareClient();
const registration = await client.product.getRegistration();
- expect(registration).toBeNull();
+ expect(registration).toStrictEqual({
+ code: null,
+ email: null,
+ requirement: "optional"
+ });
});
});
- describe("if there is registration code", () => {
+ describe("if the product is registered", () => {
beforeEach(() => {
registrationProxy.RegCode = "111222";
registrationProxy.Email = "test@test.com";
registrationProxy.Requirement = 2;
});
- it("returns the registration", async () => {
+ it("returns the expected registration", async () => {
const client = new SoftwareClient();
const registration = await client.product.getRegistration();
expect(registration).toStrictEqual({
@@ -107,6 +113,19 @@ describe("#product", () => {
});
describe("#register", () => {
+ beforeEach(() => {
+ registrationProxy.Register = jest.fn().mockResolvedValue([0, ""]);
+ });
+
+ it("performs the expected D-Bus call", async () => {
+ const client = new SoftwareClient();
+ await client.product.register("111222", "test@test.com");
+ expect(registrationProxy.Register).toHaveBeenCalledWith(
+ "111222",
+ { Email: { t: "s", v: "test@test.com" } }
+ );
+ });
+
describe("when the action is correctly done", () => {
beforeEach(() => {
registrationProxy.Register = jest.fn().mockResolvedValue([0, ""]);
From 5738769f9694150262aaae11082924d0625e1abc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Thu, 2 Nov 2023 13:38:50 +0000
Subject: [PATCH 83/97] [web] Add EmailInput component
---
web/src/components/core/EmailInput.jsx | 86 +++++++++++++++++++
web/src/components/core/EmailInput.test.jsx | 92 +++++++++++++++++++++
web/src/components/core/index.js | 1 +
3 files changed, 179 insertions(+)
create mode 100644 web/src/components/core/EmailInput.jsx
create mode 100644 web/src/components/core/EmailInput.test.jsx
diff --git a/web/src/components/core/EmailInput.jsx b/web/src/components/core/EmailInput.jsx
new file mode 100644
index 0000000000..dc84711157
--- /dev/null
+++ b/web/src/components/core/EmailInput.jsx
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) [2023] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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, { useEffect, useState } from "react";
+import { InputGroup, TextInput } from "@patternfly/react-core";
+import { noop } from "~/utils";
+
+/**
+ * Email validation.
+ *
+ * Code inspired by https://github.com/manishsaraan/email-validator/blob/master/index.js
+ *
+ * @param {string} email
+ * @returns {boolean}
+ */
+const validateEmail = (email) => {
+ const regexp = /^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/;
+
+ const validateFormat = (email) => {
+ const parts = email.split('@');
+
+ return parts.length === 2 && regexp.test(email);
+ };
+
+ const validateSizes = (email) => {
+ const [account, address] = email.split('@');
+
+ if (account.length > 64) return false;
+ if (address.length > 255) return false;
+
+ const domainParts = address.split('.');
+
+ if (domainParts.find(p => p.length > 63)) return false;
+
+ return true;
+ };
+
+ return validateFormat(email) && validateSizes(email);
+};
+
+/**
+ * Renders an email input field which validates its value.
+ * @component
+ *
+ * @param {(boolean) => void} onValidate - Callback to be called every time the input value is
+ * validated.
+ * @param {Object} props - Props matching the {@link https://www.patternfly.org/components/forms/text-input PF/TextInput},
+ * except `type` and `validated` which are managed by the component.
+ */
+export default function EmailInput({ onValidate = noop, ...props }) {
+ const [isValid, setIsValid] = useState(true);
+
+ useEffect(() => {
+ const isValid = props.value.length === 0 || validateEmail(props.value);
+ setIsValid(isValid);
+ onValidate(isValid);
+ }, [onValidate, props.value, setIsValid]);
+
+ return (
+
+
+
+ );
+}
diff --git a/web/src/components/core/EmailInput.test.jsx b/web/src/components/core/EmailInput.test.jsx
new file mode 100644
index 0000000000..5834ca8a21
--- /dev/null
+++ b/web/src/components/core/EmailInput.test.jsx
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) [2023] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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 { screen } from "@testing-library/react";
+
+import EmailInput from "./EmailInput";
+import { plainRender } from "~/test-utils";
+
+describe("EmailInput component", () => {
+ it("renders an email input", () => {
+ plainRender(
+
+ );
+
+ const inputField = screen.getByLabelText("User email");
+ expect(inputField).toHaveAttribute("type", "email");
+ });
+
+ // Using a controlled component for testing the rendered result instead of testing if
+ // the given onChange callback is called. The former is more aligned with the
+ // React Testing Library principles, https://testing-library.com/docs/guiding-principles/
+ const EmailInputTest = (props) => {
+ const [email, setEmail] = useState("");
+ const [isValid, setIsValid] = useState(true);
+
+ return (
+ <>
+ setEmail(v)}
+ onValidate={setIsValid}
+ />
+ {email && Email value updated!
}
+ {isValid === false && Email is not valid!
}
+ >
+ );
+ };
+
+ it("triggers onChange callback", async () => {
+ const { user } = plainRender( );
+ const emailInput = screen.getByLabelText("Test email");
+
+ expect(screen.queryByText("Email value updated!")).toBeNull();
+
+ await user.type(emailInput, "test@test.com");
+ screen.getByText("Email value updated!");
+ });
+
+ it("triggers onValidate callback", async () => {
+ const { user } = plainRender( );
+ const emailInput = screen.getByLabelText("Test email");
+
+ expect(screen.queryByText("Email is not valid!")).toBeNull();
+
+ await user.type(emailInput, "foo");
+ await screen.findByText("Email is not valid!");
+ });
+
+ it("marks the input as invalid if the value is not a valid email", async () => {
+ const { user } = plainRender( );
+ const emailInput = screen.getByLabelText("Test email");
+
+ await user.type(emailInput, "foo");
+
+ expect(emailInput).toHaveAttribute("aria-invalid");
+ });
+});
diff --git a/web/src/components/core/index.js b/web/src/components/core/index.js
index d457f49889..73593f96ed 100644
--- a/web/src/components/core/index.js
+++ b/web/src/components/core/index.js
@@ -30,6 +30,7 @@ export { default as FormLabel } from "./FormLabel";
export { default as FormValidationError } from "./FormValidationError";
export { default as Fieldset } from "./Fieldset";
export { default as Em } from "./Em";
+export { default as EmailInput } from "./EmailInput";
export { default as If } from "./If";
export { default as Installation } from "./Installation";
export { default as InstallationFinished } from "./InstallationFinished";
From 85caaefd51b8d3b45c308b3f62c479681dc51440 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 3 Nov 2023 09:46:03 +0000
Subject: [PATCH 84/97] [web] Some code improvements
---
.../components/overview/ProductSection.jsx | 20 +++++++++++--------
.../components/overview/SoftwareSection.jsx | 14 +++++--------
.../components/software/PatternSelector.jsx | 3 ++-
3 files changed, 19 insertions(+), 18 deletions(-)
diff --git a/web/src/components/overview/ProductSection.jsx b/web/src/components/overview/ProductSection.jsx
index 8b95d792af..1c4ebd89b5 100644
--- a/web/src/components/overview/ProductSection.jsx
+++ b/web/src/components/overview/ProductSection.jsx
@@ -21,15 +21,15 @@
import React, { useEffect, useState } from "react";
import { Text } from "@patternfly/react-core";
-import { useCancellablePromise } from "~/utils";
+import { toValidationError, useCancellablePromise } from "~/utils";
import { useInstallerClient } from "~/context/installer";
import { useProduct } from "~/context/product";
-import { Section, SectionSkeleton } from "~/components/core";
+import { If, Section, SectionSkeleton } from "~/components/core";
import { _ } from "~/i18n";
const errorsFrom = (issues) => {
const errors = issues.filter(i => i.severity === "error");
- return errors.map(e => ({ message: e.description }));
+ return errors.map(toValidationError);
};
export default function ProductSection() {
@@ -44,12 +44,16 @@ export default function ProductSection() {
}, [cancellablePromise, setIssues, software]);
const Content = ({ isLoading = false }) => {
- if (isLoading) return ;
-
return (
-
- {selectedProduct?.name}
-
+ }
+ else={
+
+ {selectedProduct?.name}
+
+ }
+ />
);
};
diff --git a/web/src/components/overview/SoftwareSection.jsx b/web/src/components/overview/SoftwareSection.jsx
index fd9867e5e1..fbcd81cee8 100644
--- a/web/src/components/overview/SoftwareSection.jsx
+++ b/web/src/components/overview/SoftwareSection.jsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022] SUSE LLC
+ * Copyright (c) [2022-2023] SUSE LLC
*
* All Rights Reserved.
*
@@ -20,13 +20,13 @@
*/
import React, { useReducer, useEffect } from "react";
+import { BUSY } from "~/client/status";
import { Button } from "@patternfly/react-core";
-import { ProgressText, Section } from "~/components/core";
import { Icon } from "~/components/layout";
+import { ProgressText, Section } from "~/components/core";
+import { toValidationError, useCancellablePromise } from "~/utils";
import { UsedSize } from "~/components/software";
-import { useCancellablePromise } from "~/utils";
import { useInstallerClient } from "~/context/installer";
-import { BUSY } from "~/client/status";
import { _ } from "~/i18n";
const initialState = {
@@ -78,10 +78,6 @@ export default function SoftwareSection({ showErrors }) {
return client.onStatusChange(updateStatus);
}, [client, cancellablePromise]);
- useEffect(() => {
- cancellablePromise(client.getStatus()).then(updateStatus);
- }, [client, cancellablePromise]);
-
useEffect(() => {
const updateProposal = async () => {
const errors = await cancellablePromise(client.getIssues());
@@ -145,7 +141,7 @@ export default function SoftwareSection({ showErrors }) {
title={_("Software")}
icon="apps"
loading={state.busy}
- errors={errors.map(e => ({ message: e.description }))}
+ errors={errors.map(toValidationError)}
path="/software"
>
diff --git a/web/src/components/software/PatternSelector.jsx b/web/src/components/software/PatternSelector.jsx
index 4a64cb551d..30b550a947 100644
--- a/web/src/components/software/PatternSelector.jsx
+++ b/web/src/components/software/PatternSelector.jsx
@@ -27,6 +27,7 @@ import { useInstallerClient } from "~/context/installer";
import { Section, ValidationErrors } from "~/components/core";
import PatternGroup from "./PatternGroup";
import PatternItem from "./PatternItem";
+import { toValidationError } from "~/utils";
import UsedSize from "./UsedSize";
import { _ } from "~/i18n";
@@ -207,7 +208,7 @@ function PatternSelector() {
// FIXME: ValidationErrors should be replaced by an equivalent component to show issues.
// Note that only the Users client uses the old Validation D-Bus interface.
- const validationErrors = errors.map(e => ({ message: e.description }));
+ const validationErrors = errors.map(toValidationError);
return (
<>
From 403cdde155f7d46bbb071218e9c5426d0c1ce06f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 3 Nov 2023 09:46:50 +0000
Subject: [PATCH 85/97] [web] Add product page
---
.../components/overview/ProductSection.jsx | 2 +-
web/src/components/product/ProductPage.jsx | 435 ++++++++++++++++++
.../components/product/ProductPage.test.jsx | 382 +++++++++++++++
.../product/ProductRegistrationForm.jsx | 76 +++
.../product/ProductRegistrationForm.test.jsx | 93 ++++
web/src/components/product/index.js | 2 +
web/src/index.js | 3 +-
7 files changed, 991 insertions(+), 2 deletions(-)
create mode 100644 web/src/components/product/ProductPage.jsx
create mode 100644 web/src/components/product/ProductPage.test.jsx
create mode 100644 web/src/components/product/ProductRegistrationForm.jsx
create mode 100644 web/src/components/product/ProductRegistrationForm.test.jsx
diff --git a/web/src/components/overview/ProductSection.jsx b/web/src/components/overview/ProductSection.jsx
index 1c4ebd89b5..935d20ef16 100644
--- a/web/src/components/overview/ProductSection.jsx
+++ b/web/src/components/overview/ProductSection.jsx
@@ -68,7 +68,7 @@ export default function ProductSection() {
icon="inventory_2"
errors={errors}
loading={isLoading}
- path="/products"
+ path="/product"
>
diff --git a/web/src/components/product/ProductPage.jsx b/web/src/components/product/ProductPage.jsx
new file mode 100644
index 0000000000..4814f33231
--- /dev/null
+++ b/web/src/components/product/ProductPage.jsx
@@ -0,0 +1,435 @@
+/*
+ * Copyright (c) [2023] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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.
+ */
+
+// cspell:ignore Deregistration
+
+import React, { useEffect, useState } from "react";
+import { Alert, Button, Form } from "@patternfly/react-core";
+import { sprintf } from "sprintf-js";
+
+import { _ } from "~/i18n";
+import { BUSY } from "~/client/status";
+import { If, Page, Popup, Section } from "~/components/core";
+import { noop, useCancellablePromise } from "~/utils";
+import { ProductRegistrationForm, ProductSelector } from "~/components/product";
+import { useInstallerClient } from "~/context/installer";
+import { useProduct } from "~/context/product";
+
+/**
+ * Popup for selecting a product.
+ * @component
+ *
+ * @param {object} props
+ * @param {boolean} props.isOpen
+ * @param {function} props.onFinish - Callback to be called when the product is correctly selected.
+ * @param {function} props.onCancel - Callback to be called when the product selection is canceled.
+ */
+const ProductSelectionPopup = ({ isOpen = false, onFinish = noop, onCancel = noop }) => {
+ const { manager, software } = useInstallerClient();
+ const { products, selectedProduct } = useProduct();
+ const [newProductId, setNewProductId] = useState(selectedProduct?.id);
+
+ const onSubmit = async (e) => {
+ e.preventDefault();
+
+ if (newProductId !== selectedProduct?.id) {
+ await software.product.select(newProductId);
+ manager.startProbing();
+ }
+
+ onFinish();
+ };
+
+ return (
+
+
+
+
+ {_("Accept")}
+
+
+
+
+ );
+};
+
+/**
+ * Popup for registering a product.
+ * @component
+ *
+ * @param {object} props
+ * @param {boolean} props.isOpen
+ * @param {function} props.onFinish - Callback to be called when the product is correctly
+ * registered.
+ * @param {function} props.onCancel - Callback to be called when the product registration is
+ * canceled.
+ */
+const ProductRegistrationPopup = ({
+ isOpen = false,
+ onFinish = noop,
+ onCancel: onCancelProp = noop
+}) => {
+ const { software } = useInstallerClient();
+ const { selectedProduct } = useProduct();
+ const [isLoading, setIsLoading] = useState(false);
+ const [isFormValid, setIsFormValid] = useState(true);
+ const [error, setError] = useState();
+
+ const onSubmit = async ({ code, email }) => {
+ setIsLoading(true);
+ const result = await software.product.register(code, email);
+ setIsLoading(false);
+ if (result.success) {
+ software.probe();
+ onFinish();
+ } else {
+ setError(result.message);
+ }
+ };
+
+ const onCancel = () => {
+ setError(null);
+ onCancelProp();
+ };
+
+ const isDisabled = isLoading || !isFormValid;
+
+ return (
+
+
+ {error}
+
+ }
+ />
+
+
+
+ {_("Accept")}
+
+
+
+
+ );
+};
+
+/**
+ * Popup to deregister a product.
+ * @component
+ *
+ * @param {object} props
+ * @param {boolean} props.isOpen
+ * @param {function} props.onFinish - Callback to be called when the product is correctly
+ * deregistered.
+ * @param {function} props.onCancel - Callback to be called when the product de-registration is
+ * canceled.
+ */
+const ProductDeregistrationPopup = ({
+ isOpen = false,
+ onFinish = noop,
+ onCancel: onCancelProp = noop
+}) => {
+ const { software } = useInstallerClient();
+ const { selectedProduct } = useProduct();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState();
+
+ const onAccept = async () => {
+ setIsLoading(true);
+ const result = await software.product.deregister();
+ setIsLoading(false);
+ if (result.success) {
+ software.probe();
+ onFinish();
+ } else {
+ setError(result.message);
+ }
+ };
+
+ const onCancel = () => {
+ setError(null);
+ onCancelProp();
+ };
+
+ return (
+
+
+ {error}
+
+ }
+ />
+
+ {sprintf(_("Do you want to deregister %s?"), selectedProduct.name)}
+
+
+
+ {_("Accept")}
+
+
+
+
+ );
+};
+
+/**
+ * Popup to show a warning when there is a registered product.
+ * @component
+ *
+ * @param {object} props
+ * @param {boolean} props.isOpen
+ * @param {function} props.onAccept - Callback to be called when the warning is accepted.
+ */
+const RegisteredWarningPopup = ({ isOpen = false, onAccept = noop }) => {
+ const { selectedProduct } = useProduct();
+
+ return (
+
+
+ {
+ sprintf(
+ _("The product %s must be deregistered before selecting a new product."),
+ selectedProduct.name
+ )
+ }
+
+
+
+ {_("Accept")}
+
+
+
+ );
+};
+
+/**
+ * Buttons for a product that does not require registration.
+ * @component
+ *
+ * @param {object} props
+ * @param {boolean} props.isDisabled
+ */
+const WithoutRegistrationButtons = ({ isDisabled = false }) => {
+ const [isPopupOpen, setIsPopupOpen] = useState(false);
+
+ return (
+ <>
+ setIsPopupOpen(true)}
+ isDisabled={isDisabled}
+ >
+ {_("Change product")}
+
+ setIsPopupOpen(false)}
+ onCancel={() => setIsPopupOpen(false)}
+ />
+ >
+ );
+};
+
+/**
+ * Buttons for a product that is not registered yet.
+ * @component
+ *
+ * @param {object} props
+ * @param {boolean} props.isDisabled
+ */
+const DeregisteredButtons = ({ isDisabled = false }) => {
+ const [isRegistrationPopupOpen, setIsRegistrationPopupOpen] = useState(false);
+ const [isSelectionPopupOpen, setIsSelectionPopupOpen] = useState(false);
+
+ return (
+ <>
+ setIsRegistrationPopupOpen(true)}
+ isDisabled={isDisabled}
+ >
+ {_("Register")}
+
+ setIsSelectionPopupOpen(true)}
+ isDisabled={isDisabled}
+ >
+ {_("Change product")}
+
+ setIsRegistrationPopupOpen(false)}
+ onCancel={() => setIsRegistrationPopupOpen(false)}
+ />
+ setIsSelectionPopupOpen(false)}
+ onCancel={() => setIsSelectionPopupOpen(false)}
+ />
+ >
+ );
+};
+
+/**
+ * Buttons for a product that is already registered.
+ * @component
+ *
+ * @param {object} props
+ * @param {boolean} props.isDisabled
+ */
+const RegisteredButtons = ({ isDisabled = false }) => {
+ const [isDeregistrationPopupOpen, setIsDeregistrationPopupOpen] = useState(false);
+ const [isWarningPopupOpen, setIsWarningPopupOpen] = useState(false);
+
+ return (
+ <>
+ setIsDeregistrationPopupOpen(true)}
+ isDisabled={isDisabled}
+ >
+ {_("Deregister")}
+
+ setIsWarningPopupOpen(true)}
+ isDisabled={isDisabled}
+ >
+ {_("Change product")}
+
+ setIsDeregistrationPopupOpen(false)}
+ onCancel={() => setIsDeregistrationPopupOpen(false)}
+ />
+ setIsWarningPopupOpen(false)}
+ />
+ >
+ );
+};
+
+/**
+ * Renders the actions for the current product.
+ * @component
+ *
+ * @param {object} props
+ * @param {boolean} props.isDisabled
+ */
+const ProductActions = ({ isDisabled = false }) => {
+ const { registration } = useProduct();
+
+ const withRegistration = registration.requirement !== "not-required";
+ const registered = registration.code !== null;
+
+ return (
+ <>
+
+ }
+ else={ }
+ />
+ }
+ else={ }
+ />
+
+ {_("Configuring product. Actions are disabled until the product is configured.")}
+ }
+ />
+ >
+ );
+};
+
+/**
+ * Page for configuring a product.
+ * @component
+ */
+export default function ProductPage() {
+ const [managerStatus, setManagerStatus] = useState();
+ const [softwareStatus, setSoftwareStatus] = useState();
+ const { cancellablePromise } = useCancellablePromise();
+ const { manager, software } = useInstallerClient();
+ const { selectedProduct, registration } = useProduct();
+
+ useEffect(() => {
+ cancellablePromise(manager.getStatus()).then(setManagerStatus);
+ return manager.onStatusChange(setManagerStatus);
+ }, [cancellablePromise, manager]);
+
+ useEffect(() => {
+ cancellablePromise(software.getStatus()).then(setSoftwareStatus);
+ return software.onStatusChange(setSoftwareStatus);
+ }, [cancellablePromise, software]);
+
+ const isLoading = managerStatus === BUSY || softwareStatus === BUSY;
+
+ return (
+
+
+ {selectedProduct.description}
+
+
+ {_("Registration code:")}
+ {registration.code}
+
+
+ {_("Email:")}
+ {registration.email}
+
+ >
+ }
+ />
+
+
+
+ );
+}
diff --git a/web/src/components/product/ProductPage.test.jsx b/web/src/components/product/ProductPage.test.jsx
new file mode 100644
index 0000000000..4443436266
--- /dev/null
+++ b/web/src/components/product/ProductPage.test.jsx
@@ -0,0 +1,382 @@
+/*
+ * Copyright (c) [2023] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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 { act, screen, within } from "@testing-library/react";
+
+import { BUSY } from "~/client/status";
+import { installerRender } from "~/test-utils";
+import { ProductPage } from "~/components/product";
+import { createClient } from "~/client";
+
+let mockManager;
+let mockSoftware;
+let mockRegistration;
+
+const products = [
+ {
+ id: "Test-Product1",
+ name: "Test Product1",
+ description: "Test Product1 description"
+ },
+ {
+ id: "Test-Product2",
+ name: "Test Product2",
+ description: "Test Product2 description"
+ }
+];
+
+const selectedProduct = {
+ id: "Test-Product1",
+ name: "Test Product1",
+ description: "Test Product1 description"
+};
+
+jest.mock("~/client");
+
+jest.mock("~/context/product", () => ({
+ ...jest.requireActual("~/context/product"),
+ useProduct: () => ({ products, selectedProduct, registration: mockRegistration })
+}));
+
+beforeEach(() => {
+ mockManager = {
+ startProbing: jest.fn(),
+ getStatus: jest.fn().mockResolvedValue(),
+ onStatusChange: jest.fn()
+ };
+
+ mockSoftware = {
+ probe: jest.fn(),
+ getStatus: jest.fn().mockResolvedValue(),
+ onStatusChange: jest.fn(),
+ product: {
+ getSelected: selectedProduct.id,
+ select: jest.fn().mockResolvedValue(),
+ onChange: jest.fn()
+ }
+ };
+
+ mockRegistration = {
+ requirement: "not-required",
+ code: null,
+ email: null
+ };
+
+ createClient.mockImplementation(() => (
+ {
+ manager: mockManager,
+ software: mockSoftware
+ }
+ ));
+});
+
+it("renders the product name and description", async () => {
+ installerRender( );
+ await screen.findByText("Test Product1");
+ await screen.findByText("Test Product1 description");
+});
+
+describe("if the product is already registered", () => {
+ beforeEach(() => {
+ mockRegistration.code = "111222";
+ mockRegistration.email = "test@test.com";
+ });
+
+ it("shows the information about the registration", async () => {
+ installerRender( );
+ await screen.findByText("111222");
+ await screen.findByText("test@test.com");
+ });
+});
+
+describe("if the product does not require registration", () => {
+ beforeEach(() => {
+ mockRegistration.requirement = "not-required";
+ });
+
+ it("shows a button to change the product", async () => {
+ installerRender( );
+ await screen.findByRole("button", { name: "Change product" });
+ });
+
+ it("does not show a button to register the product", async () => {
+ installerRender( );
+ expect(screen.queryByRole("button", { name: "Register" })).not.toBeInTheDocument();
+ });
+});
+
+describe("if the product requires registration", () => {
+ beforeEach(() => {
+ mockRegistration.requirement = "required";
+ });
+
+ it("shows a button to change the product", async () => {
+ installerRender( );
+ await screen.findByRole("button", { name: "Change product" });
+ });
+
+ describe("and the product is not registered yet", () => {
+ beforeEach(() => {
+ mockRegistration.code = null;
+ });
+
+ it("shows a button to register the product", async () => {
+ installerRender( );
+ await screen.findByRole("button", { name: "Register" });
+ });
+ });
+
+ describe("and the product is already registered", () => {
+ beforeEach(() => {
+ mockRegistration.code = "11112222";
+ });
+
+ it("shows a button to deregister the product", async () => {
+ installerRender( );
+ await screen.findByRole("button", { name: "Deregister" });
+ });
+ });
+});
+
+describe("when the services are busy", () => {
+ beforeEach(() => {
+ mockRegistration.requirement = "required";
+ mockRegistration.code = null;
+ mockSoftware.getStatus = jest.fn().mockResolvedValue(BUSY);
+ });
+
+ it("shows disabled buttons", async () => {
+ await act(async () => installerRender( ));
+
+ const selectButton = await screen.findByRole("button", { name: "Change product" });
+ const registerButton = screen.getByRole("button", { name: "Register" });
+
+ expect(selectButton).toHaveAttribute("disabled");
+ expect(registerButton).toHaveAttribute("disabled");
+ });
+
+ it("shows a message about configuring product", async () => {
+ await act(async () => installerRender( ));
+
+ screen.getByText(/Actions are disabled until the product is configured/);
+ });
+});
+
+describe("when the button for changing the product is clicked", () => {
+ describe("and the product is not registered", () => {
+ beforeEach(() => {
+ mockRegistration.code = null;
+ });
+
+ it("opens a popup for selecting a new product", async () => {
+ const { user } = installerRender( );
+
+ const button = screen.getByRole("button", { name: "Change product" });
+ await user.click(button);
+
+ const popup = await screen.findByRole("dialog");
+ within(popup).getByText("Choose a product");
+ within(popup).getByRole("radio", { name: "Test Product1" });
+ const radio = within(popup).getByRole("radio", { name: "Test Product2" });
+
+ await user.click(radio);
+ const accept = within(popup).getByRole("button", { name: "Accept" });
+ await user.click(accept);
+
+ expect(mockSoftware.product.select).toHaveBeenCalledWith("Test-Product2");
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ });
+
+ describe("if the popup is canceled", () => {
+ it("closes the popup without selecting a new product", async () => {
+ const { user } = installerRender( );
+
+ const button = screen.getByRole("button", { name: "Change product" });
+ await user.click(button);
+
+ const popup = await screen.findByRole("dialog");
+ const radio = within(popup).getByRole("radio", { name: "Test Product2" });
+
+ await user.click(radio);
+ const cancel = within(popup).getByRole("button", { name: "Cancel" });
+ await user.click(cancel);
+
+ expect(mockSoftware.product.select).not.toHaveBeenCalled();
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ describe("and the product is registered", () => {
+ beforeEach(() => {
+ mockRegistration.requirement = "mandatory";
+ mockRegistration.code = "111222";
+ });
+
+ it("shows a warning", async () => {
+ const { user } = installerRender( );
+
+ const button = screen.getByRole("button", { name: "Change product" });
+ await user.click(button);
+
+ const popup = await screen.findByRole("dialog");
+ within(popup).getByText(/must be deregistered/);
+
+ const accept = within(popup).getByRole("button", { name: "Accept" });
+ await user.click(accept);
+
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ });
+ });
+});
+
+describe("when the button for registering the product is clicked", () => {
+ beforeEach(() => {
+ mockRegistration.requirement = "mandatory";
+ mockRegistration.code = null;
+ mockSoftware.product.register = jest.fn().mockResolvedValue({ success: true });
+ });
+
+ it("opens a popup for registering the product", async () => {
+ const { user } = installerRender( );
+
+ const button = screen.getByRole("button", { name: "Register" });
+ await user.click(button);
+
+ const popup = await screen.findByRole("dialog");
+ within(popup).getByText("Register Test Product1");
+ const codeInput = within(popup).getByLabelText(/Registration code/);
+ const emailInput = within(popup).getByLabelText("Email");
+
+ await user.type(codeInput, "111222");
+ await user.type(emailInput, "test@test.com");
+ const accept = within(popup).getByRole("button", { name: "Accept" });
+ await user.click(accept);
+
+ expect(mockSoftware.product.register).toHaveBeenCalledWith("111222", "test@test.com");
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ });
+
+ describe("if the popup is canceled", () => {
+ it("closes the popup without registering the product", async () => {
+ const { user } = installerRender( );
+
+ const button = screen.getByRole("button", { name: "Register" });
+ await user.click(button);
+
+ const popup = await screen.findByRole("dialog");
+ const cancel = within(popup).getByRole("button", { name: "Cancel" });
+ await user.click(cancel);
+
+ expect(mockSoftware.product.register).not.toHaveBeenCalled();
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ });
+ });
+
+ describe("if there is an error registering the product", () => {
+ beforeEach(() => {
+ mockSoftware.product.register = jest.fn().mockResolvedValue({
+ success: false,
+ message: "Error registering product"
+ });
+ });
+
+ it("does not close the popup and shows the error", async () => {
+ const { user } = installerRender( );
+
+ const button = screen.getByRole("button", { name: "Register" });
+ await user.click(button);
+
+ const popup = await screen.findByRole("dialog");
+ within(popup).getByText("Register Test Product1");
+ const codeInput = within(popup).getByLabelText(/Registration code/);
+
+ await user.type(codeInput, "111222");
+ const accept = within(popup).getByRole("button", { name: "Accept" });
+ await user.click(accept);
+
+ within(popup).getByText("Error registering product");
+ });
+ });
+});
+
+describe("when the button to perform product de-registration is clicked", () => {
+ beforeEach(() => {
+ mockRegistration.requirement = "mandatory";
+ mockRegistration.code = "111222";
+ mockSoftware.product.deregister = jest.fn().mockResolvedValue({ success: true });
+ });
+
+ it("opens a popup to deregister the product", async () => {
+ const { user } = installerRender( );
+
+ const button = screen.getByRole("button", { name: "Deregister" });
+ await user.click(button);
+
+ const popup = await screen.findByRole("dialog");
+ within(popup).getByText("Deregister Test Product1");
+
+ const accept = within(popup).getByRole("button", { name: "Accept" });
+ await user.click(accept);
+
+ expect(mockSoftware.product.deregister).toHaveBeenCalled();
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ });
+
+ describe("if the popup is canceled", () => {
+ it("closes the popup without performing product de-registration", async () => {
+ const { user } = installerRender( );
+
+ const button = screen.getByRole("button", { name: "Deregister" });
+ await user.click(button);
+
+ const popup = await screen.findByRole("dialog");
+ const cancel = within(popup).getByRole("button", { name: "Cancel" });
+ await user.click(cancel);
+
+ expect(mockSoftware.product.deregister).not.toHaveBeenCalled();
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ });
+ });
+
+ describe("if there is an error performing the product de-registration", () => {
+ beforeEach(() => {
+ mockSoftware.product.deregister = jest.fn().mockResolvedValue({
+ success: false,
+ message: "Product cannot be deregistered"
+ });
+ });
+
+ it("does not close the popup and shows the error", async () => {
+ const { user } = installerRender( );
+
+ const button = screen.getByRole("button", { name: "Deregister" });
+ await user.click(button);
+
+ const popup = await screen.findByRole("dialog");
+ const accept = within(popup).getByRole("button", { name: "Accept" });
+ await user.click(accept);
+
+ within(popup).getByText("Product cannot be deregistered");
+ });
+ });
+});
diff --git a/web/src/components/product/ProductRegistrationForm.jsx b/web/src/components/product/ProductRegistrationForm.jsx
new file mode 100644
index 0000000000..2d0fd5efb5
--- /dev/null
+++ b/web/src/components/product/ProductRegistrationForm.jsx
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) [2023] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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, { useEffect, useState } from "react";
+import { Form, FormGroup, TextInput } from "@patternfly/react-core";
+
+import { _ } from "~/i18n";
+import { EmailInput } from "~/components/core";
+import { noop } from "~/utils";
+
+/**
+ * Form for registering a product.
+ * @component
+ *
+ * @param {object} props
+ * @param {boolean} props.id - Form id.
+ * @param {function} props.onSubmit - Callback to be called when the form is submitted.
+ * @param {(isValid: boolean) => void} props.onValidate - Callback to be called when the form is
+ * validated.
+ */
+export default function ProductRegistrationForm({
+ id,
+ onSubmit: onSubmitProp = noop,
+ onValidate = noop
+}) {
+ const [code, setCode] = useState("");
+ const [email, setEmail] = useState("");
+ const [isValidEmail, setIsValidEmail] = useState(true);
+
+ const onSubmit = (e) => {
+ e.preventDefault();
+ onSubmitProp({ code, email });
+ };
+
+ useEffect(() => {
+ const validate = () => {
+ return code.length > 0 && isValidEmail;
+ };
+
+ onValidate(validate());
+ }, [code, isValidEmail, onValidate]);
+
+ return (
+
+ );
+}
diff --git a/web/src/components/product/ProductRegistrationForm.test.jsx b/web/src/components/product/ProductRegistrationForm.test.jsx
new file mode 100644
index 0000000000..14a9d18c4a
--- /dev/null
+++ b/web/src/components/product/ProductRegistrationForm.test.jsx
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) [2023] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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 { Button } from "@patternfly/react-core";
+import { screen } from "@testing-library/react";
+import { plainRender } from "~/test-utils";
+import { ProductRegistrationForm } from "~/components/product";
+
+it("renders a field for entering the registration code", async() => {
+ plainRender( );
+ await screen.findByLabelText(/Registration code/);
+});
+
+it("renders a field for entering an email", async() => {
+ plainRender( );
+ await screen.findByLabelText("Email");
+});
+
+const ProductRegistrationFormTest = () => {
+ const [isSubmitted, setIsSubmitted] = useState(false);
+ const [isValid, setIsValid] = useState(true);
+
+ return (
+ <>
+
+ Accept
+ {isSubmitted && Form is submitted!
}
+ {isValid === false && Form is not valid!
}
+ >
+ );
+};
+
+it("triggers the onSubmit callback", async () => {
+ const { user } = plainRender( );
+
+ expect(screen.queryByText("Form is submitted!")).toBeNull();
+
+ const button = screen.getByRole("button", { name: "Accept" });
+ await user.click(button);
+ await screen.findByText("Form is submitted!");
+});
+
+it("sets the form as invalid if there is no code", async () => {
+ plainRender( );
+ await screen.findByText("Form is not valid!");
+});
+
+it("sets the form as invalid if there is a code and a wrong email", async () => {
+ const { user } = plainRender( );
+ const codeInput = await screen.findByLabelText(/Registration code/);
+ const emailInput = await screen.findByLabelText("Email");
+ await user.type(codeInput, "111222");
+ await user.type(emailInput, "foo");
+
+ await screen.findByText("Form is not valid!");
+});
+
+it("does not set the form as invalid if there is a code and no email", async () => {
+ const { user } = plainRender( );
+ const codeInput = await screen.findByLabelText(/Registration code/);
+ await user.type(codeInput, "111222");
+
+ expect(screen.queryByText("Form is not valid!")).toBeNull();
+});
+
+it("does not set the form as invalid if there is a code and a correct email", async () => {
+ const { user } = plainRender( );
+ const codeInput = await screen.findByLabelText(/Registration code/);
+ const emailInput = await screen.findByLabelText("Email");
+ await user.type(codeInput, "111222");
+ await user.type(emailInput, "test@test.com");
+
+ expect(screen.queryByText("Form is not valid!")).toBeNull();
+});
diff --git a/web/src/components/product/index.js b/web/src/components/product/index.js
index 12e0a814b0..be115a18c8 100644
--- a/web/src/components/product/index.js
+++ b/web/src/components/product/index.js
@@ -19,5 +19,7 @@
* find current contact information at www.suse.com.
*/
+export { default as ProductPage } from "./ProductPage";
+export { default as ProductRegistrationForm } from "./ProductRegistrationForm";
export { default as ProductSelectionPage } from "./ProductSelectionPage";
export { default as ProductSelector } from "./ProductSelector";
diff --git a/web/src/index.js b/web/src/index.js
index f47f5a76b5..53d58610a3 100644
--- a/web/src/index.js
+++ b/web/src/index.js
@@ -37,7 +37,7 @@ import App from "~/App";
import Main from "~/Main";
import DevServerWrapper from "~/DevServerWrapper";
import { Overview } from "~/components/overview";
-import { ProductSelectionPage } from "~/components/product";
+import { ProductPage, ProductSelectionPage } from "~/components/product";
import { SoftwarePage } from "~/components/software";
import { ProposalPage as StoragePage, ISCSIPage, DASDPage, ZFCPPage } from "~/components/storage";
import { UsersPage } from "~/components/users";
@@ -77,6 +77,7 @@ root.render(
}>
} />
} />
+ } />
} />
} />
} />
From 33cf3a669f144e020b4d27e35d60f554c0238959 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 3 Nov 2023 09:55:53 +0000
Subject: [PATCH 86/97] [service] Do not automatically probe software after
registration
---
service/lib/agama/dbus/software/product.rb | 8 ++++++++
service/lib/agama/software/manager.rb | 11 +----------
2 files changed, 9 insertions(+), 10 deletions(-)
diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb
index 5085e42444..6c94418a9d 100644
--- a/service/lib/agama/dbus/software/product.rb
+++ b/service/lib/agama/dbus/software/product.rb
@@ -142,6 +142,10 @@ def requirement
# Tries to register with the given registration code.
#
+ # @note Software is not automatically probed after registering the product. The reason is
+ # to avoid dealing with possible probing issues in the registration D-Bus API. Clients
+ # have to explicitly call to #Probe after registering a product.
+ #
# @param reg_code [String]
# @param email [String, nil]
#
@@ -174,6 +178,10 @@ def register(reg_code, email: nil)
# Tries to deregister.
#
+ # @note Software is not automatically probed after deregistering the product. The reason is
+ # to avoid dealing with possible probing issues in the deregistration D-Bus API. Clients
+ # have to explicitly call to #Probe after deregistering a product.
+ #
# @return [Array(Integer, String)] Result code and a description.
# Possible result codes:
# 0: success
diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb
index cb398b00dc..be47087609 100644
--- a/service/lib/agama/software/manager.rb
+++ b/service/lib/agama/software/manager.rb
@@ -286,16 +286,7 @@ def used_disk_space
end
def registration
- return @registration if @registration
-
- @registration = Registration.new(self, @logger)
- @registration.on_change do
- # reprobe and repropose when system is register or deregistered
- probe
- proposal
- end
-
- @registration
+ @registration ||= Registration.new(self, logger)
end
# code is based on https://github.com/yast/yast-registration/blob/master/src/lib/registration/sw_mgmt.rb#L365
From 298dd3cd1e3326929690b3eeeb7ab70f97b00163 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 3 Nov 2023 11:20:06 +0000
Subject: [PATCH 87/97] [web] Fix tests
---
web/src/components/core/IssuesLink.test.jsx | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/web/src/components/core/IssuesLink.test.jsx b/web/src/components/core/IssuesLink.test.jsx
index dc2fdae1ba..8073276beb 100644
--- a/web/src/components/core/IssuesLink.test.jsx
+++ b/web/src/components/core/IssuesLink.test.jsx
@@ -25,17 +25,15 @@ import { installerRender, withNotificationProvider } from "~/test-utils";
import { createClient } from "~/client";
import { IssuesLink } from "~/components/core";
-let hasIssues = false;
+let mockIssues = {};
jest.mock("~/client");
beforeEach(() => {
createClient.mockImplementation(() => {
return {
- issues: {
- any: () => Promise.resolve(hasIssues),
- onIssuesChange: jest.fn()
- }
+ issues: jest.fn().mockResolvedValue(mockIssues),
+ onIssuesChange: jest.fn()
};
});
});
@@ -48,7 +46,9 @@ it("renders a link for navigating to the issues page", async () => {
describe("if there are issues", () => {
beforeEach(() => {
- hasIssues = true;
+ mockIssues = {
+ storage: [{ description: "issue 1" }]
+ };
});
it("includes a notification mark", async () => {
@@ -60,7 +60,7 @@ describe("if there are issues", () => {
describe("if there are not issues", () => {
beforeEach(() => {
- hasIssues = false;
+ mockIssues = {};
});
it("does not include a notification mark", async () => {
From 310ce7146ceab6bd172478919b0898e77516ecf4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Fri, 3 Nov 2023 16:09:00 +0000
Subject: [PATCH 88/97] [web] Remove link for changing the product
---
web/src/App.jsx | 2 -
web/src/components/core/Sidebar.test.jsx | 1 -
.../components/software/ChangeProductLink.jsx | 39 -----------
.../software/ChangeProductLink.test.jsx | 69 -------------------
web/src/components/software/index.js | 1 -
5 files changed, 112 deletions(-)
delete mode 100644 web/src/components/software/ChangeProductLink.jsx
delete mode 100644 web/src/components/software/ChangeProductLink.test.jsx
diff --git a/web/src/App.jsx b/web/src/App.jsx
index 0e162ca045..4099afcd10 100644
--- a/web/src/App.jsx
+++ b/web/src/App.jsx
@@ -39,7 +39,6 @@ import {
ShowTerminalButton,
Sidebar
} from "~/components/core";
-import { ChangeProductLink } from "~/components/software";
import { LanguageSwitcher } from "./components/l10n";
import { Layout, Loading, Title } from "./components/layout";
import { useL10n } from "./context/l10n";
@@ -107,7 +106,6 @@ function App() {
<>
-
diff --git a/web/src/components/core/Sidebar.test.jsx b/web/src/components/core/Sidebar.test.jsx
index 44ef3b4357..dce362f2fd 100644
--- a/web/src/components/core/Sidebar.test.jsx
+++ b/web/src/components/core/Sidebar.test.jsx
@@ -27,7 +27,6 @@ import { createClient } from "~/client";
// Mock some components using contexts and not relevant for below tests
jest.mock("~/components/core/LogsButton", () => () => LogsButton Mock
);
-jest.mock("~/components/software/ChangeProductLink", () => () => ChangeProductLink Mock
);
let mockIssues;
diff --git a/web/src/components/software/ChangeProductLink.jsx b/web/src/components/software/ChangeProductLink.jsx
deleted file mode 100644
index 3eb872231a..0000000000
--- a/web/src/components/software/ChangeProductLink.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (c) [2022-2023] SUSE LLC
- *
- * All Rights Reserved.
- *
- * This program is free software; you can redistribute it and/or modify it
- * under the terms of version 2 of the GNU General Public License as published
- * by the Free Software Foundation.
- *
- * 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 { Link } from "react-router-dom";
-import { useProduct } from "~/context/product";
-import { Icon } from "~/components/layout";
-import { _ } from "~/i18n";
-
-export default function ChangeProductLink() {
- const { products } = useProduct();
-
- if (products?.length === 1) return null;
-
- return (
-
-
- {_("Change product")}
-
- );
-}
diff --git a/web/src/components/software/ChangeProductLink.test.jsx b/web/src/components/software/ChangeProductLink.test.jsx
deleted file mode 100644
index 55afa5e24b..0000000000
--- a/web/src/components/software/ChangeProductLink.test.jsx
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (c) [2022-2023] SUSE LLC
- *
- * All Rights Reserved.
- *
- * This program is free software; you can redistribute it and/or modify it
- * under the terms of version 2 of the GNU General Public License as published
- * by the Free Software Foundation.
- *
- * 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, waitFor } from "@testing-library/react";
-import { installerRender } from "~/test-utils";
-import { ChangeProductLink } from "~/components/software";
-
-let mockProducts;
-
-jest.mock("~/context/product", () => ({
- ...jest.requireActual("~/context/product"),
- useProduct: () => {
- return {
- products: mockProducts,
- };
- }
-}));
-
-describe("ChangeProductLink", () => {
- describe("when there is only a single product", () => {
- beforeEach(() => {
- mockProducts = [
- { id: "openSUSE", name: "openSUSE Tumbleweed" }
- ];
- });
-
- it("renders nothing", async () => {
- installerRender( );
-
- const main = await screen.findByRole("main");
- await waitFor(() => expect(main).toBeEmptyDOMElement());
- });
- });
-
- describe("when there is more than one product", () => {
- beforeEach(() => {
- mockProducts = [
- { id: "openSUSE", name: "openSUSE Tumbleweed" },
- { id: "Leap Micro", name: "openSUSE Micro" }
- ];
- });
-
- it("renders a link for navigating to the selection product page", async () => {
- installerRender( );
- const link = await screen.findByRole("link", { name: "Change product" });
-
- expect(link).toHaveAttribute("href", "/products");
- });
- });
-});
diff --git a/web/src/components/software/index.js b/web/src/components/software/index.js
index 84f7a58f05..af42a2eb9d 100644
--- a/web/src/components/software/index.js
+++ b/web/src/components/software/index.js
@@ -19,7 +19,6 @@
* find current contact information at www.suse.com.
*/
-export { default as ChangeProductLink } from "./ChangeProductLink";
export { default as PatternSelector } from "./PatternSelector";
export { default as UsedSize } from "./UsedSize";
export { default as SoftwarePage } from "./SoftwarePage";
From 608cd40751df9c0dc6f2ddb26d1f6079c08d8f24 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Tue, 14 Nov 2023 14:30:19 +0000
Subject: [PATCH 89/97] [web] Add registration section to product page
---
web/src/assets/styles/utilities.scss | 4 +
web/src/components/product/ProductPage.jsx | 212 +++++++++---------
.../components/product/ProductPage.test.jsx | 54 +++--
3 files changed, 142 insertions(+), 128 deletions(-)
diff --git a/web/src/assets/styles/utilities.scss b/web/src/assets/styles/utilities.scss
index 6b64d2f47b..1f84016877 100644
--- a/web/src/assets/styles/utilities.scss
+++ b/web/src/assets/styles/utilities.scss
@@ -142,6 +142,10 @@
padding: 0;
}
+.p-0 {
+ padding: 0;
+}
+
.no-stack-gutter {
--stack-gutter: 0;
}
diff --git a/web/src/components/product/ProductPage.jsx b/web/src/components/product/ProductPage.jsx
index 4814f33231..77c671f855 100644
--- a/web/src/components/product/ProductPage.jsx
+++ b/web/src/components/product/ProductPage.jsx
@@ -42,7 +42,7 @@ import { useProduct } from "~/context/product";
* @param {function} props.onFinish - Callback to be called when the product is correctly selected.
* @param {function} props.onCancel - Callback to be called when the product selection is canceled.
*/
-const ProductSelectionPopup = ({ isOpen = false, onFinish = noop, onCancel = noop }) => {
+const ChangeProductPopup = ({ isOpen = false, onFinish = noop, onCancel = noop }) => {
const { manager, software } = useInstallerClient();
const { products, selectedProduct } = useProduct();
const [newProductId, setNewProductId] = useState(selectedProduct?.id);
@@ -87,7 +87,7 @@ const ProductSelectionPopup = ({ isOpen = false, onFinish = noop, onCancel = noo
* @param {function} props.onCancel - Callback to be called when the product registration is
* canceled.
*/
-const ProductRegistrationPopup = ({
+const RegisterProductPopup = ({
isOpen = false,
onFinish = noop,
onCancel: onCancelProp = noop
@@ -156,7 +156,7 @@ const ProductRegistrationPopup = ({
* @param {function} props.onCancel - Callback to be called when the product de-registration is
* canceled.
*/
-const ProductDeregistrationPopup = ({
+const DeregisterProductPopup = ({
isOpen = false,
onFinish = noop,
onCancel: onCancelProp = noop
@@ -197,7 +197,10 @@ const ProductDeregistrationPopup = ({
}
/>
- {sprintf(_("Do you want to deregister %s?"), selectedProduct.name)}
+ {
+ // TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite)
+ sprintf(_("Do you want to deregister %s?"), selectedProduct.name)
+ }
@@ -225,6 +228,7 @@ const RegisteredWarningPopup = ({ isOpen = false, onAccept = noop }) => {
{
sprintf(
+ // TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite)
_("The product %s must be deregistered before selecting a new product."),
selectedProduct.name
)
@@ -239,29 +243,40 @@ const RegisteredWarningPopup = ({ isOpen = false, onAccept = noop }) => {
);
};
-/**
- * Buttons for a product that does not require registration.
- * @component
- *
- * @param {object} props
- * @param {boolean} props.isDisabled
- */
-const WithoutRegistrationButtons = ({ isDisabled = false }) => {
+const ChangeProductButton = ({ isDisabled = false }) => {
const [isPopupOpen, setIsPopupOpen] = useState(false);
+ const { registration } = useProduct();
+
+ const openPopup = () => setIsPopupOpen(true);
+ const closePopup = () => setIsPopupOpen(false);
+
+ const isRegistered = registration.code !== null;
return (
<>
setIsPopupOpen(true)}
+ variant="link"
+ className="p-0"
+ onClick={openPopup}
isDisabled={isDisabled}
>
{_("Change product")}
- setIsPopupOpen(false)}
- onCancel={() => setIsPopupOpen(false)}
+
+ }
+ else={
+
+ }
/>
>
);
@@ -274,115 +289,122 @@ const WithoutRegistrationButtons = ({ isDisabled = false }) => {
* @param {object} props
* @param {boolean} props.isDisabled
*/
-const DeregisteredButtons = ({ isDisabled = false }) => {
- const [isRegistrationPopupOpen, setIsRegistrationPopupOpen] = useState(false);
- const [isSelectionPopupOpen, setIsSelectionPopupOpen] = useState(false);
+const RegisterProductButton = ({ isDisabled = false }) => {
+ const [isPopupOpen, setIsPopupOpen] = useState(false);
+
+ const openPopup = () => setIsPopupOpen(true);
+ const closePopup = () => setIsPopupOpen(false);
return (
<>
setIsRegistrationPopupOpen(true)}
+ onClick={openPopup}
isDisabled={isDisabled}
>
{_("Register")}
- setIsSelectionPopupOpen(true)}
- isDisabled={isDisabled}
- >
- {_("Change product")}
-
- setIsRegistrationPopupOpen(false)}
- onCancel={() => setIsRegistrationPopupOpen(false)}
- />
- setIsSelectionPopupOpen(false)}
- onCancel={() => setIsSelectionPopupOpen(false)}
+
>
);
};
/**
- * Buttons for a product that is already registered.
+ * Buttons for a product that is not registered yet.
* @component
*
* @param {object} props
* @param {boolean} props.isDisabled
*/
-const RegisteredButtons = ({ isDisabled = false }) => {
- const [isDeregistrationPopupOpen, setIsDeregistrationPopupOpen] = useState(false);
- const [isWarningPopupOpen, setIsWarningPopupOpen] = useState(false);
+const DeregisterProductButton = ({ isDisabled = false }) => {
+ const [isPopupOpen, setIsPopupOpen] = useState(false);
+
+ const openPopup = () => setIsPopupOpen(true);
+ const closePopup = () => setIsPopupOpen(false);
return (
<>
setIsDeregistrationPopupOpen(true)}
- isDisabled={isDisabled}
- >
- {_("Deregister")}
-
- setIsWarningPopupOpen(true)}
+ variant="link"
+ className="p-0"
+ onClick={openPopup}
isDisabled={isDisabled}
>
- {_("Change product")}
+ {_("Deregister product")}
- setIsDeregistrationPopupOpen(false)}
- onCancel={() => setIsDeregistrationPopupOpen(false)}
- />
- setIsWarningPopupOpen(false)}
+
>
);
};
-/**
- * Renders the actions for the current product.
- * @component
- *
- * @param {object} props
- * @param {boolean} props.isDisabled
- */
-const ProductActions = ({ isDisabled = false }) => {
+const ProductSection = ({ isLoading = false }) => {
+ const { products, selectedProduct } = useProduct();
+
+ return (
+
+ {selectedProduct?.description}
+ 1}
+ then={ }
+ />
+
+ );
+};
+
+const RegistrationContent = ({ isLoading = false }) => {
const { registration } = useProduct();
- const withRegistration = registration.requirement !== "not-required";
- const registered = registration.code !== null;
+ const mask = (v) => v.replace(v.slice(0, -4), "*".repeat(Math.max(v.length - 4, 0)));
return (
<>
- }
- else={ }
- />
- }
- else={ }
- />
+ {_("Code:")}
+ {mask(registration.code)}
+
+ {_("Email:")}
+ {registration.email}
+
+
+ >
+ );
+};
+
+const RegistrationSection = ({ isLoading = false }) => {
+ const { registration } = useProduct();
+
+ const isRequired = registration?.requirement !== "not-required";
+ const isRegistered = registration?.code !== null;
+
+ return (
+ // TRANSLATORS: section title.
+
{_("Configuring product. Actions are disabled until the product is configured.")}
+ }
+ else={
+ <>
+ {_("This product requires registration.")}
+
+ >
+ }
+ />
}
+ else={{_("This product does not require registration.")}
}
/>
- >
+
);
};
@@ -395,7 +417,6 @@ export default function ProductPage() {
const [softwareStatus, setSoftwareStatus] = useState();
const { cancellablePromise } = useCancellablePromise();
const { manager, software } = useInstallerClient();
- const { selectedProduct, registration } = useProduct();
useEffect(() => {
cancellablePromise(manager.getStatus()).then(setManagerStatus);
@@ -411,25 +432,8 @@ export default function ProductPage() {
return (
-
- {selectedProduct.description}
-
-
- {_("Registration code:")}
- {registration.code}
-
-
- {_("Email:")}
- {registration.email}
-
- >
- }
- />
-
-
+
+
);
}
diff --git a/web/src/components/product/ProductPage.test.jsx b/web/src/components/product/ProductPage.test.jsx
index 4443436266..8b46818a5c 100644
--- a/web/src/components/product/ProductPage.test.jsx
+++ b/web/src/components/product/ProductPage.test.jsx
@@ -29,6 +29,7 @@ import { createClient } from "~/client";
let mockManager;
let mockSoftware;
+let mockProducts;
let mockRegistration;
const products = [
@@ -54,7 +55,7 @@ jest.mock("~/client");
jest.mock("~/context/product", () => ({
...jest.requireActual("~/context/product"),
- useProduct: () => ({ products, selectedProduct, registration: mockRegistration })
+ useProduct: () => ({ products: mockProducts, selectedProduct, registration: mockRegistration })
}));
beforeEach(() => {
@@ -75,6 +76,8 @@ beforeEach(() => {
}
};
+ mockProducts = products;
+
mockRegistration = {
requirement: "not-required",
code: null,
@@ -95,15 +98,34 @@ it("renders the product name and description", async () => {
await screen.findByText("Test Product1 description");
});
+it("shows a button to change the product", async () => {
+ installerRender( );
+ await screen.findByRole("button", { name: "Change product" });
+});
+
+describe("if there is only a product", () => {
+ beforeEach(() => {
+ mockProducts = [products[0]];
+ });
+
+ it("does not show a button to change the product", async () => {
+ installerRender( );
+ expect(screen.queryByRole("button", { name: "Change product" })).not.toBeInTheDocument();
+ });
+});
+
describe("if the product is already registered", () => {
beforeEach(() => {
- mockRegistration.code = "111222";
- mockRegistration.email = "test@test.com";
+ mockRegistration = {
+ requirement: "mandatory",
+ code: "111222",
+ email: "test@test.com"
+ };
});
it("shows the information about the registration", async () => {
installerRender( );
- await screen.findByText("111222");
+ await screen.findByText("**1222");
await screen.findByText("test@test.com");
});
});
@@ -113,11 +135,6 @@ describe("if the product does not require registration", () => {
mockRegistration.requirement = "not-required";
});
- it("shows a button to change the product", async () => {
- installerRender( );
- await screen.findByRole("button", { name: "Change product" });
- });
-
it("does not show a button to register the product", async () => {
installerRender( );
expect(screen.queryByRole("button", { name: "Register" })).not.toBeInTheDocument();
@@ -129,11 +146,6 @@ describe("if the product requires registration", () => {
mockRegistration.requirement = "required";
});
- it("shows a button to change the product", async () => {
- installerRender( );
- await screen.findByRole("button", { name: "Change product" });
- });
-
describe("and the product is not registered yet", () => {
beforeEach(() => {
mockRegistration.code = null;
@@ -152,7 +164,7 @@ describe("if the product requires registration", () => {
it("shows a button to deregister the product", async () => {
installerRender( );
- await screen.findByRole("button", { name: "Deregister" });
+ await screen.findByRole("button", { name: "Deregister product" });
});
});
});
@@ -173,12 +185,6 @@ describe("when the services are busy", () => {
expect(selectButton).toHaveAttribute("disabled");
expect(registerButton).toHaveAttribute("disabled");
});
-
- it("shows a message about configuring product", async () => {
- await act(async () => installerRender( ));
-
- screen.getByText(/Actions are disabled until the product is configured/);
- });
});
describe("when the button for changing the product is clicked", () => {
@@ -329,7 +335,7 @@ describe("when the button to perform product de-registration is clicked", () =>
it("opens a popup to deregister the product", async () => {
const { user } = installerRender( );
- const button = screen.getByRole("button", { name: "Deregister" });
+ const button = screen.getByRole("button", { name: "Deregister product" });
await user.click(button);
const popup = await screen.findByRole("dialog");
@@ -346,7 +352,7 @@ describe("when the button to perform product de-registration is clicked", () =>
it("closes the popup without performing product de-registration", async () => {
const { user } = installerRender( );
- const button = screen.getByRole("button", { name: "Deregister" });
+ const button = screen.getByRole("button", { name: "Deregister product" });
await user.click(button);
const popup = await screen.findByRole("dialog");
@@ -369,7 +375,7 @@ describe("when the button to perform product de-registration is clicked", () =>
it("does not close the popup and shows the error", async () => {
const { user } = installerRender( );
- const button = screen.getByRole("button", { name: "Deregister" });
+ const button = screen.getByRole("button", { name: "Deregister product" });
await user.click(button);
const popup = await screen.findByRole("dialog");
From 354805c3c0611b4bf8bf116b783a0958703faa61 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?=
Date: Tue, 14 Nov 2023 14:30:49 +0000
Subject: [PATCH 90/97] [web] Use password field for registration code
---
web/src/components/product/ProductRegistrationForm.jsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/web/src/components/product/ProductRegistrationForm.jsx b/web/src/components/product/ProductRegistrationForm.jsx
index 2d0fd5efb5..510f0c6c20 100644
--- a/web/src/components/product/ProductRegistrationForm.jsx
+++ b/web/src/components/product/ProductRegistrationForm.jsx
@@ -20,10 +20,10 @@
*/
import React, { useEffect, useState } from "react";
-import { Form, FormGroup, TextInput } from "@patternfly/react-core";
+import { Form, FormGroup } from "@patternfly/react-core";
import { _ } from "~/i18n";
-import { EmailInput } from "~/components/core";
+import { EmailInput, PasswordInput } from "~/components/core";
import { noop } from "~/utils";
/**
@@ -61,7 +61,7 @@ export default function ProductRegistrationForm({
return (