foreman_rh_cloud 13.2.9 → 13.2.10

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ea9a50897e8e89678229d4357182f5cf0f487e1c7775048c7e6e363d9eb0b68
4
- data.tar.gz: 574992cb545615924d46aec3cb5f1856abaa4c287bd0ddfde4af1b23fc7c8ca0
3
+ metadata.gz: 4301f698357e719f6ba5d08677bdeaf3ccefb995835dcf86f3e253cb0c01d599
4
+ data.tar.gz: 77f3bc1ef3e85cc1d8988a53f2cd5fdcd65b99a9fb6d6da1cc5f366bbfebc6d3
5
5
  SHA512:
6
- metadata.gz: 92bf788426b36beda79e9444141f93cbe1ac00b5c51cb0c28717d23dec6c220548c0cf4d9bdebe30e8cfb3a5f27922aad3b447f5344c1226a44de1f2482e4279
7
- data.tar.gz: ed6b64ed049ea2ed4948d22a7160f5953d64cd2230353636e0075ead94331bae1d6aa9b805d64bff10b2ce5a27339bc78b3564ea3d535db1d0ed61cb3fd0e420
6
+ metadata.gz: 000fad5efc5a8bd42e03045e33bc10795a5d658aa3ef1098e6f95fa2a0476630cc85fe6b7b40929e817a4ca0d166fd88c8073b4faa972b72a84d0a146eb7aa70
7
+ data.tar.gz: 06fe93496e585831f593b8d31223577eb236adf7d1e4d90342cd203ddc9fbf9a80e9b58636b033fa04adefc806885feba8326a4e34dc38844bbaacd083df9c91
@@ -32,14 +32,10 @@ module InsightsCloud::Api
32
32
  rescue RestClient::ExceptionWithResponse => e
33
33
  response_obj = e.response.presence || e.exception
34
34
  code = response_obj.try(:code) || response_obj.try(:http_code) || 500
35
- message = 'Cloud request failed'
36
-
37
- return render json: {
38
- :message => message,
39
- :error => response_obj.to_s,
40
- :headers => {},
41
- :response => response_obj,
42
- }, status: code
35
+ upstream_content_type = response_obj.try(:headers)&.[](:content_type)
36
+ content_type = upstream_content_type&.match?(/json/) ? upstream_content_type : 'application/json'
37
+
38
+ return render body: response_obj.to_s, status: code, content_type: content_type
43
39
  rescue StandardError => e
44
40
  # Catch any other exceptions here, such as Errno::ECONNREFUSED
45
41
  logger.warn("Cloud request failed with exception: #{e}")
@@ -36,14 +36,10 @@ module InsightsCloud
36
36
  rescue RestClient::ExceptionWithResponse => e
37
37
  response_obj = e.response.presence || e.exception
38
38
  code = response_obj.try(:code) || response_obj.try(:http_code) || 500
39
- message = 'Cloud request failed'
39
+ upstream_content_type = response_obj.try(:headers)&.[](:content_type)
40
+ content_type = upstream_content_type&.match?(/json/) ? upstream_content_type : 'application/json'
40
41
 
41
- return render json: {
42
- :message => message,
43
- :error => response_obj.to_s,
44
- :headers => {},
45
- :response => response_obj,
46
- }, status: code
42
+ return render body: response_obj.to_s, status: code, content_type: content_type
47
43
  rescue StandardError => e
48
44
  # Catch any other exceptions here, such as Errno::ECONNREFUSED
49
45
  logger.warn("Cloud request failed with exception: #{e}")
@@ -114,6 +114,7 @@ module ForemanInventoryUpload
114
114
 
115
115
  def host_ips(host)
116
116
  # Determines and returns the IP addresses associated with a host, applying obfuscation if enabled.
117
+ return {} if host.nil?
117
118
 
118
119
  # If IP obfuscation is enabled for the host return a representation of obfuscated IP addresses.
119
120
  return obfuscated_ips(host) if obfuscate_ips?(host)
@@ -163,10 +164,29 @@ module ForemanInventoryUpload
163
164
 
164
165
  def hostname_match
165
166
  bash_hostname = `uname -n`.chomp
166
- foreman_hostname = ForemanRhCloud.foreman_host&.name
167
- if bash_hostname == foreman_hostname
168
- fqdn(ForemanRhCloud.foreman_host)
169
- elsif Setting[:obfuscate_inventory_hostnames]
167
+ foreman_host = ForemanRhCloud.foreman_host
168
+
169
+ # If bash hostname matches foreman_host, use fqdn
170
+ if foreman_host && bash_hostname == foreman_host.name
171
+ return fqdn(foreman_host)
172
+ end
173
+
174
+ # If no foreman_host, try foreman_host_name from Setting[:foreman_url]
175
+ unless foreman_host
176
+ foreman_hostname_from_setting = ForemanRhCloud.foreman_host_name
177
+ if foreman_hostname_from_setting
178
+ # Apply obfuscation if enabled
179
+ return obfuscate_fqdn(foreman_hostname_from_setting) if Setting[:obfuscate_inventory_hostnames]
180
+
181
+ return foreman_hostname_from_setting
182
+ end
183
+ # Otherwise fall through to bash_hostname below
184
+ # NOTE: Containerized foremanctl setups must configure Setting[:foreman_url]
185
+ # as bash hostname may not be available or meaningful in containers
186
+ end
187
+
188
+ # Fallback to bash hostname (with obfuscation if enabled)
189
+ if Setting[:obfuscate_inventory_hostnames]
170
190
  obfuscate_fqdn(bash_hostname)
171
191
  else
172
192
  bash_hostname
@@ -174,6 +194,8 @@ module ForemanInventoryUpload
174
194
  end
175
195
 
176
196
  def bios_uuid(host)
197
+ return nil if host.nil?
198
+
177
199
  value = fact_value(host, 'dmi::system::uuid') || ''
178
200
  uuid_value(value)
179
201
  end
@@ -96,7 +96,14 @@ module ForemanInventoryUpload
96
96
  end
97
97
 
98
98
  def self.inventory_self_url
99
- inventory_base_url + "?hostname_or_id=#{ForemanRhCloud.foreman_host.fqdn}"
99
+ host = ForemanRhCloud.foreman_host
100
+ hostname = host ? host.fqdn : ForemanRhCloud.foreman_host_name
101
+ if hostname.nil?
102
+ Rails.logger.warn("Cannot determine Foreman hostname for inventory sync. " \
103
+ "Please configure Setting[:foreman_url]. " \
104
+ "Containerized setups must explicitly set this.")
105
+ end
106
+ inventory_base_url + "?hostname_or_id=#{hostname}"
100
107
  end
101
108
 
102
109
  def self.host_by_id_url(host_uuid)
@@ -1,3 +1,3 @@
1
1
  module ForemanRhCloud
2
- VERSION = '13.2.9'.freeze
2
+ VERSION = '13.2.10'.freeze
3
3
  end
@@ -84,23 +84,50 @@ module ForemanRhCloud
84
84
 
85
85
  # For testing purposes we can override the default hostname with an environment variable SATELLITE_RH_CLOUD_FOREMAN_HOST
86
86
  def self.foreman_host
87
- @foreman_host ||= begin
88
- fullname = foreman_host_name
89
- ::Host.unscoped.friendly.find(fullname)
90
- rescue ActiveRecord::RecordNotFound
91
- # fullname didn't work. Let's try shortname
87
+ return @foreman_host if defined?(@foreman_host)
88
+
89
+ fullname = foreman_host_name
90
+ return @foreman_host = nil unless fullname
91
+
92
+ # Try fullname first
93
+ host = ::Host.unscoped.friendly.where(name: fullname).first
94
+
95
+ # If not found, try shortname
96
+ if host.nil?
92
97
  shortname = /(?<shortname>[^\.]*)\.?.*/.match(fullname)[:shortname]
93
- ::Host.unscoped.friendly.find(shortname)
98
+ host = ::Host.unscoped.friendly.where(name: shortname).first
94
99
  end
100
+
101
+ @foreman_host = host
95
102
  end
96
103
 
97
104
  def self.foreman_host_name
98
- ENV['SATELLITE_RH_CLOUD_FOREMAN_HOST'] || marked_foreman_host&.name || ::SmartProxy.default_capsule.name
105
+ ENV['SATELLITE_RH_CLOUD_FOREMAN_HOST'] || marked_foreman_host&.name || foreman_url_hostname
106
+ end
107
+
108
+ def self.foreman_url_hostname
109
+ return nil unless Setting[:foreman_url]
110
+
111
+ begin
112
+ # Ensure setting is a string to avoid TypeError from URI.parse
113
+ url = Setting[:foreman_url].to_s
114
+ URI.parse(url).host
115
+ rescue URI::InvalidURIError, ArgumentError, TypeError => e
116
+ Rails.logger.warn("Invalid foreman_url setting: #{e.message}")
117
+ nil
118
+ end
99
119
  end
100
120
 
101
121
  def self.marked_foreman_host
102
- ::Host.unscoped.search_for('infrastructure_facet.foreman = true').first
103
- rescue ScopedSearch::QueryNotSupported
122
+ # Find host with infrastructure_facet.foreman_instance = true
123
+ # Facets use a special mechanism in Foreman, so we query the facet table directly
124
+ return nil unless defined?(HostFacets::InfrastructureFacet)
125
+
126
+ facet = HostFacets::InfrastructureFacet.find_by(foreman_instance: true)
127
+ facet&.host
128
+ rescue ActiveRecord::StatementInvalid => e
129
+ # Table might not exist yet during migrations
130
+ Rails.logger.debug("Could not query marked foreman host: #{e.message}")
104
131
  nil
105
132
  end
106
133
 
@@ -13,7 +13,16 @@ module InsightsCloud
13
13
  end
14
14
 
15
15
  def add_satellite_notifications
16
- hits_count = InsightsHit.where(host_id: foreman_host.id).count
16
+ host = foreman_host
17
+
18
+ # Skip if no Foreman host record exists
19
+ unless host
20
+ logger.debug("Skipping Insights notifications: no Foreman host record found")
21
+ blueprint&.notifications&.destroy_all
22
+ return
23
+ end
24
+
25
+ hits_count = InsightsHit.where(host_id: host.id).count
17
26
 
18
27
  # Remove stale notifications
19
28
  blueprint.notifications.destroy_all
@@ -4,7 +4,14 @@ module InventorySync
4
4
  set_callback :step, :around, :create_facets
5
5
 
6
6
  def plan
7
- super(ForemanRhCloud.foreman_host.organization)
7
+ host = ForemanRhCloud.foreman_host
8
+
9
+ if host.nil?
10
+ logger.warn("Skipping self-host inventory sync: no Foreman host record found.")
11
+ return
12
+ end
13
+
14
+ super(host.organization)
8
15
  end
9
16
 
10
17
  def create_facets
@@ -22,7 +29,10 @@ module InventorySync
22
29
  private
23
30
 
24
31
  def add_missing_insights_facet(uuids_hash)
25
- facet = InsightsFacet.find_or_create_by(host_id: ForemanRhCloud.foreman_host.id) do |facet|
32
+ host = ForemanRhCloud.foreman_host
33
+ return unless host # Guard against nil
34
+
35
+ facet = InsightsFacet.find_or_create_by(host_id: host.id) do |facet|
26
36
  facet.uuid = uuids_hash.values.first
27
37
  end
28
38
 
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_rh_cloud",
3
- "version": "13.2.9",
3
+ "version": "13.2.10",
4
4
  "description": "Inventory Upload =============",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -154,8 +154,22 @@ module InsightsCloud::Api
154
154
 
155
155
  get :forward_request, params: { "path" => "platform/module-update-router/v1/channel" }
156
156
  assert_equal 500, @response.status
157
- assert_equal 'Cloud request failed', JSON.parse(@response.body)['message']
158
- assert_match /#{@body}/, JSON.parse(@response.body)['response']
157
+ assert_equal @body, @response.body
158
+ end
159
+
160
+ test "should forward JSON error responses without double-escaping" do
161
+ json_error = { errors: [{ detail: 'inventory_id must exist', status: '404' }] }.to_json
162
+ net_http_resp = Net::HTTPResponse.new(1.0, 404, "Not Found")
163
+ net_http_resp['content-type'] = 'application/json'
164
+ res = RestClient::Response.create(json_error, net_http_resp, @http_req)
165
+ ::ForemanRhCloud::CloudRequestForwarder.any_instance.stubs(:execute_cloud_request).raises(RestClient::NotFound.new(res))
166
+
167
+ get :forward_request, params: { "path" => "api/vulnerability/v1/systems/00000000-0000-0000-0000-000000000000" }
168
+ assert_equal 404, @response.status
169
+ assert_includes @response.content_type, 'application/json'
170
+ assert_equal json_error, @response.body
171
+ parsed = JSON.parse(@response.body)
172
+ assert_equal 'inventory_id must exist', parsed['errors'][0]['detail']
159
173
  end
160
174
 
161
175
  test "should create insights facet" do
@@ -177,8 +177,22 @@ module InsightsCloud
177
177
 
178
178
  get :forward_request, params: { "controller" => "vulnerabilities", "path" => "api/vulnerability/v1/cves" }, session: set_session
179
179
  assert_equal 500, @response.status
180
- assert_equal 'Cloud request failed', JSON.parse(@response.body)['message']
181
- assert_match(/#{@body}/, JSON.parse(@response.body)['response'])
180
+ assert_equal @body, @response.body
181
+ end
182
+
183
+ test "should forward JSON error responses without double-escaping" do
184
+ json_error = { errors: [{ detail: 'inventory_id must exist', status: '404' }] }.to_json
185
+ net_http_resp = Net::HTTPResponse.new(1.0, 404, "Not Found")
186
+ net_http_resp['content-type'] = 'application/json'
187
+ res = RestClient::Response.create(json_error, net_http_resp, @http_req)
188
+ ::ForemanRhCloud::InsightsApiForwarder.any_instance.stubs(:execute_cloud_request).raises(RestClient::NotFound.new(res))
189
+
190
+ get :forward_request, params: { "controller" => "vulnerabilities", "path" => "api/vulnerability/v1/systems/00000000-0000-0000-0000-000000000000" }, session: set_session
191
+ assert_equal 404, @response.status
192
+ assert_includes @response.content_type, 'application/json'
193
+ assert_equal json_error, @response.body
194
+ parsed = JSON.parse(@response.body)
195
+ assert_equal 'inventory_id must exist', parsed['errors'][0]['detail']
182
196
  end
183
197
 
184
198
  test "should allow forward_request with nil location (Any location)" do
@@ -0,0 +1,26 @@
1
+ require 'test_plugin_helper'
2
+ require 'foreman_tasks/test_helpers'
3
+
4
+ class InsightsGenerateNotificationsTest < ActiveSupport::TestCase
5
+ include Dynflow::Testing::Factories
6
+
7
+ setup do
8
+ User.current = User.find_by(login: 'secret_admin')
9
+ end
10
+
11
+ test 'skips notifications when foreman_host is nil' do
12
+ ForemanRhCloud.stubs(:foreman_host).returns(nil)
13
+
14
+ # Ensure blueprint exists or create it
15
+ NotificationBlueprint.find_or_create_by(name: 'insights_satellite_hits') do |bp|
16
+ bp.message = 'Test message'
17
+ bp.level = 'info'
18
+ bp.expires_in = 7.days
19
+ end
20
+
21
+ plan = ForemanTasks.sync_task(InsightsCloud::Async::InsightsGenerateNotifications)
22
+
23
+ # Should not raise an error, task completes successfully
24
+ assert_includes ['success', 'stopped'], plan.state, "Task should complete without error"
25
+ end
26
+ end
@@ -106,4 +106,13 @@ class InventorySelfHostSyncTest < ActiveSupport::TestCase
106
106
 
107
107
  assert_equal @host1_inventory_id, @host1.insights.uuid
108
108
  end
109
+
110
+ test 'skips sync when foreman_host is nil' do
111
+ ForemanRhCloud.stubs(:foreman_host).returns(nil)
112
+
113
+ plan = ForemanTasks.sync_task(InventorySync::Async::InventorySelfHostSync)
114
+
115
+ # Task should be stopped (not executed) when host is nil, not failed
116
+ assert_equal 'stopped', plan.state
117
+ end
109
118
  end
@@ -2,8 +2,9 @@ require 'test_plugin_helper'
2
2
 
3
3
  class ForemanRhCloudSelfHostTest < ActiveSupport::TestCase
4
4
  setup do
5
- # reset cached value
6
- ForemanRhCloud.instance_variable_set(:@foreman_host, nil)
5
+ # reset cached value - must remove the variable entirely, not just set to nil
6
+ ForemanRhCloud.remove_instance_variable(:@foreman_host) if ForemanRhCloud.instance_variable_defined?(:@foreman_host)
7
+ ENV.delete('SATELLITE_RH_CLOUD_FOREMAN_HOST')
7
8
  end
8
9
 
9
10
  test 'finds host by fullname' do
@@ -32,4 +33,51 @@ class ForemanRhCloudSelfHostTest < ActiveSupport::TestCase
32
33
 
33
34
  assert_equal @host, actual
34
35
  end
36
+
37
+ test 'returns nil when host does not exist' do
38
+ ForemanRhCloud.expects(:foreman_host_name).returns('nonexistent.example.com')
39
+
40
+ actual = ForemanRhCloud.foreman_host
41
+
42
+ assert_nil actual
43
+ end
44
+
45
+ test 'returns nil and does not query Host when foreman_host_name is nil' do
46
+ ForemanRhCloud.stubs(:foreman_host_name).returns(nil)
47
+ ::Host.unscoped.friendly.expects(:where).never
48
+
49
+ assert_nil ForemanRhCloud.foreman_host
50
+ end
51
+
52
+ test 'caches nil value to avoid repeated lookups' do
53
+ ForemanRhCloud.expects(:foreman_host_name).once.returns('nonexistent.example.com')
54
+
55
+ 2.times { ForemanRhCloud.foreman_host }
56
+ end
57
+
58
+ test 'extracts hostname from foreman_url setting' do
59
+ Setting[:foreman_url] = 'https://satellite.example.com'
60
+
61
+ actual = ForemanRhCloud.foreman_url_hostname
62
+
63
+ assert_equal 'satellite.example.com', actual
64
+ end
65
+
66
+ test 'handles invalid foreman_url gracefully' do
67
+ # Stub Setting to return invalid URL without validation
68
+ Setting.stubs(:[]).with(:foreman_url).returns('not a valid url')
69
+
70
+ actual = ForemanRhCloud.foreman_url_hostname
71
+
72
+ assert_nil actual
73
+ end
74
+
75
+ test 'foreman_host_name uses foreman_url when marked_foreman_host is nil' do
76
+ ForemanRhCloud.expects(:marked_foreman_host).returns(nil)
77
+ ForemanRhCloud.expects(:foreman_url_hostname).returns('satellite.example.com')
78
+
79
+ actual = ForemanRhCloud.foreman_host_name
80
+
81
+ assert_equal 'satellite.example.com', actual
82
+ end
35
83
  end
@@ -2,7 +2,8 @@ require 'test_plugin_helper'
2
2
 
3
3
  class MetadataGeneratorTest < ActiveSupport::TestCase
4
4
  setup do
5
- ForemanRhCloud.instance_variable_set(:@foreman_host, nil)
5
+ # reset cached value - must remove the variable entirely, not just set to nil
6
+ ForemanRhCloud.remove_instance_variable(:@foreman_host) if ForemanRhCloud.instance_variable_defined?(:@foreman_host)
6
7
  end
7
8
 
8
9
  test 'generates an empty report' do
@@ -64,4 +65,26 @@ class MetadataGeneratorTest < ActiveSupport::TestCase
64
65
  assert_not_nil(slice = slices['test_12345'])
65
66
  assert_equal 3, slice['number_hosts']
66
67
  end
68
+
69
+ test 'generates metadata when foreman_host is nil' do
70
+ ForemanRhCloud.stubs(:foreman_host).returns(nil)
71
+ ForemanRhCloud.stubs(:foreman_host_name).returns('satellite.example.com')
72
+
73
+ generator = ForemanInventoryUpload::Generators::Metadata.new
74
+
75
+ # Should not raise an error
76
+ json_str = nil
77
+ assert_nothing_raised do
78
+ json_str = generator.render do
79
+ end
80
+ end
81
+
82
+ # Verify hostname is from foreman_host_name
83
+ actual = JSON.parse(json_str.join("\n"))
84
+ assert_equal 'satellite.example.com', actual['reporting_host_name']
85
+ # Verify IP and BIOS UUID fields are nil when host is nil
86
+ # This is acceptable per SAT-25889 - cloud services don't rely on these fields
87
+ assert_nil actual['reporting_host_ips']
88
+ assert_nil actual['reporting_host_bios_uuid']
89
+ end
67
90
  end
@@ -3,6 +3,10 @@ import PropTypes from 'prop-types';
3
3
  import { ScalprumComponent, ScalprumProvider } from '@scalprum/react-core';
4
4
  import { createProviderOptions } from '../common/ScalprumModule/ScalprumContext';
5
5
  import { useInsightsPermissions } from '../common/Hooks/PermissionsHooks';
6
+ import {
7
+ vulnerabilityDisabled,
8
+ useTabRedirect,
9
+ } from '../ForemanRhCloudHelpers';
6
10
  import './CVEsHostDetailsTab.scss';
7
11
 
8
12
  const CVEsHostDetailsTab = ({ systemId }) => {
@@ -26,6 +30,15 @@ CVEsHostDetailsTab.propTypes = {
26
30
 
27
31
  const CVEsHostDetailsTabWrapper = ({ response }) => {
28
32
  const permissions = useInsightsPermissions();
33
+ const isHostDataLoaded = Boolean(response?.id);
34
+ const shouldHideTab = useTabRedirect(
35
+ isHostDataLoaded && vulnerabilityDisabled({ hostDetails: response })
36
+ );
37
+
38
+ if (shouldHideTab) {
39
+ return null;
40
+ }
41
+
29
42
  return (
30
43
  <ScalprumProvider {...createProviderOptions(permissions)}>
31
44
  <CVEsHostDetailsTab
@@ -38,10 +51,19 @@ const CVEsHostDetailsTabWrapper = ({ response }) => {
38
51
 
39
52
  CVEsHostDetailsTabWrapper.propTypes = {
40
53
  response: PropTypes.shape({
54
+ id: PropTypes.number,
55
+ operatingsystem_name: PropTypes.string,
56
+ vulnerability: PropTypes.shape({
57
+ enabled: PropTypes.bool,
58
+ }),
41
59
  subscription_facet_attributes: PropTypes.shape({
42
- uuid: PropTypes.string.isRequired,
60
+ uuid: PropTypes.string,
43
61
  }),
44
- }).isRequired,
62
+ }),
63
+ };
64
+
65
+ CVEsHostDetailsTabWrapper.defaultProps = {
66
+ response: {},
45
67
  };
46
68
 
47
69
  export default CVEsHostDetailsTabWrapper;
@@ -1,6 +1,17 @@
1
1
  import React from 'react';
2
2
  import { render } from '@testing-library/react';
3
+ import { MemoryRouter } from 'react-router-dom';
3
4
  import CVEsHostDetailsTabWrapper from '../CVEsHostDetailsTab';
5
+ import { OVERVIEW_TAB_PATH } from '../../ForemanRhCloudHelpers';
6
+
7
+ const mockHistoryReplace = jest.fn();
8
+
9
+ jest.mock('react-router-dom', () => ({
10
+ ...jest.requireActual('react-router-dom'),
11
+ useHistory: () => ({
12
+ replace: mockHistoryReplace,
13
+ }),
14
+ }));
4
15
 
5
16
  jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
6
17
  useForemanContext: () => ({
@@ -25,31 +36,44 @@ jest.mock('@scalprum/react-core', () => {
25
36
  };
26
37
  });
27
38
 
39
+ const defaultResponse = {
40
+ id: 1,
41
+ operatingsystem_name: 'Red Hat Enterprise Linux 8',
42
+ vulnerability: { enabled: true },
43
+ subscription_facet_attributes: { uuid: '1-2-3' },
44
+ };
45
+
28
46
  describe('CVEsHostDetailsTabWrapper', () => {
29
47
  beforeEach(() => {
30
48
  jest.clearAllMocks();
31
49
  });
32
50
 
33
- it('renders without crashing', () => {
51
+ it('renders without crashing and does not redirect for valid host', () => {
34
52
  const { container } = render(
35
- <CVEsHostDetailsTabWrapper
36
- response={{ subscription_facet_attributes: { uuid: '1-2-3' } }}
37
- />
53
+ <MemoryRouter>
54
+ <CVEsHostDetailsTabWrapper response={defaultResponse} />
55
+ </MemoryRouter>
38
56
  );
39
57
  expect(
40
58
  container.querySelector(
41
59
  '.rh-cloud-insights-vulnerability-host-details-component'
42
60
  )
43
61
  ).toBeTruthy();
62
+ expect(mockHistoryReplace).not.toHaveBeenCalled();
44
63
  });
45
64
 
46
65
  it('remounts ScalprumComponent when systemId changes', () => {
47
66
  const { ScalprumComponent } = require('@scalprum/react-core');
48
67
 
68
+ const responseHostA = {
69
+ ...defaultResponse,
70
+ subscription_facet_attributes: { uuid: 'uuid-host-A' },
71
+ };
72
+
49
73
  const { rerender } = render(
50
- <CVEsHostDetailsTabWrapper
51
- response={{ subscription_facet_attributes: { uuid: 'uuid-host-A' } }}
52
- />
74
+ <MemoryRouter>
75
+ <CVEsHostDetailsTabWrapper response={responseHostA} />
76
+ </MemoryRouter>
53
77
  );
54
78
 
55
79
  expect(mockUnmountTracker).not.toHaveBeenCalled();
@@ -58,10 +82,15 @@ describe('CVEsHostDetailsTabWrapper', () => {
58
82
  expect.anything()
59
83
  );
60
84
 
85
+ const responseHostB = {
86
+ ...defaultResponse,
87
+ subscription_facet_attributes: { uuid: 'uuid-host-B' },
88
+ };
89
+
61
90
  rerender(
62
- <CVEsHostDetailsTabWrapper
63
- response={{ subscription_facet_attributes: { uuid: 'uuid-host-B' } }}
64
- />
91
+ <MemoryRouter>
92
+ <CVEsHostDetailsTabWrapper response={responseHostB} />
93
+ </MemoryRouter>
65
94
  );
66
95
 
67
96
  expect(mockUnmountTracker).toHaveBeenCalledTimes(1);
@@ -70,4 +99,38 @@ describe('CVEsHostDetailsTabWrapper', () => {
70
99
  expect.anything()
71
100
  );
72
101
  });
102
+
103
+ it('redirects to Overview when tab should be hidden', () => {
104
+ const nonRhelResponse = {
105
+ id: 2,
106
+ operatingsystem_name: 'Ubuntu 20.04',
107
+ vulnerability: { enabled: false },
108
+ subscription_facet_attributes: { uuid: '1-2-3' },
109
+ };
110
+
111
+ const { container } = render(
112
+ <MemoryRouter>
113
+ <CVEsHostDetailsTabWrapper response={nonRhelResponse} />
114
+ </MemoryRouter>
115
+ );
116
+
117
+ expect(mockHistoryReplace).toHaveBeenCalledWith(OVERVIEW_TAB_PATH);
118
+ expect(
119
+ container.querySelector(
120
+ '.rh-cloud-insights-vulnerability-host-details-component'
121
+ )
122
+ ).toBeNull();
123
+ });
124
+
125
+ it('does not redirect when host data is not yet loaded', () => {
126
+ const emptyResponse = { subscription_facet_attributes: { uuid: '1-2-3' } };
127
+
128
+ render(
129
+ <MemoryRouter>
130
+ <CVEsHostDetailsTabWrapper response={emptyResponse} />
131
+ </MemoryRouter>
132
+ );
133
+
134
+ expect(mockHistoryReplace).not.toHaveBeenCalled();
135
+ });
73
136
  });
@@ -1,3 +1,6 @@
1
+ import { useEffect } from 'react';
2
+ import { useHistory } from 'react-router-dom';
3
+
1
4
  /**
2
5
  * copied from core, since it's not in the ReactApp folder,
3
6
  * it's complicated to import it and mock it in tests.
@@ -5,6 +8,25 @@
5
8
  */
6
9
  export const foremanUrl = path => `${window.URL_PREFIX}${path}`;
7
10
 
11
+ export const OVERVIEW_TAB_PATH = '/Overview';
12
+
13
+ /**
14
+ * Redirects to Overview tab when the current tab should be hidden
15
+ * @param {boolean} shouldRedirect - Whether to redirect (e.g., host loaded AND tab should hide)
16
+ * @returns {boolean} - Returns shouldRedirect for convenience
17
+ */
18
+ export const useTabRedirect = shouldRedirect => {
19
+ const history = useHistory();
20
+
21
+ useEffect(() => {
22
+ if (shouldRedirect && history) {
23
+ history.replace(OVERVIEW_TAB_PATH);
24
+ }
25
+ }, [shouldRedirect, history]);
26
+
27
+ return shouldRedirect;
28
+ };
29
+
8
30
  export const isNotRhelHost = ({ hostDetails }) =>
9
31
  // This regex tries matches sane variations of "RedHat", "RHEL" and "RHCOS"
10
32
  !new RegExp('red[\\s\\-]?hat|rh[\\s\\-]?el|rhc[\\s\\-]?os', 'i').test(
@@ -25,6 +25,11 @@ import { useIopConfig } from '../common/Hooks/ConfigHooks';
25
25
  import { generateRuleUrl } from '../InsightsCloudSync/InsightsCloudSync';
26
26
  import { createProviderOptions } from '../common/ScalprumModule/ScalprumContext';
27
27
  import { useInsightsPermissions } from '../common/Hooks/PermissionsHooks';
28
+ import {
29
+ isNotRhelHost,
30
+ hasNoInsightsFacet,
31
+ useTabRedirect,
32
+ } from '../ForemanRhCloudHelpers';
28
33
 
29
34
  // Hosted Insights advisor
30
35
  const NewHostDetailsTab = ({ hostName, router }) => {
@@ -165,7 +170,18 @@ const IopInsightsTabWrapped = props => {
165
170
  };
166
171
 
167
172
  const InsightsTab = props => {
173
+ const { response } = props;
168
174
  const isIop = useIopConfig();
175
+ const isHostDataLoaded = Boolean(response?.id);
176
+ const shouldHideTab = useTabRedirect(
177
+ isHostDataLoaded &&
178
+ (isNotRhelHost({ hostDetails: response }) ||
179
+ hasNoInsightsFacet({ response, hostDetails: response }))
180
+ );
181
+
182
+ if (shouldHideTab) {
183
+ return null;
184
+ }
169
185
 
170
186
  return isIop ? (
171
187
  <IopInsightsTabWrapped {...props} />
@@ -174,6 +190,16 @@ const InsightsTab = props => {
174
190
  );
175
191
  };
176
192
 
177
- InsightsTab.defaultProps = {};
193
+ InsightsTab.propTypes = {
194
+ response: PropTypes.shape({
195
+ id: PropTypes.number,
196
+ operatingsystem_name: PropTypes.string,
197
+ insights_attributes: PropTypes.object,
198
+ }),
199
+ };
200
+
201
+ InsightsTab.defaultProps = {
202
+ response: {},
203
+ };
178
204
 
179
205
  export default InsightsTab;
@@ -2,9 +2,20 @@ import React from 'react';
2
2
  import { render } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
4
  import { Provider } from 'react-redux';
5
+ import { MemoryRouter } from 'react-router-dom';
5
6
  import configureMockStore from 'redux-mock-store';
6
7
  import thunk from 'redux-thunk';
7
8
  import NewHostDetailsTab from '../NewHostDetailsTab';
9
+ import { OVERVIEW_TAB_PATH } from '../../ForemanRhCloudHelpers';
10
+
11
+ const mockHistoryReplace = jest.fn();
12
+
13
+ jest.mock('react-router-dom', () => ({
14
+ ...jest.requireActual('react-router-dom'),
15
+ useHistory: () => ({
16
+ replace: mockHistoryReplace,
17
+ }),
18
+ }));
8
19
 
9
20
  jest.mock('../../common/Hooks/ConfigHooks', () => ({
10
21
  useIopConfig: jest.fn(() => false),
@@ -14,8 +25,33 @@ jest.mock('foremanReact/common/I18n', () => ({
14
25
  translate: jest.fn(str => str),
15
26
  }));
16
27
 
28
+ jest.mock(
29
+ 'foremanReact/components/SearchBar',
30
+ () => () => <div>SearchBar</div>,
31
+ { virtual: true }
32
+ );
33
+
34
+ jest.mock('../../InsightsCloudSync/Components/InsightsTable', () => () => (
35
+ <div>InsightsTable</div>
36
+ ));
37
+
38
+ jest.mock('../../InsightsCloudSync/Components/RemediationModal', () => () => (
39
+ <div>RemediationModal</div>
40
+ ));
41
+
42
+ jest.mock(
43
+ '../../InsightsCloudSync/Components/InsightsTable/Pagination',
44
+ () => () => <div>Pagination</div>
45
+ );
46
+
17
47
  const mockStore = configureMockStore([thunk]);
18
48
 
49
+ const defaultResponse = {
50
+ id: 1,
51
+ operatingsystem_name: 'Red Hat Enterprise Linux 8',
52
+ insights_attributes: { uuid: 'test-uuid' },
53
+ };
54
+
19
55
  describe('NewHostDetailsTab', () => {
20
56
  let store;
21
57
  let mockRouter;
@@ -68,17 +104,18 @@ describe('NewHostDetailsTab', () => {
68
104
  it('should preserve hash when clearing search params on unmount', () => {
69
105
  const { unmount } = render(
70
106
  <Provider store={store}>
71
- <NewHostDetailsTab
72
- hostName="test-host.example.com"
73
- router={mockRouter}
74
- />
107
+ <MemoryRouter>
108
+ <NewHostDetailsTab
109
+ hostName="test-host.example.com"
110
+ router={mockRouter}
111
+ response={defaultResponse}
112
+ />
113
+ </MemoryRouter>
75
114
  </Provider>
76
115
  );
77
116
 
78
- // Unmount the component to trigger cleanup
79
117
  unmount();
80
118
 
81
- // Verify router.replace was called with both search: null AND the existing hash
82
119
  expect(mockRouter.replace).toHaveBeenCalledWith({
83
120
  search: null,
84
121
  hash: '#/Insights',
@@ -90,16 +127,18 @@ describe('NewHostDetailsTab', () => {
90
127
 
91
128
  const { unmount } = render(
92
129
  <Provider store={store}>
93
- <NewHostDetailsTab
94
- hostName="test-host.example.com"
95
- router={mockRouter}
96
- />
130
+ <MemoryRouter>
131
+ <NewHostDetailsTab
132
+ hostName="test-host.example.com"
133
+ router={mockRouter}
134
+ response={defaultResponse}
135
+ />
136
+ </MemoryRouter>
97
137
  </Provider>
98
138
  );
99
139
 
100
140
  unmount();
101
141
 
102
- // When there's no hash, should only pass search: null
103
142
  expect(mockRouter.replace).toHaveBeenCalledWith({
104
143
  search: null,
105
144
  });
@@ -114,16 +153,18 @@ describe('NewHostDetailsTab', () => {
114
153
 
115
154
  const { unmount } = render(
116
155
  <Provider store={store}>
117
- <NewHostDetailsTab
118
- hostName="test-host.example.com"
119
- router={routerWithoutLocation}
120
- />
156
+ <MemoryRouter>
157
+ <NewHostDetailsTab
158
+ hostName="test-host.example.com"
159
+ router={routerWithoutLocation}
160
+ response={defaultResponse}
161
+ />
162
+ </MemoryRouter>
121
163
  </Provider>
122
164
  );
123
165
 
124
166
  unmount();
125
167
 
126
- // Should still call replace with search: null even if location is undefined
127
168
  expect(routerWithoutLocation.replace).toHaveBeenCalledWith({
128
169
  search: null,
129
170
  });
@@ -132,23 +173,94 @@ describe('NewHostDetailsTab', () => {
132
173
  it('should use the latest hash value at unmount time, not a stale captured value', () => {
133
174
  const { unmount } = render(
134
175
  <Provider store={store}>
135
- <NewHostDetailsTab
136
- hostName="test-host.example.com"
137
- router={mockRouter}
138
- />
176
+ <MemoryRouter>
177
+ <NewHostDetailsTab
178
+ hostName="test-host.example.com"
179
+ router={mockRouter}
180
+ response={defaultResponse}
181
+ />
182
+ </MemoryRouter>
139
183
  </Provider>
140
184
  );
141
185
 
142
- // Change the hash after mount, before unmount
143
186
  mockRouter.location.hash = '#/Overview';
144
187
 
145
188
  unmount();
146
189
 
147
- // Verify router.replace was called with the UPDATED hash, not the initial '#/Insights'
148
190
  expect(mockRouter.replace).toHaveBeenCalledWith({
149
191
  search: null,
150
192
  hash: '#/Overview',
151
193
  });
152
194
  });
153
195
  });
196
+
197
+ describe('tab visibility', () => {
198
+ it('should redirect to Overview when host is not RHEL', () => {
199
+ const nonRhelResponse = {
200
+ id: 2,
201
+ operatingsystem_name: 'Ubuntu 20.04',
202
+ insights_attributes: { uuid: 'test-uuid' },
203
+ };
204
+
205
+ render(
206
+ <Provider store={store}>
207
+ <MemoryRouter>
208
+ <NewHostDetailsTab
209
+ hostName="test-host.example.com"
210
+ response={nonRhelResponse}
211
+ />
212
+ </MemoryRouter>
213
+ </Provider>
214
+ );
215
+
216
+ expect(mockHistoryReplace).toHaveBeenCalledWith(OVERVIEW_TAB_PATH);
217
+ });
218
+
219
+ it('should redirect to Overview when insights facet is missing', () => {
220
+ const responseWithoutInsights = {
221
+ id: 3,
222
+ operatingsystem_name: 'Red Hat Enterprise Linux 8',
223
+ };
224
+
225
+ render(
226
+ <Provider store={store}>
227
+ <MemoryRouter>
228
+ <NewHostDetailsTab
229
+ hostName="test-host.example.com"
230
+ response={responseWithoutInsights}
231
+ />
232
+ </MemoryRouter>
233
+ </Provider>
234
+ );
235
+
236
+ expect(mockHistoryReplace).toHaveBeenCalledWith(OVERVIEW_TAB_PATH);
237
+ });
238
+
239
+ it('should not redirect when host is valid RHEL with insights facet', () => {
240
+ render(
241
+ <Provider store={store}>
242
+ <MemoryRouter>
243
+ <NewHostDetailsTab
244
+ hostName="test-host.example.com"
245
+ response={defaultResponse}
246
+ />
247
+ </MemoryRouter>
248
+ </Provider>
249
+ );
250
+
251
+ expect(mockHistoryReplace).not.toHaveBeenCalled();
252
+ });
253
+
254
+ it('should not redirect when host data is not yet loaded', () => {
255
+ render(
256
+ <Provider store={store}>
257
+ <MemoryRouter>
258
+ <NewHostDetailsTab hostName="test-host.example.com" response={{}} />
259
+ </MemoryRouter>
260
+ </Provider>
261
+ );
262
+
263
+ expect(mockHistoryReplace).not.toHaveBeenCalled();
264
+ });
265
+ });
154
266
  });
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.9
4
+ version: 13.2.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Red Hat Cloud team
@@ -30,6 +30,9 @@ dependencies:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
32
  version: 10.0.0
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: 13.0.0
33
36
  type: :runtime
34
37
  prerelease: false
35
38
  version_requirements: !ruby/object:Gem::Requirement
@@ -37,6 +40,9 @@ dependencies:
37
40
  - - ">="
38
41
  - !ruby/object:Gem::Version
39
42
  version: 10.0.0
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: 13.0.0
40
46
  - !ruby/object:Gem::Dependency
41
47
  name: katello
42
48
  requirement: !ruby/object:Gem::Requirement
@@ -265,6 +271,7 @@ files:
265
271
  - test/jobs/host_inventory_report_job_test.rb
266
272
  - test/jobs/insights_client_status_aging_test.rb
267
273
  - test/jobs/insights_full_sync_test.rb
274
+ - test/jobs/insights_generate_notifications_test.rb
268
275
  - test/jobs/insights_resolutions_sync_test.rb
269
276
  - test/jobs/insights_rules_sync_test.rb
270
277
  - test/jobs/inventory_full_sync_test.rb
@@ -673,7 +680,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
673
680
  - !ruby/object:Gem::Version
674
681
  version: '0'
675
682
  requirements: []
676
- rubygems_version: 4.0.6
683
+ rubygems_version: 4.0.16
677
684
  specification_version: 4
678
685
  summary: Summary of ForemanRhCloud.
679
686
  test_files:
@@ -697,6 +704,7 @@ test_files:
697
704
  - test/jobs/host_inventory_report_job_test.rb
698
705
  - test/jobs/insights_client_status_aging_test.rb
699
706
  - test/jobs/insights_full_sync_test.rb
707
+ - test/jobs/insights_generate_notifications_test.rb
700
708
  - test/jobs/insights_resolutions_sync_test.rb
701
709
  - test/jobs/insights_rules_sync_test.rb
702
710
  - test/jobs/inventory_full_sync_test.rb