foreman_rh_cloud 13.2.8 → 13.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/insights_cloud/candlepin_proxies_extensions.rb +23 -0
  3. data/app/controllers/concerns/insights_cloud/package_profile_upload_extensions.rb +9 -0
  4. data/app/controllers/insights_cloud/api/machine_telemetries_controller.rb +9 -2
  5. data/app/models/concerns/rh_cloud_host.rb +4 -2
  6. data/app/models/insights_client_report_status.rb +9 -1
  7. data/app/models/inventory_sync/inventory_status.rb +16 -4
  8. data/lib/foreman_rh_cloud/engine.rb +1 -0
  9. data/lib/foreman_rh_cloud/version.rb +1 -1
  10. data/lib/inventory_sync/async/inventory_full_sync.rb +39 -3
  11. data/package.json +1 -1
  12. data/test/controllers/insights_cloud/api/machine_telemetries_controller_test.rb +40 -0
  13. data/test/controllers/insights_cloud/candlepin_proxies_extensions_test.rb +70 -0
  14. data/test/jobs/insights_client_status_aging_test.rb +40 -0
  15. data/test/jobs/inventory_full_sync_test.rb +212 -0
  16. data/test/models/insights_client_report_status_test.rb +109 -0
  17. data/test/models/inventory_sync/inventory_status_test.rb +85 -0
  18. data/test/unit/rh_cloud_host_test.rb +60 -0
  19. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/SyncButtonActions.js +8 -2
  20. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/__snapshots__/integrations.test.js.snap +1 -0
  21. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/integrations.test.js +1 -0
  22. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/components/Toast.js +43 -17
  23. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/components/__tests__/Toast.test.js +82 -0
  24. metadata +7 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04c6a1daea1675deb081565cf7d9e83e669a3fd01a577a032c9d7d5ba825cbce
4
- data.tar.gz: 5172a5c764d0dbcb71cf47e19e423f0eae01db27636be594320dece4c6364719
3
+ metadata.gz: 8ea9a50897e8e89678229d4357182f5cf0f487e1c7775048c7e6e363d9eb0b68
4
+ data.tar.gz: 574992cb545615924d46aec3cb5f1856abaa4c287bd0ddfde4af1b23fc7c8ca0
5
5
  SHA512:
6
- metadata.gz: 15b24817679825c5e60bc0e0ca0b33c18eadae7277483155c641f3d3d140bd911c0c7ad7dca15c40e3d63f9a8e354acfcd5789048658fd2888a33fa4b9741d0d
7
- data.tar.gz: 0cfe77f01b645de55c8e3e41ac4411201c5ec4819e5926d3054bac08b551e12dc923dffa6b1503956cb1075c688a5a3d94e5a63a8c4e332414e10f78054d3954
6
+ metadata.gz: 92bf788426b36beda79e9444141f93cbe1ac00b5c51cb0c28717d23dec6c220548c0cf4d9bdebe30e8cfb3a5f27922aad3b447f5344c1226a44de1f2482e4279
7
+ data.tar.gz: ed6b64ed049ea2ed4948d22a7160f5953d64cd2230353636e0075ead94331bae1d6aa9b805d64bff10b2ce5a27339bc78b3564ea3d535db1d0ed61cb3fd0e420
@@ -0,0 +1,23 @@
1
+ module InsightsCloud
2
+ module CandlepinProxiesExtensions
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ # Update insights client status when hosts check in via subscription-manager facts upload
7
+ # rubocop:disable Rails/LexicallyScopedActionFilter
8
+ after_action :update_insights_client_status, only: [:facts]
9
+ # rubocop:enable Rails/LexicallyScopedActionFilter
10
+ end
11
+
12
+ def update_insights_client_status
13
+ # Update InsightsClientReportStatus whenever host checks in via subscription-manager
14
+ # This ensures USER_OMITTED status gets set even when insights-client isn't installed
15
+ # (parameter=false means insights-client won't be installed, so it won't hit MachineTelemetriesController)
16
+ hoststatus = @host.get_status(InsightsClientReportStatus)
17
+ Rails.logger.debug "Current status: #{hoststatus.to_label}"
18
+ @host.get_status(InsightsClientReportStatus).refresh!
19
+ @host.refresh_global_status!
20
+ Rails.logger.debug "New status: #{hoststatus.to_label}"
21
+ end
22
+ end
23
+ end
@@ -6,6 +6,7 @@ module InsightsCloud
6
6
  # This method explicitly listens on Katello actions
7
7
  # rubocop:disable Rails/LexicallyScopedActionFilter
8
8
  after_action :generate_host_report, only: [:upload_package_profile, :upload_profiles]
9
+ after_action :update_insights_client_status, only: [:upload_package_profile, :upload_profiles]
9
10
  # rubocop:enable Rails/LexicallyScopedActionFilter
10
11
  end
11
12
 
@@ -31,5 +32,13 @@ module InsightsCloud
31
32
  insights_facet = @host.build_insights(uuid: @host.subscription_facet.uuid)
32
33
  insights_facet.save
33
34
  end
35
+
36
+ def update_insights_client_status
37
+ # Update InsightsClientReportStatus whenever host checks in via subscription-manager
38
+ # This ensures USER_OMITTED status gets set even when insights-client isn't installed
39
+ # (parameter=false means insights-client won't be installed, so it won't hit MachineTelemetriesController)
40
+ @host.get_status(InsightsClientReportStatus).refresh!
41
+ @host.refresh_global_status!
42
+ end
34
43
  end
35
44
  end
@@ -88,9 +88,16 @@ module InsightsCloud::Api
88
88
  private
89
89
 
90
90
  def ensure_telemetry_enabled_for_consumer
91
- unless (config = telemetry_config(@host))
91
+ config = telemetry_config(@host)
92
+
93
+ # Always update the status based on current parameter value
94
+ # This will set USER_OMITTED if parameter is false, or REPORTING/NO_REPORT if true
95
+ @host.get_status(InsightsClientReportStatus).refresh!
96
+ @host.refresh_global_status!
97
+
98
+ unless config
92
99
  logger.debug("Rejected telemetry forwarding for host #{@host.name}, insights param is set to: #{config}")
93
- render_message 'Telemetry is not enabled for this host', :status => 403
100
+ return render_message('Telemetry is not enabled for this host', :status => 403)
94
101
  end
95
102
  config
96
103
  end
@@ -15,12 +15,14 @@ module RhCloudHost
15
15
  has_one :insights_client_report_status_object, :class_name => '::InsightsClientReportStatus', :foreign_key => 'host_id'
16
16
  scoped_search :relation => :insights_client_report_status_object, :on => :status, :rename => :insights_client_report_status,
17
17
  :complete_value => { :reporting => ::InsightsClientReportStatus::REPORTING,
18
- :no_report => ::InsightsClientReportStatus::NO_REPORT }
18
+ :no_report => ::InsightsClientReportStatus::NO_REPORT,
19
+ :user_omitted => ::InsightsClientReportStatus::USER_OMITTED }
19
20
 
20
21
  has_one :inventory_sync_status_object, :class_name => '::InventorySync::InventoryStatus', :foreign_key => 'host_id'
21
22
  scoped_search :relation => :inventory_sync_status_object, :on => :status, :rename => :insights_inventory_sync_status,
22
23
  :complete_value => { :disconnect => ::InventorySync::InventoryStatus::DISCONNECT,
23
- :sync => ::InventorySync::InventoryStatus::SYNC }
24
+ :sync => ::InventorySync::InventoryStatus::SYNC,
25
+ :user_omitted => ::InventorySync::InventoryStatus::USER_OMITTED }
24
26
  scoped_search :on => :id, :rename => :insights_uuid, :only_explicit => true,
25
27
  :ext_method => :search_by_insights_uuid, :complete_value => false
26
28
 
@@ -3,8 +3,9 @@ class InsightsClientReportStatus < HostStatus::Status
3
3
 
4
4
  REPORTING = 0
5
5
  NO_REPORT = 1
6
+ USER_OMITTED = 2
6
7
 
7
- scope :stale, -> { where.not(reported_at: (Time.now - REPORT_INTERVAL)..Time.now) }
8
+ scope :stale, -> { where.not(status: USER_OMITTED).where.not(reported_at: (Time.now - REPORT_INTERVAL)..Time.now) }
8
9
  scope :reporting, -> { where(status: REPORTING) }
9
10
 
10
11
  def self.status_name
@@ -17,6 +18,8 @@ class InsightsClientReportStatus < HostStatus::Status
17
18
  N_('Reporting')
18
19
  when NO_REPORT
19
20
  N_('Not reporting')
21
+ when USER_OMITTED
22
+ N_('Not reporting because host_registration_insights parameter value is false')
20
23
  end
21
24
  end
22
25
 
@@ -26,10 +29,15 @@ class InsightsClientReportStatus < HostStatus::Status
26
29
  ::HostStatus::Global::OK
27
30
  when NO_REPORT
28
31
  ::HostStatus::Global::ERROR
32
+ when USER_OMITTED
33
+ ::HostStatus::Global::OK
29
34
  end
30
35
  end
31
36
 
32
37
  def to_status
38
+ excluded_by_host_param =
39
+ ::Foreman::Cast.to_bool(host.host_param('host_registration_insights')) == false
40
+ return USER_OMITTED if excluded_by_host_param
33
41
  in_interval? ? REPORTING : NO_REPORT
34
42
  end
35
43
 
@@ -2,6 +2,7 @@ module InventorySync
2
2
  class InventoryStatus < HostStatus::Status
3
3
  DISCONNECT = 0
4
4
  SYNC = 1
5
+ USER_OMITTED = 2
5
6
 
6
7
  def self.status_name
7
8
  N_('Inventory')
@@ -13,6 +14,8 @@ module InventorySync
13
14
  ::HostStatus::Global::WARN
14
15
  when SYNC
15
16
  ::HostStatus::Global::OK
17
+ when USER_OMITTED
18
+ ::HostStatus::Global::OK
16
19
  else
17
20
  ::HostStatus::Global::WARN
18
21
  end
@@ -21,16 +24,25 @@ module InventorySync
21
24
  def to_label
22
25
  case status
23
26
  when DISCONNECT
24
- N_('Host was not uploaded to your RH cloud inventory')
27
+ N_('Host is not present on console.redhat.com Inventory service')
25
28
  when SYNC
26
- N_('Successfully uploaded to your RH cloud inventory')
29
+ N_('Host is uploaded and present on console.redhat.com Inventory service')
30
+ when USER_OMITTED
31
+ N_('Host is excluded from upload to console.redhat.com Inventory service due to host parameter')
27
32
  end
28
33
  end
29
34
 
30
35
  def to_status(options = {})
31
- # this method used to calculate status.
32
- # Since the calculation is done externally we should return the previously calculated status
36
+ # Normally, this method is used to calculate status.
37
+ # In foreman_rh_cloud 'we do things a bit differently around here.'
38
+ # Calculation is done externally in InventorySync::Async::InventoryFullSync, so we simply return the previously calculated status.
33
39
  status
34
40
  end
41
+
42
+ def relevant?(_options = {})
43
+ # Inventory status is not relevant in IoP mode since we use single-host reports
44
+ # that don't sync with the cloud inventory service
45
+ !ForemanRhCloud.with_iop_smart_proxy?
46
+ end
35
47
  end
36
48
  end
@@ -40,6 +40,7 @@ module ForemanRhCloud
40
40
  ::Host::Managed.include RhCloudHost
41
41
 
42
42
  ::Katello::Api::Rhsm::CandlepinDynflowProxyController.include InsightsCloud::PackageProfileUploadExtensions
43
+ ::Katello::Api::Rhsm::CandlepinProxiesController.include InsightsCloud::CandlepinProxiesExtensions
43
44
  ::Katello::RegistrationManager.singleton_class.prepend ::ForemanRhCloud::RegistrationManagerExtensions
44
45
  end
45
46
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanRhCloud
2
- VERSION = '13.2.8'.freeze
2
+ VERSION = '13.2.9'.freeze
3
3
  end
@@ -15,16 +15,26 @@ module InventorySync
15
15
 
16
16
  def setup_statuses
17
17
  @subscribed_hosts_ids = Set.new(affected_host_ids)
18
+ @omitted_ids = Set.new(user_omitted_host_ids)
19
+
20
+ # Remove user-omitted hosts from subscribed set. In normal operation, affected_host_ids
21
+ # already excludes user-omitted hosts via for_slice, but this handles edge cases like
22
+ # a host transitioning from uploaded to user-omitted between syncs.
23
+ @subscribed_hosts_ids.subtract(@omitted_ids)
18
24
 
19
25
  InventorySync::InventoryStatus.transaction do
20
26
  InventorySync::InventoryStatus.where(host_id: @subscribed_hosts_ids).delete_all
27
+ InventorySync::InventoryStatus.where(host_id: @omitted_ids).delete_all
21
28
  yield
22
- add_missing_hosts_statuses(@subscribed_hosts_ids)
29
+ add_missing_hosts_statuses(@subscribed_hosts_ids) # any remaining hosts after yield are disconnected
30
+ add_user_omitted_host_statuses(@omitted_ids)
23
31
  host_statuses[:disconnect] += @subscribed_hosts_ids.size
32
+ host_statuses[:user_omitted] += @omitted_ids.size
24
33
  end
25
34
 
26
- logger.debug("Synced hosts amount: #{host_statuses[:sync]}")
27
- logger.debug("Disconnected hosts amount: #{host_statuses[:disconnect]}")
35
+ logger.debug("Synced hosts count: #{host_statuses[:sync]}")
36
+ logger.debug("Disconnected hosts count: #{host_statuses[:disconnect]}")
37
+ logger.debug("User-omitted hosts count: #{host_statuses[:user_omitted]}")
28
38
  output[:host_statuses] = host_statuses
29
39
  end
30
40
 
@@ -44,6 +54,7 @@ module InventorySync
44
54
  private
45
55
 
46
56
  def update_hosts_status(status_hashes)
57
+ # create Inventory statuses
47
58
  InventorySync::InventoryStatus.create(status_hashes)
48
59
  updated_ids = status_hashes.map { |hash| hash[:host_id] }
49
60
  @subscribed_hosts_ids.subtract(updated_ids)
@@ -61,10 +72,23 @@ module InventorySync
61
72
  )
62
73
  end
63
74
 
75
+ def add_user_omitted_host_statuses(host_ids)
76
+ InventorySync::InventoryStatus.create(
77
+ host_ids.map do |host_id|
78
+ {
79
+ host_id: host_id,
80
+ status: InventorySync::InventoryStatus::USER_OMITTED,
81
+ reported_at: DateTime.current,
82
+ }
83
+ end
84
+ )
85
+ end
86
+
64
87
  def host_statuses
65
88
  @host_statuses ||= {
66
89
  sync: 0,
67
90
  disconnect: 0,
91
+ user_omitted: 0,
68
92
  }
69
93
  end
70
94
 
@@ -73,6 +97,18 @@ module InventorySync
73
97
  Host.unscoped.where(organization: organizations)
74
98
  ).pluck(:id)
75
99
  end
100
+
101
+ def user_omitted_host_ids
102
+ param_name = InsightsCloud.enable_client_param_inventory
103
+
104
+ # Use search_for to respect parameter inheritance (global, org, hostgroup, host)
105
+ # This matches the same logic used by for_slice, ensuring consistency
106
+ Host.unscoped
107
+ .where(organization: organizations)
108
+ .joins(:subscription_facet)
109
+ .search_for("params.#{param_name} = f")
110
+ .pluck(:id)
111
+ end
76
112
  end
77
113
  end
78
114
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_rh_cloud",
3
- "version": "13.2.7",
3
+ "version": "13.2.9",
4
4
  "description": "Inventory Upload =============",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -4,6 +4,7 @@ require 'rest-client'
4
4
  module InsightsCloud::Api
5
5
  class MachineTelemetriesControllerTest < ActionController::TestCase
6
6
  include KatelloCVEHelper
7
+ include CandlepinIsolation
7
8
 
8
9
  setup do
9
10
  FactoryBot.create(:common_parameter, name: InsightsCloud.enable_client_param, key_type: 'boolean', value: true)
@@ -170,6 +171,45 @@ module InsightsCloud::Api
170
171
 
171
172
  assert_not_nil InsightsFacet.find_by(host_id: @host.id)
172
173
  end
174
+
175
+ test "should update InsightsClientReportStatus when parameter is false and block request" do
176
+ # Remove the common parameter from setup and set it to false
177
+ CommonParameter.where(name: InsightsCloud.enable_client_param).delete_all
178
+ FactoryBot.create(:common_parameter, name: InsightsCloud.enable_client_param, key_type: 'boolean', value: false)
179
+
180
+ # Stub telemetry_config to return false (disabled)
181
+ InsightsCloud::Api::MachineTelemetriesController.any_instance.stubs(:telemetry_config).returns(false)
182
+
183
+ get :forward_request, params: { "path" => "platform/ingress/v1/upload" }
184
+
185
+ # Request should be blocked
186
+ assert_response 403
187
+ assert_equal 'Telemetry is not enabled for this host', JSON.parse(@response.body)['message']
188
+
189
+ # But status should be updated to USER_OMITTED
190
+ @host.reload
191
+ insights_status = @host.get_status(InsightsClientReportStatus)
192
+ assert insights_status.persisted?, 'InsightsClientReportStatus should be created'
193
+ assert_equal InsightsClientReportStatus::USER_OMITTED, insights_status.status
194
+ end
195
+
196
+ test "should update InsightsClientReportStatus on successful upload" do
197
+ # Stub telemetry_config to return true (enabled) - common param from setup works for this
198
+ InsightsCloud::Api::MachineTelemetriesController.any_instance.stubs(:telemetry_config).returns(true)
199
+
200
+ net_http_resp = Net::HTTPResponse.new(1.0, 200, "OK")
201
+ res = RestClient::Response.create('response body', net_http_resp, @http_req)
202
+ ::ForemanRhCloud::CloudRequestForwarder.any_instance.stubs(:forward_request).returns(res)
203
+
204
+ get :forward_request, params: { "path" => "/redhat_access/r/insights/uploads/" }
205
+
206
+ assert_response :success
207
+
208
+ # Status should have been refreshed during the request
209
+ @host.reload
210
+ insights_status = @host.get_status(InsightsClientReportStatus)
211
+ assert insights_status.persisted?, 'InsightsClientReportStatus should be created and persisted'
212
+ end
173
213
  end
174
214
 
175
215
  context '#branch_info' do
@@ -0,0 +1,70 @@
1
+ require 'test_plugin_helper'
2
+
3
+ module InsightsCloud
4
+ class CandlepinProxiesExtensionsTest < ActiveSupport::TestCase
5
+ setup do
6
+ @organization = FactoryBot.create(:organization)
7
+ @host = FactoryBot.create(:host, :with_subscription, :managed, organization: @organization)
8
+ end
9
+
10
+ test 'updates InsightsClientReportStatus to USER_OMITTED when parameter is false' do
11
+ # Set parameter to false so status should be USER_OMITTED
12
+ FactoryBot.create(:common_parameter, name: 'host_registration_insights', key_type: 'boolean', value: false)
13
+
14
+ # Simulate what the callback does
15
+ @host.get_status(InsightsClientReportStatus).refresh!
16
+ @host.refresh_global_status!
17
+
18
+ # Verify the status was updated
19
+ @host.reload
20
+ insights_status = @host.get_status(InsightsClientReportStatus)
21
+ assert insights_status.persisted?, 'InsightsClientReportStatus should be persisted'
22
+ assert_equal InsightsClientReportStatus::USER_OMITTED, insights_status.status,
23
+ 'Status should be USER_OMITTED when host_registration_insights=false'
24
+ end
25
+
26
+ test 'sets USER_OMITTED status when parameter is inherited from hostgroup' do
27
+ # Create hostgroup with parameter
28
+ hostgroup = FactoryBot.create(:hostgroup)
29
+ hostgroup.group_parameters << GroupParameter.create(
30
+ name: 'host_registration_insights',
31
+ value: 'false',
32
+ key_type: 'boolean'
33
+ )
34
+ hostgroup.save!
35
+
36
+ @host.hostgroup = hostgroup
37
+ @host.save!
38
+
39
+ # Simulate what the callback does
40
+ @host.get_status(InsightsClientReportStatus).refresh!
41
+ @host.refresh_global_status!
42
+
43
+ # Verify status respects inherited parameter
44
+ @host.reload
45
+ insights_status = @host.get_status(InsightsClientReportStatus)
46
+ assert_equal InsightsClientReportStatus::USER_OMITTED, insights_status.status,
47
+ 'Status should be USER_OMITTED when host_registration_insights=false is inherited from hostgroup'
48
+ end
49
+
50
+ test 'refreshes global status' do
51
+ FactoryBot.create(:common_parameter, name: 'host_registration_insights', key_type: 'boolean', value: true)
52
+
53
+ # Simulate what the callback does
54
+ @host.get_status(InsightsClientReportStatus).refresh!
55
+ @host.refresh_global_status!
56
+
57
+ # Verify global status was updated
58
+ @host.reload
59
+ assert_not_nil @host.global_status, 'Global status should be set'
60
+ end
61
+
62
+ test 'CandlepinProxiesController includes the update_insights_client_status method' do
63
+ # Verify the concern is included and method is available
64
+ controller = Katello::Api::Rhsm::CandlepinProxiesController.new
65
+
66
+ assert_respond_to controller, :update_insights_client_status,
67
+ 'CandlepinProxiesController should have update_insights_client_status method from concern'
68
+ end
69
+ end
70
+ end
@@ -31,4 +31,44 @@ class InsightsClientStatusAgingTest < ActiveSupport::TestCase
31
31
  assert_equal InsightsClientReportStatus::NO_REPORT, @host3.get_status(InsightsClientReportStatus).status
32
32
  assert_equal InsightsClientReportStatus::NO_REPORT, @host4.get_status(InsightsClientReportStatus).status
33
33
  end
34
+
35
+ test 'aging job does not affect USER_OMITTED hosts' do
36
+ # Host 1: USER_OMITTED with old reported_at (should stay USER_OMITTED)
37
+ InsightsClientReportStatus.find_or_initialize_by(host_id: @host1.id).update(
38
+ status: InsightsClientReportStatus::USER_OMITTED,
39
+ reported_at: Time.now - InsightsClientReportStatus::REPORT_INTERVAL - 1.day
40
+ )
41
+
42
+ # Host 2: REPORTING with old reported_at (should change to NO_REPORT)
43
+ InsightsClientReportStatus.find_or_initialize_by(host_id: @host2.id).update(
44
+ status: InsightsClientReportStatus::REPORTING,
45
+ reported_at: Time.now - InsightsClientReportStatus::REPORT_INTERVAL - 1.day
46
+ )
47
+
48
+ # Host 3: USER_OMITTED with recent reported_at (should stay USER_OMITTED)
49
+ InsightsClientReportStatus.find_or_initialize_by(host_id: @host3.id).update(
50
+ status: InsightsClientReportStatus::USER_OMITTED,
51
+ reported_at: Time.now - 1.day
52
+ )
53
+
54
+ # Host 4: REPORTING with recent reported_at (should stay REPORTING)
55
+ InsightsClientReportStatus.find_or_initialize_by(host_id: @host4.id).update(
56
+ status: InsightsClientReportStatus::REPORTING,
57
+ reported_at: Time.now - 1.day
58
+ )
59
+
60
+ action = create_and_plan_action(InsightsCloud::Async::InsightsClientStatusAging)
61
+ run_action(action)
62
+
63
+ @hosts.each(&:reload)
64
+
65
+ assert_equal InsightsClientReportStatus::USER_OMITTED, @host1.get_status(InsightsClientReportStatus).status,
66
+ 'USER_OMITTED host with old report should stay USER_OMITTED'
67
+ assert_equal InsightsClientReportStatus::NO_REPORT, @host2.get_status(InsightsClientReportStatus).status,
68
+ 'REPORTING host with old report should change to NO_REPORT'
69
+ assert_equal InsightsClientReportStatus::USER_OMITTED, @host3.get_status(InsightsClientReportStatus).status,
70
+ 'USER_OMITTED host with recent report should stay USER_OMITTED'
71
+ assert_equal InsightsClientReportStatus::REPORTING, @host4.get_status(InsightsClientReportStatus).status,
72
+ 'REPORTING host with recent report should stay REPORTING'
73
+ end
34
74
  end
@@ -5,6 +5,7 @@ class InventoryFullSyncTest < ActiveSupport::TestCase
5
5
  include Dynflow::Testing::Factories
6
6
  include MockCerts
7
7
  include KatelloCVEHelper
8
+ include CandlepinIsolation
8
9
 
9
10
  setup do
10
11
  User.current = User.find_by(login: 'secret_admin')
@@ -313,4 +314,215 @@ class InventoryFullSyncTest < ActiveSupport::TestCase
313
314
 
314
315
  assert_nil InventorySync::InventoryStatus.where(host_id: @host3.id).first
315
316
  end
317
+
318
+ test 'user-omitted hosts get USER_OMITTED status' do
319
+ # Add parameter to exclude host1
320
+ @host1.host_parameters << HostParameter.create(
321
+ name: 'host_registration_insights_inventory',
322
+ value: 'false',
323
+ parameter_type: 'boolean'
324
+ )
325
+ @host1.save!
326
+
327
+ setup_certs_expectation do
328
+ InventorySync::Async::InventoryFullSync.any_instance.stubs(:candlepin_id_cert)
329
+ end
330
+ InventorySync::Async::InventoryFullSync.any_instance.expects(:query_inventory).returns(@inventory)
331
+ FactoryBot.create(:fact_value, fact_name: fact_names['virt::uuid'], value: '1234', host: @host2)
332
+
333
+ action = create_and_plan_action(InventorySync::Async::InventoryFullSync, @host1.organization)
334
+ run_action(action)
335
+
336
+ @host1.reload
337
+ @host2.reload
338
+
339
+ # Host1 should be USER_OMITTED
340
+ assert_equal InventorySync::InventoryStatus::USER_OMITTED,
341
+ InventorySync::InventoryStatus.where(host_id: @host1.id).first.status,
342
+ 'Host with host_registration_insights_inventory=false should have USER_OMITTED status'
343
+
344
+ # Host2 should be SYNC
345
+ assert_equal InventorySync::InventoryStatus::SYNC,
346
+ InventorySync::InventoryStatus.where(host_id: @host2.id).first.status,
347
+ 'Normal host should have SYNC status'
348
+ end
349
+
350
+ test 'user-omitted hosts are not marked as disconnected' do
351
+ # Add parameter to exclude host1
352
+ @host1.host_parameters << HostParameter.create(
353
+ name: 'host_registration_insights_inventory',
354
+ value: 'false',
355
+ parameter_type: 'boolean'
356
+ )
357
+ @host1.save!
358
+
359
+ # Host1 is not in the cloud inventory response
360
+ setup_certs_expectation do
361
+ InventorySync::Async::InventoryFullSync.any_instance.stubs(:candlepin_id_cert)
362
+ end
363
+ InventorySync::Async::InventoryFullSync.any_instance.expects(:query_inventory).returns(@inventory)
364
+ InventorySync::Async::InventoryFullSync.any_instance.expects(:affected_host_ids).returns([@host1.id, @host2.id])
365
+ FactoryBot.create(:fact_value, fact_name: fact_names['virt::uuid'], value: '1234', host: @host2)
366
+
367
+ action = create_and_plan_action(InventorySync::Async::InventoryFullSync, @host1.organization)
368
+ run_action(action)
369
+
370
+ @host1.reload
371
+
372
+ # Host1 should be USER_OMITTED, not DISCONNECT
373
+ assert_equal InventorySync::InventoryStatus::USER_OMITTED,
374
+ InventorySync::InventoryStatus.where(host_id: @host1.id).first.status,
375
+ 'User-omitted host should have USER_OMITTED status even if not in cloud inventory'
376
+ end
377
+
378
+ test 'host_statuses output includes all three counts' do
379
+ # Create a mix of hosts with different statuses
380
+ # Host1: will be user-omitted
381
+ @host1.host_parameters << HostParameter.create(
382
+ name: 'host_registration_insights_inventory',
383
+ value: 'false',
384
+ parameter_type: 'boolean'
385
+ )
386
+ @host1.save!
387
+
388
+ # Host2: will be synced (in cloud inventory)
389
+ # Host3: will be disconnected (not in cloud inventory)
390
+
391
+ # Create inventory response with only host2 (exclude host1 and host3)
392
+ inventory_with_host2_only = @inventory.dup
393
+ inventory_with_host2_only['results'] = [@inventory['results'][0]] # Only host2
394
+ inventory_with_host2_only['total'] = 1
395
+ inventory_with_host2_only['count'] = 1
396
+
397
+ setup_certs_expectation do
398
+ InventorySync::Async::InventoryFullSync.any_instance.stubs(:candlepin_id_cert)
399
+ end
400
+ InventorySync::Async::InventoryFullSync.any_instance.expects(:query_inventory).returns(inventory_with_host2_only)
401
+ InventorySync::Async::InventoryFullSync.any_instance.expects(:affected_host_ids).returns([@host1.id, @host2.id, @host3.id])
402
+ FactoryBot.create(:fact_value, fact_name: fact_names['virt::uuid'], value: '1234', host: @host2)
403
+
404
+ action = create_and_plan_action(InventorySync::Async::InventoryFullSync, @host1.organization)
405
+ run_action(action)
406
+
407
+ # Verify the statuses were actually set correctly
408
+ @host1.reload
409
+ @host2.reload
410
+ @host3.reload
411
+
412
+ assert_equal InventorySync::InventoryStatus::USER_OMITTED,
413
+ InventorySync::InventoryStatus.where(host_id: @host1.id).first.status,
414
+ 'Host with host_registration_insights_inventory=false should have USER_OMITTED status'
415
+ assert_equal InventorySync::InventoryStatus::SYNC,
416
+ InventorySync::InventoryStatus.where(host_id: @host2.id).first.status,
417
+ 'Host in cloud inventory should have SYNC status'
418
+ assert_equal InventorySync::InventoryStatus::DISCONNECT,
419
+ InventorySync::InventoryStatus.where(host_id: @host3.id).first.status,
420
+ 'Host not in cloud inventory and not user-omitted should have DISCONNECT status'
421
+ end
422
+
423
+ test 'user-omitted status respects parameter inheritance from hostgroup' do
424
+ # Create a hostgroup and assign it to host1
425
+ hostgroup = FactoryBot.create(:hostgroup)
426
+ hostgroup.organizations << @host1.organization
427
+ @host1.hostgroup = hostgroup
428
+ @host1.save!
429
+
430
+ # Set parameter at hostgroup level, not directly on host
431
+ # This verifies the fix that uses search_for instead of querying HostParameter directly
432
+ hostgroup.group_parameters << GroupParameter.create(
433
+ name: 'host_registration_insights_inventory',
434
+ value: 'false',
435
+ key_type: 'boolean'
436
+ )
437
+ hostgroup.save!
438
+
439
+ # Verify parameter is inherited (not set directly on host)
440
+ assert_nil @host1.parameters.find_by(name: 'host_registration_insights_inventory'),
441
+ 'Test setup: parameter should not be set directly on host'
442
+ refute ::Foreman::Cast.to_bool(@host1.host_param('host_registration_insights_inventory')),
443
+ 'Test setup: parameter should be inherited from hostgroup'
444
+
445
+ # Host2 remains normal (no parameter)
446
+ setup_certs_expectation do
447
+ InventorySync::Async::InventoryFullSync.any_instance.stubs(:candlepin_id_cert)
448
+ end
449
+ InventorySync::Async::InventoryFullSync.any_instance.expects(:query_inventory).returns(@inventory)
450
+ InventorySync::Async::InventoryFullSync.any_instance.expects(:affected_host_ids).returns([@host1.id, @host2.id])
451
+ FactoryBot.create(:fact_value, fact_name: fact_names['virt::uuid'], value: '1234', host: @host2)
452
+
453
+ action = create_and_plan_action(InventorySync::Async::InventoryFullSync, @host1.organization)
454
+ run_action(action)
455
+
456
+ @host1.reload
457
+ @host2.reload
458
+
459
+ # Host1 should be USER_OMITTED (inherited parameter from hostgroup)
460
+ host1_status = InventorySync::InventoryStatus.where(host_id: @host1.id).first
461
+ assert_not_nil host1_status, 'Host1 should have an inventory status'
462
+ assert_equal InventorySync::InventoryStatus::USER_OMITTED, host1_status.status,
463
+ 'Host with inherited host_registration_insights_inventory=false should have USER_OMITTED status'
464
+
465
+ # Host2 should be SYNC
466
+ assert_equal InventorySync::InventoryStatus::SYNC,
467
+ InventorySync::InventoryStatus.where(host_id: @host2.id).first.status,
468
+ 'Normal host should have SYNC status'
469
+ end
470
+
471
+ test 'user-omitted statuses are cleared before re-creating them to avoid silent create failures' do
472
+ # First sync: host1 is user-omitted
473
+ @host1.host_parameters << HostParameter.create(
474
+ name: 'host_registration_insights_inventory',
475
+ value: 'false',
476
+ parameter_type: 'boolean'
477
+ )
478
+ @host1.save!
479
+
480
+ setup_certs_expectation do
481
+ InventorySync::Async::InventoryFullSync.any_instance.stubs(:candlepin_id_cert)
482
+ end
483
+ InventorySync::Async::InventoryFullSync.any_instance.expects(:query_inventory).returns(@inventory).twice
484
+ InventorySync::Async::InventoryFullSync.any_instance.expects(:affected_host_ids).returns([@host1.id, @host2.id]).twice
485
+ FactoryBot.create(:fact_value, fact_name: fact_names['virt::uuid'], value: '1234', host: @host2)
486
+
487
+ # Run first sync
488
+ action = create_and_plan_action(InventorySync::Async::InventoryFullSync, @host1.organization)
489
+ run_action(action)
490
+
491
+ @host1.reload
492
+ initial_status = InventorySync::InventoryStatus.where(host_id: @host1.id).first
493
+ assert_equal InventorySync::InventoryStatus::USER_OMITTED, initial_status.status,
494
+ 'Initial sync: host should be USER_OMITTED'
495
+ initial_status_id = initial_status.id
496
+
497
+ # Verify there's only one status record for host1
498
+ assert_equal 1, InventorySync::InventoryStatus.where(host_id: @host1.id).count,
499
+ 'Should have exactly one status record after first sync'
500
+
501
+ # Run second sync (parameter still false)
502
+ # Without clearing old user_omitted statuses, the .create would silently fail
503
+ # with 'Host has already been taken' due to uniqueness constraint
504
+ setup_certs_expectation do
505
+ InventorySync::Async::InventoryFullSync.any_instance.stubs(:candlepin_id_cert)
506
+ end
507
+
508
+ action2 = create_and_plan_action(InventorySync::Async::InventoryFullSync, @host1.organization)
509
+ run_action(action2)
510
+
511
+ @host1.reload
512
+ final_status = InventorySync::InventoryStatus.where(host_id: @host1.id).first
513
+
514
+ # Verify status is still USER_OMITTED (not stale from first sync)
515
+ assert_equal InventorySync::InventoryStatus::USER_OMITTED, final_status.status,
516
+ 'Status should be USER_OMITTED after second sync'
517
+
518
+ # Verify there's still only one status record (old one was deleted before creating new one)
519
+ assert_equal 1, InventorySync::InventoryStatus.where(host_id: @host1.id).count,
520
+ 'Should have exactly one status record (old cleared before new created)'
521
+
522
+ # Verify the old status record was actually deleted and replaced with a new one
523
+ refute InventorySync::InventoryStatus.exists?(initial_status_id),
524
+ 'Old status record should have been deleted before creating new one'
525
+ assert_not_equal initial_status_id, final_status.id,
526
+ 'New status record should have different ID, proving old was deleted'
527
+ end
316
528
  end
@@ -72,4 +72,113 @@ class InsightsClientReportStatusTest < ActiveSupport::TestCase
72
72
 
73
73
  assert_equal HostStatus::Global::ERROR, @host.global_status
74
74
  end
75
+
76
+ test 'host with host_registration_insights parameter set to false gets USER_OMITTED status' do
77
+ @host.host_parameters << HostParameter.create(
78
+ name: 'host_registration_insights',
79
+ value: 'false',
80
+ parameter_type: 'boolean'
81
+ )
82
+ @host.save!
83
+
84
+ insights_status = @host.get_status(InsightsClientReportStatus)
85
+ insights_status.refresh!
86
+
87
+ assert_equal InsightsClientReportStatus::USER_OMITTED, insights_status.status
88
+ assert_equal HostStatus::Global::OK, insights_status.to_global
89
+ end
90
+
91
+ test 'USER_OMITTED status has correct label' do
92
+ insights_status = @host.get_status(InsightsClientReportStatus)
93
+ insights_status.status = InsightsClientReportStatus::USER_OMITTED
94
+ insights_status.save!
95
+
96
+ label = insights_status.to_label
97
+ assert_match(/host_registration_insights/, label)
98
+ assert_match(/false/, label)
99
+ end
100
+
101
+ test 'stale scope excludes USER_OMITTED hosts' do
102
+ host1 = FactoryBot.create(:host, :managed)
103
+ host2 = FactoryBot.create(:host, :managed)
104
+ host3 = FactoryBot.create(:host, :managed)
105
+
106
+ # Host 1: USER_OMITTED with old reported_at (should NOT be in stale scope)
107
+ status1 = host1.get_status(InsightsClientReportStatus)
108
+ status1.status = InsightsClientReportStatus::USER_OMITTED
109
+ status1.reported_at = Time.zone.now - InsightsClientReportStatus::REPORT_INTERVAL - 1.day
110
+ status1.save!
111
+
112
+ # Host 2: REPORTING with old reported_at (should be in stale scope)
113
+ status2 = host2.get_status(InsightsClientReportStatus)
114
+ status2.status = InsightsClientReportStatus::REPORTING
115
+ status2.reported_at = Time.zone.now - InsightsClientReportStatus::REPORT_INTERVAL - 1.day
116
+ status2.save!
117
+
118
+ # Host 3: NO_REPORT with old reported_at (should be in stale scope)
119
+ status3 = host3.get_status(InsightsClientReportStatus)
120
+ status3.status = InsightsClientReportStatus::NO_REPORT
121
+ status3.reported_at = Time.zone.now - InsightsClientReportStatus::REPORT_INTERVAL - 1.day
122
+ status3.save!
123
+
124
+ stale_statuses = InsightsClientReportStatus.stale
125
+ stale_host_ids = stale_statuses.pluck(:host_id)
126
+
127
+ assert_not_includes stale_host_ids, host1.id, 'USER_OMITTED host should not be in stale scope'
128
+ assert_includes stale_host_ids, host2.id, 'REPORTING host with old report should be in stale scope'
129
+ assert_includes stale_host_ids, host3.id, 'NO_REPORT host with old report should be in stale scope'
130
+ end
131
+
132
+ test 'USER_OMITTED status respects parameter inheritance from hostgroup' do
133
+ # Create a hostgroup with parameter = false
134
+ hostgroup = FactoryBot.create(:hostgroup)
135
+ hostgroup.group_parameters << GroupParameter.create(
136
+ name: 'host_registration_insights',
137
+ value: 'false',
138
+ key_type: 'boolean'
139
+ )
140
+ hostgroup.save!
141
+
142
+ @host.hostgroup = hostgroup
143
+ @host.save!
144
+
145
+ # Verify parameter is inherited (not set directly on host)
146
+ assert_nil @host.parameters.find_by(name: 'host_registration_insights'),
147
+ 'Test setup: parameter should not be set directly on host'
148
+
149
+ insights_status = @host.get_status(InsightsClientReportStatus)
150
+ insights_status.refresh!
151
+
152
+ assert_equal InsightsClientReportStatus::USER_OMITTED, insights_status.status,
153
+ 'Status should be USER_OMITTED when host_registration_insights=false is inherited from hostgroup'
154
+ assert_equal HostStatus::Global::OK, insights_status.to_global,
155
+ 'USER_OMITTED status should not affect global status'
156
+ end
157
+
158
+ test 'host parameter overrides inherited parameter from hostgroup' do
159
+ # Create a hostgroup with parameter = false
160
+ hostgroup = FactoryBot.create(:hostgroup)
161
+ hostgroup.group_parameters << GroupParameter.create(
162
+ name: 'host_registration_insights',
163
+ value: 'false',
164
+ key_type: 'boolean'
165
+ )
166
+ hostgroup.save!
167
+
168
+ @host.hostgroup = hostgroup
169
+
170
+ # Override with host parameter = true
171
+ @host.host_parameters << HostParameter.create(
172
+ name: 'host_registration_insights',
173
+ value: 'true',
174
+ parameter_type: 'boolean'
175
+ )
176
+ @host.save!
177
+
178
+ insights_status = @host.get_status(InsightsClientReportStatus)
179
+ insights_status.refresh!
180
+
181
+ assert_not_equal InsightsClientReportStatus::USER_OMITTED, insights_status.status,
182
+ 'Status should not be USER_OMITTED when host parameter overrides hostgroup parameter with true'
183
+ end
75
184
  end
@@ -0,0 +1,85 @@
1
+ require 'test_plugin_helper'
2
+
3
+ module InventorySync
4
+ class InventoryStatusTest < ActiveSupport::TestCase
5
+ setup do
6
+ @host = FactoryBot.create(:host, :managed)
7
+ end
8
+
9
+ test 'status constants are defined correctly' do
10
+ assert_equal 0, InventorySync::InventoryStatus::DISCONNECT
11
+ assert_equal 1, InventorySync::InventoryStatus::SYNC
12
+ assert_equal 2, InventorySync::InventoryStatus::USER_OMITTED
13
+ end
14
+
15
+ test 'to_global returns OK for USER_OMITTED status' do
16
+ status = @host.get_status(InventorySync::InventoryStatus)
17
+ status.status = InventorySync::InventoryStatus::USER_OMITTED
18
+ status.save!
19
+
20
+ assert_equal HostStatus::Global::OK, status.to_global
21
+ end
22
+
23
+ test 'to_global returns WARN for DISCONNECT status' do
24
+ status = @host.get_status(InventorySync::InventoryStatus)
25
+ status.status = InventorySync::InventoryStatus::DISCONNECT
26
+ status.save!
27
+
28
+ assert_equal HostStatus::Global::WARN, status.to_global
29
+ end
30
+
31
+ test 'to_global returns OK for SYNC status' do
32
+ status = @host.get_status(InventorySync::InventoryStatus)
33
+ status.status = InventorySync::InventoryStatus::SYNC
34
+ status.save!
35
+
36
+ assert_equal HostStatus::Global::OK, status.to_global
37
+ end
38
+
39
+ test 'to_label returns appropriate messages for each status' do
40
+ status = @host.get_status(InventorySync::InventoryStatus)
41
+
42
+ # Test DISCONNECT label
43
+ status.status = InventorySync::InventoryStatus::DISCONNECT
44
+ status.save!
45
+ label = status.to_label
46
+ assert_match(/not present/, label.downcase)
47
+ assert_match(/console\.redhat\.com/, label)
48
+
49
+ # Test SYNC label
50
+ status.status = InventorySync::InventoryStatus::SYNC
51
+ status.save!
52
+ label = status.to_label
53
+ assert_match(/uploaded.*present/, label.downcase)
54
+ assert_match(/console\.redhat\.com/, label)
55
+
56
+ # Test USER_OMITTED label
57
+ status.status = InventorySync::InventoryStatus::USER_OMITTED
58
+ status.save!
59
+ label = status.to_label
60
+ assert_match(/excluded/, label.downcase)
61
+ assert_match(/host parameter/, label.downcase)
62
+ assert_match(/console\.redhat\.com/, label)
63
+ end
64
+
65
+ test 'relevant? returns true in regular (non-IoP) mode' do
66
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
67
+
68
+ status = @host.get_status(InventorySync::InventoryStatus)
69
+ status.status = InventorySync::InventoryStatus::SYNC
70
+ status.save!
71
+
72
+ assert status.relevant?, 'Inventory status should be relevant in regular mode'
73
+ end
74
+
75
+ test 'relevant? returns false in IoP mode' do
76
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
77
+
78
+ status = @host.get_status(InventorySync::InventoryStatus)
79
+ status.status = InventorySync::InventoryStatus::SYNC
80
+ status.save!
81
+
82
+ refute status.relevant?, 'Inventory status should NOT be relevant in IoP mode'
83
+ end
84
+ end
85
+ end
@@ -342,4 +342,64 @@ class RhCloudHostTest < ActiveSupport::TestCase
342
342
  assert_not_includes results, host1
343
343
  end
344
344
  end
345
+
346
+ test 'scoped search for user_omitted inventory status works' do
347
+ host1 = FactoryBot.create(:host, :managed)
348
+ host2 = FactoryBot.create(:host, :managed)
349
+ host3 = FactoryBot.create(:host, :managed)
350
+
351
+ # Create different inventory statuses
352
+ InventorySync::InventoryStatus.create!(
353
+ host_id: host1.id,
354
+ status: InventorySync::InventoryStatus::SYNC,
355
+ reported_at: Time.zone.now
356
+ )
357
+
358
+ InventorySync::InventoryStatus.create!(
359
+ host_id: host2.id,
360
+ status: InventorySync::InventoryStatus::DISCONNECT,
361
+ reported_at: Time.zone.now
362
+ )
363
+
364
+ InventorySync::InventoryStatus.create!(
365
+ host_id: host3.id,
366
+ status: InventorySync::InventoryStatus::USER_OMITTED,
367
+ reported_at: Time.zone.now
368
+ )
369
+
370
+ # Search for user_omitted status
371
+ results = Host.search_for('insights_inventory_sync_status = user_omitted')
372
+ result_ids = results.pluck(:id)
373
+
374
+ assert_includes result_ids, host3.id, 'Host with USER_OMITTED status should be in search results'
375
+ assert_not_includes result_ids, host1.id, 'Host with SYNC status should not be in search results'
376
+ assert_not_includes result_ids, host2.id, 'Host with DISCONNECT status should not be in search results'
377
+ end
378
+
379
+ test 'scoped search for user_omitted insights client report status works' do
380
+ host1 = FactoryBot.create(:host, :managed)
381
+ host2 = FactoryBot.create(:host, :managed)
382
+ host3 = FactoryBot.create(:host, :managed)
383
+
384
+ # Create different insights client report statuses
385
+ status1 = host1.get_status(InsightsClientReportStatus)
386
+ status1.status = InsightsClientReportStatus::REPORTING
387
+ status1.save!
388
+
389
+ status2 = host2.get_status(InsightsClientReportStatus)
390
+ status2.status = InsightsClientReportStatus::NO_REPORT
391
+ status2.save!
392
+
393
+ status3 = host3.get_status(InsightsClientReportStatus)
394
+ status3.status = InsightsClientReportStatus::USER_OMITTED
395
+ status3.save!
396
+
397
+ # Search for user_omitted status
398
+ results = Host.search_for('insights_client_report_status = user_omitted')
399
+ result_ids = results.pluck(:id)
400
+
401
+ assert_includes result_ids, host3.id, 'Host with USER_OMITTED status should be in search results'
402
+ assert_not_includes result_ids, host1.id, 'Host with REPORTING status should not be in search results'
403
+ assert_not_includes result_ids, host2.id, 'Host with NO_REPORT status should not be in search results'
404
+ end
345
405
  end
@@ -39,14 +39,20 @@ export const setupInventorySyncTaskPolling = (id, dispatch) =>
39
39
  key: INVENTORY_SYNC_TASK_UPDATE,
40
40
  onTaskSuccess: ({
41
41
  output: {
42
- host_statuses: { sync, disconnect },
42
+ host_statuses: { sync, disconnect, user_omitted: userOmitted },
43
43
  },
44
44
  }) =>
45
45
  dispatch(
46
46
  addToast({
47
47
  sticky: true,
48
48
  type: 'success',
49
- message: <Toast syncHosts={sync} disconnectHosts={disconnect} />,
49
+ message: (
50
+ <Toast
51
+ syncHosts={sync}
52
+ disconnectHosts={disconnect}
53
+ userOmittedHosts={userOmitted}
54
+ />
55
+ ),
50
56
  })
51
57
  ),
52
58
  dispatch,
@@ -9,6 +9,7 @@ Array [
9
9
  "message": <Toast
10
10
  disconnectHosts={2}
11
11
  syncHosts={0}
12
+ userOmittedHosts={1}
12
13
  />,
13
14
  "sticky": true,
14
15
  "type": "success",
@@ -30,6 +30,7 @@ describe('SyncButton integration test', () => {
30
30
  host_statuses: {
31
31
  sync: 0,
32
32
  disconnect: 2,
33
+ user_omitted: 1,
33
34
  },
34
35
  },
35
36
  result: 'success',
@@ -1,33 +1,55 @@
1
1
  import React from 'react';
2
+ import { Link } from 'react-router-dom';
2
3
  import PropTypes from 'prop-types';
3
4
  import { translate as __ } from 'foremanReact/common/I18n';
4
- import { foremanUrl } from '../../../../../../ForemanRhCloudHelpers';
5
5
 
6
- const Toast = ({ syncHosts, disconnectHosts }) => {
7
- const totalHosts = syncHosts + disconnectHosts;
6
+ const statusSearchParams = statusName =>
7
+ `/new/hosts?search=insights_inventory_sync_status+%3D+${statusName}&page=1`;
8
+ const DISCONNECT = 'disconnect';
9
+ const SYNC = 'sync';
10
+ const USER_OMITTED = 'user_omitted';
11
+ const HostsWithStatusLink = ({ statusName, children }) => (
12
+ <Link to={statusSearchParams(statusName)}>{children}</Link>
13
+ );
14
+ HostsWithStatusLink.propTypes = {
15
+ statusName: PropTypes.string.isRequired,
16
+ children: PropTypes.node.isRequired,
17
+ };
18
+
19
+ const Toast = ({ syncHosts, disconnectHosts, userOmittedHosts }) => {
20
+ const totalHosts = syncHosts + disconnectHosts + userOmittedHosts;
8
21
  return (
9
22
  <span>
10
23
  <p>
11
- {__('Hosts with subscription in organization: ')}
12
- <strong>{totalHosts}</strong>
24
+ {__('Registered hosts in organization: ')}
25
+ <Link to="/new/hosts?search=set%3F+subscription_uuid&page=1">
26
+ {totalHosts}
27
+ </Link>
13
28
  </p>
14
29
  <p>
15
- {__('Successfully synced hosts: ')}
16
- <strong>{syncHosts}</strong>
30
+ {__('Uploaded and present on console.redhat.com Inventory service: ')}
31
+ <HostsWithStatusLink statusName={SYNC}>{syncHosts}</HostsWithStatusLink>
17
32
  </p>
18
33
  <p>
19
- {__('Disconnected hosts: ')}
20
- <strong>{disconnectHosts}</strong>
34
+ {__('Not present on console.redhat.com Inventory service: ')}
35
+ <HostsWithStatusLink statusName={DISCONNECT}>
36
+ {disconnectHosts}
37
+ </HostsWithStatusLink>
21
38
  </p>
39
+ {!!userOmittedHosts && (
40
+ <p>
41
+ {__(
42
+ 'Excluded from upload to console.redhat.com Inventory service because host_registration_insights_inventory parameter value is false: '
43
+ )}
44
+ <HostsWithStatusLink statusName={USER_OMITTED}>
45
+ {userOmittedHosts}
46
+ </HostsWithStatusLink>
47
+ </p>
48
+ )}
22
49
  <p>
23
- {__('For more info, please visit the')}{' '}
24
- <a
25
- href={foremanUrl('/hosts')}
26
- target="_blank"
27
- rel="noopener noreferrer"
28
- >
29
- {__('hosts page')}
30
- </a>
50
+ {__(
51
+ 'You can review this information later by looking at the Inventory status of each host.'
52
+ )}
31
53
  </p>
32
54
  </span>
33
55
  );
@@ -36,6 +58,10 @@ const Toast = ({ syncHosts, disconnectHosts }) => {
36
58
  Toast.propTypes = {
37
59
  syncHosts: PropTypes.number.isRequired,
38
60
  disconnectHosts: PropTypes.number.isRequired,
61
+ userOmittedHosts: PropTypes.number,
62
+ };
63
+ Toast.defaultProps = {
64
+ userOmittedHosts: 0,
39
65
  };
40
66
 
41
67
  export default Toast;
@@ -0,0 +1,82 @@
1
+ import React from 'react';
2
+ import { shallow } from '@theforeman/test';
3
+ import Toast from '../Toast';
4
+
5
+ describe('Toast', () => {
6
+ it('renders with all three status counts including user_omitted', () => {
7
+ const wrapper = shallow(
8
+ <Toast syncHosts={5} disconnectHosts={3} userOmittedHosts={2} />
9
+ );
10
+
11
+ const links = wrapper.find('HostsWithStatusLink');
12
+ expect(links).toHaveLength(3);
13
+
14
+ // Check the children (numbers) of each link
15
+ expect(
16
+ links
17
+ .at(0)
18
+ .children()
19
+ .text()
20
+ ).toBe('5');
21
+ expect(
22
+ links
23
+ .at(1)
24
+ .children()
25
+ .text()
26
+ ).toBe('3');
27
+ expect(
28
+ links
29
+ .at(2)
30
+ .children()
31
+ .text()
32
+ ).toBe('2');
33
+ });
34
+
35
+ it('does not render user_omitted section when count is 0', () => {
36
+ const wrapper = shallow(
37
+ <Toast syncHosts={5} disconnectHosts={3} userOmittedHosts={0} />
38
+ );
39
+
40
+ // Should have only 2 HostsWithStatusLink components (sync and disconnect)
41
+ const links = wrapper.find('HostsWithStatusLink');
42
+ expect(links).toHaveLength(2);
43
+
44
+ // Should not contain the user_omitted explanation text
45
+ expect(wrapper.text()).not.toContain(
46
+ 'host_registration_insights_inventory parameter value is false'
47
+ );
48
+ });
49
+
50
+ it('renders without crashing when userOmittedHosts is not provided (default)', () => {
51
+ const wrapper = shallow(<Toast syncHosts={5} disconnectHosts={3} />);
52
+
53
+ // Should use default value of 0, so only 2 links
54
+ const links = wrapper.find('HostsWithStatusLink');
55
+ expect(links).toHaveLength(2);
56
+
57
+ // Verify the count values
58
+ expect(
59
+ links
60
+ .at(0)
61
+ .children()
62
+ .text()
63
+ ).toBe('5');
64
+ expect(
65
+ links
66
+ .at(1)
67
+ .children()
68
+ .text()
69
+ ).toBe('3');
70
+ });
71
+
72
+ it('renders correct status links for each category', () => {
73
+ const wrapper = shallow(
74
+ <Toast syncHosts={5} disconnectHosts={3} userOmittedHosts={2} />
75
+ );
76
+
77
+ const links = wrapper.find('HostsWithStatusLink');
78
+ expect(links.at(0).prop('statusName')).toBe('sync');
79
+ expect(links.at(1).prop('statusName')).toBe('disconnect');
80
+ expect(links.at(2).prop('statusName')).toBe('user_omitted');
81
+ });
82
+ });
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_rh_cloud
3
3
  version: !ruby/object:Gem::Version
4
- version: 13.2.8
4
+ version: 13.2.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Red Hat Cloud team
@@ -101,6 +101,7 @@ files:
101
101
  - app/controllers/concerns/foreman_rh_cloud/iop_smart_proxy_access.rb
102
102
  - app/controllers/concerns/foreman_rh_cloud/registration_manager_extensions.rb
103
103
  - app/controllers/concerns/insights_cloud/candlepin_cache.rb
104
+ - app/controllers/concerns/insights_cloud/candlepin_proxies_extensions.rb
104
105
  - app/controllers/concerns/insights_cloud/client_authentication.rb
105
106
  - app/controllers/concerns/insights_cloud/package_profile_upload_extensions.rb
106
107
  - app/controllers/concerns/inventory_upload/report_actions.rb
@@ -248,6 +249,7 @@ files:
248
249
  - test/controllers/insights_cloud/api/advisor_engine_controller_test.rb
249
250
  - test/controllers/insights_cloud/api/cloud_request_controller_test.rb
250
251
  - test/controllers/insights_cloud/api/machine_telemetries_controller_test.rb
252
+ - test/controllers/insights_cloud/candlepin_proxies_extensions_test.rb
251
253
  - test/controllers/insights_cloud/ui_requests_controller_test.rb
252
254
  - test/controllers/insights_sync/settings_controller_test.rb
253
255
  - test/controllers/inventory_upload/api/inventory_controller_test.rb
@@ -274,6 +276,7 @@ files:
274
276
  - test/jobs/single_host_report_job_test.rb
275
277
  - test/jobs/upload_report_direct_job_test.rb
276
278
  - test/models/insights_client_report_status_test.rb
279
+ - test/models/inventory_sync/inventory_status_test.rb
277
280
  - test/test_plugin_helper.rb
278
281
  - test/unit/archived_report_generator_test.rb
279
282
  - test/unit/fact_helpers_test.rb
@@ -448,6 +451,7 @@ files:
448
451
  - webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/__snapshots__/integrations.test.js.snap
449
452
  - webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/integrations.test.js
450
453
  - webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/components/Toast.js
454
+ - webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/components/__tests__/Toast.test.js
451
455
  - webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/index.js
452
456
  - webpack/ForemanInventoryUpload/Components/PageHeader/components/ToolbarButtons/ToolbarButtons.js
453
457
  - webpack/ForemanInventoryUpload/Components/PageHeader/components/ToolbarButtons/__tests__/ToolbarButtons.test.js
@@ -677,6 +681,7 @@ test_files:
677
681
  - test/controllers/insights_cloud/api/advisor_engine_controller_test.rb
678
682
  - test/controllers/insights_cloud/api/cloud_request_controller_test.rb
679
683
  - test/controllers/insights_cloud/api/machine_telemetries_controller_test.rb
684
+ - test/controllers/insights_cloud/candlepin_proxies_extensions_test.rb
680
685
  - test/controllers/insights_cloud/ui_requests_controller_test.rb
681
686
  - test/controllers/insights_sync/settings_controller_test.rb
682
687
  - test/controllers/inventory_upload/api/inventory_controller_test.rb
@@ -703,6 +708,7 @@ test_files:
703
708
  - test/jobs/single_host_report_job_test.rb
704
709
  - test/jobs/upload_report_direct_job_test.rb
705
710
  - test/models/insights_client_report_status_test.rb
711
+ - test/models/inventory_sync/inventory_status_test.rb
706
712
  - test/test_plugin_helper.rb
707
713
  - test/unit/archived_report_generator_test.rb
708
714
  - test/unit/fact_helpers_test.rb