foreman_rh_cloud 14.1.0 → 14.1.2

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: ecc86813dfba949a2d7125c96245b767f5ecc4edee058828bac97042ce5d4ee0
4
- data.tar.gz: e6d920065bbe1a0a560c49050614abbad1c72d4f4c5e1494edf390db0495833e
3
+ metadata.gz: 8561ef219578d2be3678f2576fba36e1cb3f2a91aab82b5438369aa08ddd0a05
4
+ data.tar.gz: 6abcf201d049e27b6dc90fef6f5094a0f6f72d337f49aa6cdf328d8aa92e3516
5
5
  SHA512:
6
- metadata.gz: e672658acda2399401392268c238361a81720e58d6d5693e70b1195c8aaef4d22ee35918b1ee329fae40d03efb09ad77c5562e9d81a905bd9e3b86945ed5a7f7
7
- data.tar.gz: 1ff3460a85b2a26b64de9a472dcaf864bcb9cdd71043736a94f7f0784eb48ed91dcee94c84ae7ad4ae3c10bd31df8addd89175cf7c9ff6b221b8fadbe5035391
6
+ metadata.gz: cf0448da7b88a0ddca0e2726805b9c0fb6e5356651e64ac09d0a18cd16d05a393dab07f4a19c445948a39fd1a4111e17c275c8ffaf33df974cc80a9af1693ba0
7
+ data.tar.gz: 7e780f06f64dc1f5042a10ec48f3c397d5b767bc5b6ac2c08ac7d9cc059518df4415ee442b37216777a471a0286d59a73a56142a6b80cb10b04213d477a9c4d7
@@ -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}")
@@ -1,11 +1,24 @@
1
1
  module ForemanRhCloud
2
2
  class URLRemediationsRetriever < RemediationsRetriever
3
- attr_reader :url, :payload, :headers
3
+ attr_reader :url, :headers
4
4
 
5
5
  def initialize(url:, organization_id:, payload: '', headers: {}, logger: Logger.new(IO::NULL))
6
6
  super(logger: logger)
7
7
 
8
- @url = url
8
+ parsed_url = URI.parse(url)
9
+ query_params = parsed_url.query ? CGI.parse(parsed_url.query) : {}
10
+ hosts_param = query_params.delete('hosts')
11
+
12
+ if hosts_param.present?
13
+ @host_uuids = hosts_param.flat_map { |v| v.split(',') }.map(&:strip).reject(&:blank?)
14
+ @host_uuids = nil if @host_uuids.empty?
15
+ parsed_url.query = query_params.any? ? URI.encode_www_form(query_params) : nil
16
+ @url = parsed_url.to_s
17
+ else
18
+ @host_uuids = nil
19
+ @url = url
20
+ end
21
+
9
22
  @payload = payload
10
23
  @headers = headers
11
24
  @organization_id = organization_id
@@ -14,7 +27,7 @@ module ForemanRhCloud
14
27
  private
15
28
 
16
29
  def query_playbook
17
- logger.debug("Querying playbook at: #{url} with payload: #{payload} and headers: #{headers}")
30
+ logger.debug("Querying playbook via #{method.to_s.upcase} at: #{url} with payload: #{payload} and headers: #{headers}")
18
31
 
19
32
  super
20
33
  end
@@ -28,11 +41,12 @@ module ForemanRhCloud
28
41
  end
29
42
 
30
43
  def payload
31
- @payload.present? ? @payload.to_json : @payload # don't run .to_json if @payload is ''
44
+ return @host_uuids.to_json if @host_uuids.present?
45
+ @payload.present? ? @payload.to_json : @payload
32
46
  end
33
47
 
34
48
  def method
35
- :get
49
+ @host_uuids.present? ? :post : :get
36
50
  end
37
51
 
38
52
  def organization
@@ -0,0 +1,49 @@
1
+ module ForemanInventoryUpload
2
+ module Async
3
+ class DestroyOrganizationHbiHostsJob < ::Actions::EntryAction
4
+ include ForemanRhCloud::CertAuth
5
+
6
+ def plan(organization_id)
7
+ plan_self(organization_id: organization_id)
8
+ end
9
+
10
+ def run
11
+ org = Organization.find_by(id: input[:organization_id])
12
+ unless org
13
+ output[:result] = _("Organization not found")
14
+ return
15
+ end
16
+
17
+ logger.info("Destroying all HBI hosts for organization #{org.label} (id: #{org.id})")
18
+
19
+ execute_cloud_request(
20
+ organization: org,
21
+ method: :delete,
22
+ url: ForemanInventoryUpload.hosts_delete_all_url,
23
+ headers: {
24
+ content_type: :json,
25
+ }
26
+ )
27
+
28
+ output[:result] = format(_("Successfully deleted all HBI hosts for organization %s"), org.label)
29
+ rescue RestClient::NotFound
30
+ output[:result] = format(_("No HBI hosts found for organization %s"), org&.label)
31
+ rescue StandardError => e
32
+ logger.error(format(_("Failed to destroy HBI hosts for organization %s: %s"), org&.label, e.message))
33
+ raise
34
+ end
35
+
36
+ def logger
37
+ Foreman::Logging.logger('background')
38
+ end
39
+
40
+ def rescue_strategy
41
+ Dynflow::Action::Rescue::Skip
42
+ end
43
+
44
+ def humanized_name
45
+ _("Destroy HBI hosts for organization")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -103,6 +103,10 @@ module ForemanInventoryUpload
103
103
  "#{inventory_base_url}/#{host_uuid}"
104
104
  end
105
105
 
106
+ def self.hosts_delete_all_url
107
+ "#{inventory_base_url}/all?confirm_delete_all=true"
108
+ end
109
+
106
110
  def self.hosts_by_ids_url(host_uuids)
107
111
  host_ids_string = host_uuids.join(',')
108
112
  "#{inventory_base_url}/#{host_ids_string}"
@@ -42,6 +42,7 @@ module ForemanRhCloud
42
42
  ::Katello::Api::Rhsm::CandlepinDynflowProxyController.include InsightsCloud::PackageProfileUploadExtensions
43
43
  ::Katello::Api::Rhsm::CandlepinProxiesController.include InsightsCloud::CandlepinProxiesExtensions
44
44
  ::Katello::RegistrationManager.singleton_class.prepend ::ForemanRhCloud::RegistrationManagerExtensions
45
+ ::Actions::Katello::Organization::Destroy.prepend ::ForemanRhCloud::OrganizationDestroyExtensions
45
46
  end
46
47
  end
47
48
 
@@ -0,0 +1,10 @@
1
+ module ForemanRhCloud
2
+ module OrganizationDestroyExtensions
3
+ extend ActiveSupport::Concern
4
+
5
+ def remove_consumers(organization)
6
+ plan_action(ForemanInventoryUpload::Async::DestroyOrganizationHbiHostsJob, organization.id) if ForemanRhCloud.with_iop_smart_proxy?
7
+ super
8
+ end
9
+ end
10
+ end
@@ -1,3 +1,3 @@
1
1
  module ForemanRhCloud
2
- VERSION = '14.1.0'.freeze
2
+ VERSION = '14.1.2'.freeze
3
3
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_rh_cloud",
3
- "version": "14.1.0",
3
+ "version": "14.1.2",
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,63 @@
1
+ require 'test_plugin_helper'
2
+ require 'foreman_tasks/test_helpers'
3
+
4
+ class DestroyOrganizationHbiHostsJobTest < ActiveSupport::TestCase
5
+ include Dynflow::Testing::Factories
6
+
7
+ setup do
8
+ User.current = User.find_by(login: 'secret_admin')
9
+
10
+ Organization.any_instance.stubs(:manifest_expired?).returns(false)
11
+ @org = FactoryBot.create(:organization)
12
+ end
13
+
14
+ test 'Deletes all HBI hosts for organization' do
15
+ expected_url = ForemanInventoryUpload.hosts_delete_all_url
16
+
17
+ ForemanInventoryUpload::Async::DestroyOrganizationHbiHostsJob.any_instance.expects(:execute_cloud_request).with do |params|
18
+ params[:organization] == @org &&
19
+ params[:method] == :delete &&
20
+ params[:url] == expected_url &&
21
+ params[:headers][:content_type] == :json
22
+ end.returns(mock_response)
23
+
24
+ action = create_and_plan_action(ForemanInventoryUpload::Async::DestroyOrganizationHbiHostsJob, @org.id)
25
+ action = run_action(action)
26
+
27
+ assert_match(/Successfully deleted/, action.output[:result])
28
+ end
29
+
30
+ test 'Handles RestClient::NotFound gracefully' do
31
+ ForemanInventoryUpload::Async::DestroyOrganizationHbiHostsJob.any_instance.expects(:execute_cloud_request).raises(RestClient::NotFound)
32
+
33
+ action = create_and_plan_action(ForemanInventoryUpload::Async::DestroyOrganizationHbiHostsJob, @org.id)
34
+ action = run_action(action)
35
+
36
+ assert_match(/No HBI hosts found/, action.output[:result])
37
+ end
38
+
39
+ test 'Raises on other errors' do
40
+ ForemanInventoryUpload::Async::DestroyOrganizationHbiHostsJob.any_instance.expects(:execute_cloud_request).raises(
41
+ RestClient::InternalServerError.new
42
+ )
43
+
44
+ action = create_and_plan_action(ForemanInventoryUpload::Async::DestroyOrganizationHbiHostsJob, @org.id)
45
+
46
+ assert_raises(RestClient::InternalServerError) do
47
+ run_action(action)
48
+ end
49
+ end
50
+
51
+ test 'hosts_delete_all_url returns correct format' do
52
+ url = ForemanInventoryUpload.hosts_delete_all_url
53
+
54
+ assert_match %r{/hosts/all\?confirm_delete_all=true$}, url
55
+ end
56
+
57
+ def mock_response(code: 200, body: '')
58
+ response = mock('response')
59
+ response.stubs(:code).returns(code)
60
+ response.stubs(:body).returns(body)
61
+ response
62
+ end
63
+ end
@@ -0,0 +1,50 @@
1
+ require 'test_plugin_helper'
2
+ require 'foreman_tasks/test_helpers'
3
+
4
+ class OrganizationDestroyExtensionsTest < ActiveSupport::TestCase
5
+ include Dynflow::Testing
6
+
7
+ setup do
8
+ User.current = User.find_by(login: 'secret_admin')
9
+
10
+ @organization = stub
11
+ @organization.stubs(:label).returns('test_org')
12
+ @organization.stubs(:id).returns(1)
13
+ @organization.stubs(:validate_destroy).returns([])
14
+ @organization.stubs(:products).returns([])
15
+ @organization.stubs(:activation_keys).returns([])
16
+ @organization.stubs(:content_views).returns(stub(:non_default => []))
17
+ @organization.stubs(:default_content_view).returns(stub(:content_view_environments => []))
18
+ @organization.stubs(:promotion_paths).returns([])
19
+ @organization.stubs(:providers).returns([])
20
+ @organization.stubs(:library).returns(stub(:destroy! => true))
21
+
22
+ where_clause = mock
23
+ where_clause.stubs(:where).returns([])
24
+ ::Host.stubs(:unscoped).returns(where_clause)
25
+ end
26
+
27
+ teardown do
28
+ ForemanRhCloud.unstub(:with_iop_smart_proxy?)
29
+ end
30
+
31
+ test 'plans DestroyOrganizationHbiHostsJob when in IoP mode' do
32
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
33
+
34
+ action = create_action(::Actions::Katello::Organization::Destroy)
35
+ action.stubs(:action_subject).with(@organization)
36
+ plan_action(action, @organization)
37
+
38
+ assert_action_planned_with(action, ForemanInventoryUpload::Async::DestroyOrganizationHbiHostsJob, @organization.id)
39
+ end
40
+
41
+ test 'does not plan DestroyOrganizationHbiHostsJob when not in IoP mode' do
42
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
43
+
44
+ action = create_action(::Actions::Katello::Organization::Destroy)
45
+ action.stubs(:action_subject).with(@organization)
46
+ plan_action(action, @organization)
47
+
48
+ refute_action_planned(action, ForemanInventoryUpload::Async::DestroyOrganizationHbiHostsJob)
49
+ end
50
+ end
@@ -1,8 +1,8 @@
1
1
  require 'test_plugin_helper'
2
2
 
3
3
  class URLRemediationsRetrieverTest < ActiveSupport::TestCase
4
- test 'Calls the given url' do
5
- retreiver = ForemanRhCloud::URLRemediationsRetriever.new(
4
+ test 'Calls the given url with GET when no hosts in URL' do
5
+ retriever = ForemanRhCloud::URLRemediationsRetriever.new(
6
6
  organization_id: FactoryBot.create(:organization).id,
7
7
  url: 'http://test.example.com',
8
8
  payload: 'TEST_PAYLOAD',
@@ -11,19 +11,96 @@ class URLRemediationsRetrieverTest < ActiveSupport::TestCase
11
11
  }
12
12
  )
13
13
 
14
- retreiver.stubs(:cert_auth_available?).returns(true)
14
+ retriever.stubs(:cert_auth_available?).returns(true)
15
15
 
16
16
  response = mock('response')
17
17
  response.stubs(:body).returns('TEST_RESPONSE')
18
- retreiver.expects(:execute_cloud_request).with do |params|
18
+ retriever.expects(:execute_cloud_request).with do |params|
19
19
  params[:method] == :get &&
20
20
  params[:url] == 'http://test.example.com' &&
21
21
  params[:headers][:custom1] == 'TEST_HEADER' &&
22
22
  params[:payload] == "\"TEST_PAYLOAD\""
23
23
  end.returns(response)
24
24
 
25
- actual = retreiver.create_playbook
25
+ actual = retriever.create_playbook
26
26
 
27
27
  assert_equal 'TEST_RESPONSE', actual
28
28
  end
29
+
30
+ test 'Uses POST with hosts in body when URL contains hosts query param' do
31
+ retriever = ForemanRhCloud::URLRemediationsRetriever.new(
32
+ organization_id: FactoryBot.create(:organization).id,
33
+ url: 'http://test.example.com/api/remediations/1234/playbook?hosts=uuid-1,uuid-2,uuid-3'
34
+ )
35
+
36
+ retriever.stubs(:cert_auth_available?).returns(true)
37
+
38
+ response = mock('response')
39
+ response.stubs(:body).returns('TEST_PLAYBOOK')
40
+ retriever.expects(:execute_cloud_request).with do |params|
41
+ params[:method] == :post &&
42
+ params[:url] == 'http://test.example.com/api/remediations/1234/playbook' &&
43
+ JSON.parse(params[:payload]) == ['uuid-1', 'uuid-2', 'uuid-3']
44
+ end.returns(response)
45
+
46
+ actual = retriever.create_playbook
47
+
48
+ assert_equal 'TEST_PLAYBOOK', actual
49
+ end
50
+
51
+ test 'Preserves other query params when extracting hosts' do
52
+ retriever = ForemanRhCloud::URLRemediationsRetriever.new(
53
+ organization_id: FactoryBot.create(:organization).id,
54
+ url: 'http://test.example.com/api/remediations/1234/playbook?hosts=uuid-1,uuid-2&localhost=false'
55
+ )
56
+
57
+ retriever.stubs(:cert_auth_available?).returns(true)
58
+
59
+ response = mock('response')
60
+ response.stubs(:body).returns('TEST_PLAYBOOK')
61
+ retriever.expects(:execute_cloud_request).with do |params|
62
+ params[:method] == :post &&
63
+ params[:url].include?('localhost=false') &&
64
+ !params[:url].include?('hosts=') &&
65
+ JSON.parse(params[:payload]) == ['uuid-1', 'uuid-2']
66
+ end.returns(response)
67
+
68
+ retriever.create_playbook
69
+ end
70
+
71
+ test 'Merges multiple hosts query params into a single array' do
72
+ retriever = ForemanRhCloud::URLRemediationsRetriever.new(
73
+ organization_id: FactoryBot.create(:organization).id,
74
+ url: 'http://test.example.com/api/remediations/1234/playbook?hosts=uuid-1&hosts=uuid-2'
75
+ )
76
+
77
+ retriever.stubs(:cert_auth_available?).returns(true)
78
+
79
+ response = mock('response')
80
+ response.stubs(:body).returns('TEST_PLAYBOOK')
81
+ retriever.expects(:execute_cloud_request).with do |params|
82
+ params[:method] == :post &&
83
+ params[:url] == 'http://test.example.com/api/remediations/1234/playbook' &&
84
+ JSON.parse(params[:payload]) == ['uuid-1', 'uuid-2']
85
+ end.returns(response)
86
+
87
+ retriever.create_playbook
88
+ end
89
+
90
+ test 'Falls back to GET when hosts query param is empty' do
91
+ retriever = ForemanRhCloud::URLRemediationsRetriever.new(
92
+ organization_id: FactoryBot.create(:organization).id,
93
+ url: 'http://test.example.com/api/remediations/1234/playbook?hosts='
94
+ )
95
+
96
+ retriever.stubs(:cert_auth_available?).returns(true)
97
+
98
+ response = mock('response')
99
+ response.stubs(:body).returns('TEST_PLAYBOOK')
100
+ retriever.expects(:execute_cloud_request).with do |params|
101
+ params[:method] == :get
102
+ end.returns(response)
103
+
104
+ retriever.create_playbook
105
+ end
29
106
  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: 14.1.0
4
+ version: 14.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Red Hat Cloud team
@@ -51,34 +51,6 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '4.18'
54
- - !ruby/object:Gem::Dependency
55
- name: rdoc
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0'
61
- type: :development
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0'
68
- - !ruby/object:Gem::Dependency
69
- name: theforeman-rubocop
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: 0.1.0
75
- type: :development
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: 0.1.0
82
54
  description: Foreman plugin that process & upload data to Red Hat Cloud
83
55
  email:
84
56
  - rlavi@redhat.com, sshtein@redhat.com
@@ -188,6 +160,7 @@ files:
188
160
  - lib/foreman_inventory_upload/async/async_helpers.rb
189
161
  - lib/foreman_inventory_upload/async/create_missing_insights_facets.rb
190
162
  - lib/foreman_inventory_upload/async/delayed_start.rb
163
+ - lib/foreman_inventory_upload/async/destroy_organization_hbi_hosts_job.rb
191
164
  - lib/foreman_inventory_upload/async/generate_all_reports_job.rb
192
165
  - lib/foreman_inventory_upload/async/generate_host_report.rb
193
166
  - lib/foreman_inventory_upload/async/host_inventory_report_job.rb
@@ -206,6 +179,7 @@ files:
206
179
  - lib/foreman_rh_cloud.rb
207
180
  - lib/foreman_rh_cloud/async/exponential_backoff.rb
208
181
  - lib/foreman_rh_cloud/engine.rb
182
+ - lib/foreman_rh_cloud/organization_destroy_extensions.rb
209
183
  - lib/foreman_rh_cloud/plugin.rb
210
184
  - lib/foreman_rh_cloud/version.rb
211
185
  - lib/insights_cloud.rb
@@ -260,6 +234,7 @@ files:
260
234
  - test/jobs/cloud_connector_announce_task_test.rb
261
235
  - test/jobs/connector_playbook_execution_reporter_task_test.rb
262
236
  - test/jobs/create_missing_insights_facets_test.rb
237
+ - test/jobs/destroy_organization_hbi_hosts_job_test.rb
263
238
  - test/jobs/exponential_backoff_test.rb
264
239
  - test/jobs/generate_host_report_test.rb
265
240
  - test/jobs/host_inventory_report_job_test.rb
@@ -286,6 +261,7 @@ files:
286
261
  - test/unit/lib/foreman_rh_cloud/registration_manager_extensions_test.rb
287
262
  - test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb
288
263
  - test/unit/metadata_generator_test.rb
264
+ - test/unit/organization_destroy_extensions_test.rb
289
265
  - test/unit/playbook_progress_generator_test.rb
290
266
  - test/unit/rh_cloud_host_test.rb
291
267
  - test/unit/rh_cloud_http_proxy_test.rb
@@ -692,6 +668,7 @@ test_files:
692
668
  - test/jobs/cloud_connector_announce_task_test.rb
693
669
  - test/jobs/connector_playbook_execution_reporter_task_test.rb
694
670
  - test/jobs/create_missing_insights_facets_test.rb
671
+ - test/jobs/destroy_organization_hbi_hosts_job_test.rb
695
672
  - test/jobs/exponential_backoff_test.rb
696
673
  - test/jobs/generate_host_report_test.rb
697
674
  - test/jobs/host_inventory_report_job_test.rb
@@ -718,6 +695,7 @@ test_files:
718
695
  - test/unit/lib/foreman_rh_cloud/registration_manager_extensions_test.rb
719
696
  - test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb
720
697
  - test/unit/metadata_generator_test.rb
698
+ - test/unit/organization_destroy_extensions_test.rb
721
699
  - test/unit/playbook_progress_generator_test.rb
722
700
  - test/unit/rh_cloud_host_test.rb
723
701
  - test/unit/rh_cloud_http_proxy_test.rb