foreman_rh_cloud 14.4.0 → 14.6.0

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/rh_cloud/cloud_request_controller.rb +1 -0
  3. data/app/controllers/api/v2/rh_cloud/inventory_controller.rb +11 -5
  4. data/app/controllers/foreman_inventory_upload/accounts_controller.rb +0 -1
  5. data/app/controllers/foreman_inventory_upload/uploads_controller.rb +0 -11
  6. data/app/controllers/foreman_inventory_upload/uploads_settings_controller.rb +0 -8
  7. data/app/services/foreman_rh_cloud/cert_auth.rb +2 -1
  8. data/app/services/foreman_rh_cloud/cloud_presence.rb +23 -0
  9. data/app/services/foreman_rh_cloud/cloud_request.rb +2 -1
  10. data/app/services/foreman_rh_cloud/cloud_request_forwarder.rb +9 -5
  11. data/config/routes.rb +1 -3
  12. data/lib/foreman_rh_cloud/engine.rb +1 -6
  13. data/lib/foreman_rh_cloud/plugin.rb +4 -2
  14. data/lib/foreman_rh_cloud/version.rb +1 -1
  15. data/lib/foreman_rh_cloud.rb +16 -0
  16. data/lib/insights_cloud/async/cloud_connector_announce_task.rb +76 -19
  17. data/package.json +1 -1
  18. data/test/controllers/inventory_upload/api/inventory_controller_test.rb +10 -8
  19. data/test/jobs/cloud_connector_announce_task_test.rb +116 -106
  20. data/test/unit/foreman_rh_cloud_iop_metadata_test.rb +27 -0
  21. data/test/unit/services/foreman_rh_cloud/cloud_request_forwarder_test.rb +28 -0
  22. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/integrations.test.js +4 -12
  23. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/components/__tests__/Toast.test.js +62 -61
  24. data/webpack/InsightsCloudSync/Components/InsightsTable/__tests__/InsightsTableActions.test.js +2 -2
  25. data/webpack/InsightsCloudSync/__tests__/InsightsCloudSyncActions.test.js +7 -5
  26. data/webpack/InsightsHostDetailsTab/__tests__/InsightsTabIntegration.test.js +69 -8
  27. data/webpack/InsightsVulnerabilityActionsBar/__tests__/InsightsVulnerabilityActionsBar.test.js +1 -5
  28. data/webpack/test_setup.js +1 -0
  29. metadata +2 -8
  30. data/app/services/foreman_rh_cloud/cloud_connector.rb +0 -74
  31. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext.js +0 -6
  32. data/webpack/__mocks__/foremanReact/common/I18n.js +0 -6
  33. data/webpack/__mocks__/foremanReact/common/helpers.js +0 -14
  34. data/webpack/__mocks__/foremanReact/constants.js +0 -24
  35. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +0 -24
  36. data/webpack/__mocks__/foremanReact/redux/API/index.js +0 -12
@@ -1,125 +1,135 @@
1
- require 'json'
2
1
  require 'test_plugin_helper'
3
2
  require 'foreman_tasks/test_helpers'
4
- require "#{ForemanTasks::Engine.root}/test/support/dummy_dynflow_action"
5
3
 
6
4
  class CloudConnectorAnnounceTaskTest < ActiveSupport::TestCase
7
5
  include Dynflow::Testing::Factories
8
6
 
9
7
  setup do
10
- RemoteExecutionFeature.register(
11
- :ansible_configure_cloud_connector,
12
- N_('Configure Cloud Connector on given hosts'),
13
- :description => N_('Configure Cloud Connector on given hosts'),
14
- :proxy_selector_override => ::RemoteExecutionProxySelector::INTERNAL_PROXY
15
- )
8
+ InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance
9
+ .stubs(:recent_cloud_remediation?).returns(false)
10
+ end
11
+
12
+ teardown do
13
+ ForemanRhCloud.unstub(:with_iop_smart_proxy?)
14
+ end
15
+
16
+ test 'announces to sources for all organizations when rhc_instance_id is set' do
17
+ Setting[:rhc_instance_id] = 'test-rhc-id'
18
+
19
+ InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance
20
+ .stubs(:cert_auth_available?).returns(true)
16
21
 
17
- @job_invocation = generate_job_invocation(:ansible_configure_cloud_connector)
22
+ ForemanRhCloud::CloudPresence.any_instance
23
+ .expects(:announce_to_sources)
24
+ .times(Organization.unscoped.count)
18
25
 
19
- # reset connector feature ID cache
20
- InsightsCloud::Async::CloudConnectorAnnounceTask.instance_variable_set(:@connector_feature_id, nil)
26
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
27
+ action = run_action(action)
28
+
29
+ status = action.output[:status].to_s
30
+ assert_match(/Registered:|Already registered:/, status)
31
+ refute_match(/Skipped/, status)
32
+ refute_match(/Failed/, status)
21
33
  end
22
34
 
23
- test 'It executes cloud presence announcer' do
24
- ForemanRhCloud::CloudPresence.any_instance.expects(:announce_to_sources).times(Organization.unscoped.count)
35
+ test 'skips when rhc_instance_id is not set' do
36
+ Setting[:rhc_instance_id] = nil
37
+
38
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
25
39
 
26
- action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask, @job_invocation)
27
- finalize_action(action)
40
+ assert_empty action.execution_plan.planned_run_steps
28
41
  end
29
42
 
30
- private
31
-
32
- def generate_job_invocation(feature_name)
33
- job_template = FactoryBot.build(
34
- :job_template,
35
- :template => 'BLEH'
36
- )
37
- feature = RemoteExecutionFeature.feature!(feature_name).id
38
-
39
- job_invocation = FactoryBot.create(
40
- :job_invocation,
41
- remote_execution_feature_id: feature,
42
- task_id: FactoryBot.create(:dynflow_task).id
43
- )
44
-
45
- job_template.template_inputs << playbook_url_input = FactoryBot.build(:template_input,
46
- :name => 'playbook_url',
47
- :input_type => 'user',
48
- :required => true)
49
- job_template.template_inputs << report_url_input = FactoryBot.build(:template_input,
50
- :name => 'report_url',
51
- :input_type => 'user',
52
- :required => true)
53
- job_template.template_inputs << correlation_id_input = FactoryBot.build(:template_input,
54
- :name => 'correlation_id',
55
- :input_type => 'user',
56
- :required => true)
57
- job_template.template_inputs << report_interval_input = FactoryBot.build(:template_input,
58
- :name => 'report_interval',
59
- :input_type => 'user',
60
- :required => true)
61
-
62
- template_invocation = FactoryBot.build(:template_invocation,
63
- :template => job_template,
64
- :job_invocation => job_invocation)
65
-
66
- template_invocation.input_values << FactoryBot.create(
67
- :template_invocation_input_value,
68
- :template_invocation => template_invocation,
69
- :template_input => playbook_url_input,
70
- :value => 'http://example.com/TEST_PLAYBOOK'
71
- )
72
- template_invocation.input_values << FactoryBot.create(
73
- :template_invocation_input_value,
74
- :template_invocation => template_invocation,
75
- :template_input => report_url_input,
76
- :value => 'http://example.com/TEST_REPORT'
77
- )
78
- template_invocation.input_values << FactoryBot.create(
79
- :template_invocation_input_value,
80
- :template_invocation => template_invocation,
81
- :template_input => correlation_id_input,
82
- :value => 'TEST_CORRELATION'
83
- )
84
- template_invocation.input_values << FactoryBot.create(
85
- :template_invocation_input_value,
86
- :template_invocation => template_invocation,
87
- :template_input => report_interval_input,
88
- :value => '1'
89
- )
90
-
91
- @host1 = FactoryBot.create(:host, :with_insights_hits, name: 'host1')
92
- @host1.insights.uuid = 'TEST_UUID1'
93
- @host1.insights.save!
94
- @host2 = FactoryBot.create(:host, :with_insights_hits, name: 'host2')
95
- @host2.insights.uuid = 'TEST_UUID2'
96
- @host2.insights.save!
97
-
98
- targeting = FactoryBot.create(:targeting, hosts: [@host1, @host2])
99
- job_invocation.targeting = targeting
100
- job_invocation.save!
101
-
102
- job_invocation.template_invocations << FactoryBot.create(
103
- :template_invocation,
104
- run_host_job_task: FactoryBot.create(:dynflow_task),
105
- host_id: @host1.id
106
- )
107
- job_invocation.template_invocations << FactoryBot.create(
108
- :template_invocation,
109
- run_host_job_task: FactoryBot.create(:dynflow_task),
110
- host_id: @host2.id
111
- )
112
-
113
- fake_output = (1..5).map do |i|
114
- { 'timestamp' => (Time.now - (5 - i)).to_f, 'output' => "#{i}\n" }
115
- end
116
- Support::DummyDynflowAction.any_instance.stubs(:live_output).returns(fake_output)
117
- Support::DummyDynflowAction.any_instance.stubs(:exit_status).returns(0)
43
+ test 'skips when rhc_instance_id is empty string' do
44
+ Setting[:rhc_instance_id] = ''
45
+
46
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
47
+
48
+ assert_empty action.execution_plan.planned_run_steps
49
+ end
50
+
51
+ test 'skips when in IoP mode' do
52
+ Setting[:rhc_instance_id] = 'test-rhc-id'
53
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
54
+
55
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
56
+
57
+ assert_empty action.execution_plan.planned_run_steps
58
+ end
59
+
60
+ test 'skips organizations without a manifest' do
61
+ Setting[:rhc_instance_id] = 'test-rhc-id'
62
+
63
+ InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance
64
+ .stubs(:cert_auth_available?).returns(false)
65
+
66
+ ForemanRhCloud::CloudPresence.any_instance
67
+ .expects(:announce_to_sources)
68
+ .never
118
69
 
119
- job_invocation
70
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
71
+ action = run_action(action)
72
+
73
+ status = action.output[:status].to_s
74
+ assert_match(/Skipped \(no manifest\):/, status)
75
+ refute_match(/Registered/, status)
76
+ refute_match(/Failed/, status)
77
+ end
78
+
79
+ test 'still runs when allow_auto_inventory_upload is disabled' do
80
+ Setting[:rhc_instance_id] = 'test-rhc-id'
81
+ Setting[:allow_auto_inventory_upload] = false
82
+
83
+ InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance
84
+ .stubs(:cert_auth_available?).returns(true)
85
+
86
+ ForemanRhCloud::CloudPresence.any_instance
87
+ .expects(:announce_to_sources)
88
+ .times(Organization.unscoped.count)
89
+
90
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
91
+ action = run_action(action)
92
+
93
+ assert_match(/Registered:|Already registered:/, action.output[:status].to_s)
94
+ end
95
+
96
+ test 'skips orgs with recent cloud remediation jobs' do
97
+ Setting[:rhc_instance_id] = 'test-rhc-id'
98
+
99
+ InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance
100
+ .stubs(:cert_auth_available?).returns(true)
101
+ InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance
102
+ .stubs(:recent_cloud_remediation?).returns(true)
103
+
104
+ ForemanRhCloud::CloudPresence.any_instance
105
+ .expects(:announce_to_sources)
106
+ .never
107
+
108
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
109
+ action = run_action(action)
110
+
111
+ status = action.output[:status].to_s
112
+ assert_match(/Already registered \(recent cloud remediation\):/, status)
113
+ refute_match(/\bRegistered:/, status)
120
114
  end
121
115
 
122
- def read_jsonl(jsonl)
123
- jsonl.lines.map { |l| JSON.parse(l) }
116
+ test 'continues processing other orgs when one fails and task errors' do
117
+ Setting[:rhc_instance_id] = 'test-rhc-id'
118
+
119
+ InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance
120
+ .stubs(:cert_auth_available?).returns(true)
121
+
122
+ call_count = 0
123
+ ForemanRhCloud::CloudPresence.any_instance.stubs(:announce_to_sources).with do
124
+ call_count += 1
125
+ raise(StandardError.new('API error')) if call_count == 1
126
+ true
127
+ end
128
+
129
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
130
+
131
+ error = assert_raises(StandardError) { run_action(action) }
132
+ assert_match(/Sources announcement failed for:/, error.message)
133
+ assert_equal Organization.unscoped.count, call_count
124
134
  end
125
135
  end
@@ -101,6 +101,22 @@ class ForemanRhCloudIopMetadataTest < ActiveSupport::TestCase
101
101
  assert_equal 'https://iop.example.com', ForemanRhCloud.cert_base_url
102
102
  end
103
103
 
104
+ test 'cloud_cert_base_url returns IoP URL when IoP exists and force_cla_connection is off' do
105
+ create_iop_proxy
106
+ Setting[:force_cla_connection] = false
107
+
108
+ assert_equal 'https://iop.example.com', ForemanRhCloud.cloud_cert_base_url
109
+ end
110
+
111
+ test 'cloud_cert_base_url returns cloud URL when IoP exists and force_cla_connection is on' do
112
+ create_iop_proxy
113
+ Setting[:force_cla_connection] = true
114
+
115
+ assert_equal 'https://cert.cloud.redhat.com', ForemanRhCloud.cloud_cert_base_url
116
+ ensure
117
+ Setting[:force_cla_connection] = false
118
+ end
119
+
104
120
  test 'legacy_insights_url returns IoP URL when IoP smart proxy exists' do
105
121
  create_iop_proxy
106
122
 
@@ -153,6 +169,17 @@ class ForemanRhCloudIopMetadataTest < ActiveSupport::TestCase
153
169
  ENV.delete('SATELLITE_CERT_RH_CLOUD_URL')
154
170
  end
155
171
 
172
+ test 'cloud_cert_base_url uses ENV var when force_cla_connection is on' do
173
+ ENV['SATELLITE_CERT_RH_CLOUD_URL'] = 'https://env-cert.cloud.test'
174
+ Setting[:force_cla_connection] = true
175
+ create_iop_proxy
176
+
177
+ assert_equal 'https://env-cert.cloud.test', ForemanRhCloud.cloud_cert_base_url
178
+ ensure
179
+ ENV.delete('SATELLITE_CERT_RH_CLOUD_URL')
180
+ Setting[:force_cla_connection] = false
181
+ end
182
+
156
183
  test 'legacy_insights_url prefers IoP URL over ENV var' do
157
184
  ENV['SATELLITE_LEGACY_INSIGHTS_URL'] = 'https://env-legacy.insights.test'
158
185
 
@@ -10,7 +10,9 @@ class CloudRequestForwarderTest < ActiveSupport::TestCase
10
10
 
11
11
  ForemanRhCloud.stubs(:base_url).returns('https://cloud.example.com')
12
12
  ForemanRhCloud.stubs(:cert_base_url).returns('https://cert.cloud.example.com')
13
+ ForemanRhCloud.stubs(:cloud_cert_base_url).returns('https://cert.cloud.example.com')
13
14
  ForemanRhCloud.stubs(:legacy_insights_url).returns('https://cert-api.access.example.com')
15
+ ForemanRhCloud.stubs(:transformed_cloud_http_proxy_string).returns(nil)
14
16
 
15
17
  UpstreamOnlySettingsTestHelper.set_if_available('allow_multiple_content_views')
16
18
  env = FactoryBot.create(:katello_k_t_environment)
@@ -34,6 +36,7 @@ class CloudRequestForwarderTest < ActiveSupport::TestCase
34
36
 
35
37
  test 'should prepare correct cloud url' do
36
38
  paths = {
39
+ "/api/lightspeed/v1/query" => "https://cert.cloud.example.com/api/lightspeed/v1/query",
37
40
  "/redhat_access/r/insights/platform/module-update-router/v1/channel?module=insights-core" => "https://cert.cloud.example.com/api/module-update-router/v1/channel?module=insights-core",
38
41
  "/redhat_access/r/insights/v1/static/release/insights-core.egg" => "https://cert-api.access.example.com/r/insights/v1/static/release/insights-core.egg",
39
42
  "/redhat_access/r/insights/v1/static/uploader.v2.json" => "https://cert-api.access.example.com/r/insights/v1/static/uploader.v2.json",
@@ -50,6 +53,31 @@ class CloudRequestForwarderTest < ActiveSupport::TestCase
50
53
  end
51
54
  end
52
55
 
56
+ test 'does not force CLA to cloud when IoP is enabled and setting is off' do
57
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
58
+ ForemanRhCloud.stubs(:cert_base_url).returns('https://iop.example.com')
59
+ ForemanRhCloud.stubs(:cloud_cert_base_url).returns('https://iop.example.com')
60
+ Setting.stubs(:[]).with(:force_cla_connection).returns(false)
61
+
62
+ actual = @forwarder.path_params('/api/lightspeed/v1/query')
63
+
64
+ assert_equal 'https://iop.example.com/api/lightspeed/v1/query', actual[:url]
65
+ refute actual[:force_cloud]
66
+ end
67
+
68
+ test 'forwards CLA to cloud when IoP is enabled and force_cla_connection is on' do
69
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
70
+ ForemanRhCloud.stubs(:cert_base_url).returns('https://iop.example.com')
71
+ ForemanRhCloud.stubs(:cloud_cert_base_url).returns('https://cert.cloud.example.com')
72
+ ForemanRhCloud.stubs(:transformed_cloud_http_proxy_string).returns(nil)
73
+ Setting.stubs(:[]).with(:force_cla_connection).returns(true)
74
+
75
+ actual = @forwarder.path_params('/api/lightspeed/v1/query')
76
+
77
+ assert_equal 'https://cert.cloud.example.com/api/lightspeed/v1/query', actual[:url]
78
+ assert actual[:force_cloud]
79
+ end
80
+
53
81
  test 'should forward payload from request parameters' do
54
82
  params = { 'pumpkin' => 'pie' }
55
83
  req = ActionDispatch::Request.new(
@@ -3,24 +3,16 @@ import { render, screen, fireEvent } from '@testing-library/react';
3
3
  import { Provider } from 'react-redux';
4
4
  import configureMockStore from 'redux-mock-store';
5
5
  import thunk from 'redux-thunk';
6
- import * as API from 'foremanReact/redux/API';
7
6
  import ConnectedSyncButton from '../index';
8
- import { successResponse } from './SyncButtonFixtures';
9
- import { INVENTORY_SYNC } from '../SyncButtonConstants';
10
-
11
- jest.spyOn(API, 'post');
12
7
 
13
8
  const mockStore = configureMockStore([thunk]);
14
9
 
10
+ afterEach(() => {
11
+ jest.clearAllMocks();
12
+ });
13
+
15
14
  describe('SyncButton integration test', () => {
16
15
  it('dispatches sync action when button is clicked', () => {
17
- API.post.mockImplementation(({ handleSuccess, key, ...action }) => {
18
- if (key === INVENTORY_SYNC && handleSuccess) {
19
- handleSuccess(successResponse);
20
- }
21
- return { type: 'API_POST', ...action };
22
- });
23
-
24
16
  const store = mockStore({
25
17
  API: {},
26
18
  });
@@ -1,82 +1,83 @@
1
1
  import React from 'react';
2
- import { shallow } from '@theforeman/test';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ import '@testing-library/jest-dom';
3
5
  import Toast from '../Toast';
4
6
 
7
+ jest.mock('foremanReact/common/I18n', () => ({
8
+ translate: jest.fn(str => str),
9
+ }));
10
+
11
+ const renderToast = (props = {}) =>
12
+ render(
13
+ <MemoryRouter>
14
+ <Toast syncHosts={5} disconnectHosts={3} {...props} />
15
+ </MemoryRouter>
16
+ );
17
+
18
+ const omittedText =
19
+ 'Excluded from upload to console.redhat.com Inventory service because host_registration_insights_inventory parameter value is false:';
20
+
5
21
  describe('Toast', () => {
6
22
  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);
23
+ renderToast({ userOmittedHosts: 2 });
13
24
 
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');
25
+ expect(screen.getByRole('link', { name: '10' })).toHaveAttribute(
26
+ 'href',
27
+ '/new/hosts?search=set%3F+subscription_uuid&page=1'
28
+ );
29
+ expect(screen.getByRole('link', { name: '5' })).toHaveAttribute(
30
+ 'href',
31
+ '/new/hosts?search=insights_inventory_sync_status+%3D+sync&page=1'
32
+ );
33
+ expect(screen.getByRole('link', { name: '3' })).toHaveAttribute(
34
+ 'href',
35
+ '/new/hosts?search=insights_inventory_sync_status+%3D+disconnect&page=1'
36
+ );
37
+ expect(screen.getByRole('link', { name: '2' })).toHaveAttribute(
38
+ 'href',
39
+ '/new/hosts?search=insights_inventory_sync_status+%3D+user_omitted&page=1'
40
+ );
41
+ expect(screen.getByText(omittedText, { exact: false })).toBeInTheDocument();
33
42
  });
34
43
 
35
44
  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);
45
+ renderToast({ userOmittedHosts: 0 });
43
46
 
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
- );
47
+ expect(screen.getByRole('link', { name: '8' })).toBeInTheDocument();
48
+ expect(screen.getByRole('link', { name: '5' })).toBeInTheDocument();
49
+ expect(screen.getByRole('link', { name: '3' })).toBeInTheDocument();
50
+ expect(screen.queryByRole('link', { name: '2' })).not.toBeInTheDocument();
51
+ expect(
52
+ screen.queryByText(omittedText, { exact: false })
53
+ ).not.toBeInTheDocument();
48
54
  });
49
55
 
50
56
  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);
57
+ renderToast();
56
58
 
57
- // Verify the count values
59
+ expect(screen.getByRole('link', { name: '8' })).toBeInTheDocument();
60
+ expect(screen.getByRole('link', { name: '5' })).toBeInTheDocument();
61
+ expect(screen.getByRole('link', { name: '3' })).toBeInTheDocument();
58
62
  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');
63
+ screen.queryByText(omittedText, { exact: false })
64
+ ).not.toBeInTheDocument();
70
65
  });
71
66
 
72
67
  it('renders correct status links for each category', () => {
73
- const wrapper = shallow(
74
- <Toast syncHosts={5} disconnectHosts={3} userOmittedHosts={2} />
75
- );
68
+ renderToast({ userOmittedHosts: 2 });
76
69
 
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');
70
+ expect(screen.getByRole('link', { name: '5' })).toHaveAttribute(
71
+ 'href',
72
+ expect.stringContaining('sync')
73
+ );
74
+ expect(screen.getByRole('link', { name: '3' })).toHaveAttribute(
75
+ 'href',
76
+ expect.stringContaining('disconnect')
77
+ );
78
+ expect(screen.getByRole('link', { name: '2' })).toHaveAttribute(
79
+ 'href',
80
+ expect.stringContaining('user_omitted')
81
+ );
81
82
  });
82
83
  });
@@ -117,10 +117,10 @@ describe('InsightsTable actions', () => {
117
117
  expect(push).toHaveBeenCalled();
118
118
 
119
119
  const apiAction = dispatch.mock.calls.find(
120
- call => call[0] && call[0].key === INSIGHTS_HITS_API_KEY
120
+ call => call[0] && call[0].payload?.key === INSIGHTS_HITS_API_KEY
121
121
  );
122
122
  expect(apiAction).toBeTruthy();
123
- const getArg = apiAction[0];
123
+ const getArg = apiAction[0].payload;
124
124
  expect(getArg.url).toBe(INSIGHTS_HITS_PATH);
125
125
  expect(getArg.params.page).toBe(2);
126
126
  expect(getArg.params.per_page).toBe(7);
@@ -1,3 +1,4 @@
1
+ import { API_OPERATIONS } from 'foremanReact/redux/API';
1
2
  import { syncInsights } from '../InsightsCloudSyncActions';
2
3
  import { INSIGHTS_CLOUD_SYNC } from '../InsightsCloudSyncConstants';
3
4
 
@@ -17,17 +18,18 @@ describe('InsightsCloudSync actions', () => {
17
18
 
18
19
  expect(dispatch).toHaveBeenCalledTimes(1);
19
20
  const dispatched = dispatch.mock.calls[0][0];
20
- expect(dispatched.key).toBe(INSIGHTS_CLOUD_SYNC);
21
- expect(dispatched.url).toBe('/insights_cloud/tasks');
22
- expect(typeof dispatched.handleSuccess).toBe('function');
23
- expect(typeof dispatched.errorToast).toBe('function');
21
+ expect(dispatched.type).toBe(API_OPERATIONS.POST);
22
+ expect(dispatched.payload.key).toBe(INSIGHTS_CLOUD_SYNC);
23
+ expect(dispatched.payload.url).toBe('/insights_cloud/tasks');
24
+ expect(typeof dispatched.payload.handleSuccess).toBe('function');
25
+ expect(typeof dispatched.payload.errorToast).toBe('function');
24
26
  });
25
27
 
26
28
  it('errorToast returns failure message with error details', () => {
27
29
  syncInsights(jest.fn(), '')(dispatch);
28
30
  const dispatched = dispatch.mock.calls[0][0];
29
31
 
30
- const result = dispatched.errorToast('some error');
32
+ const result = dispatched.payload.errorToast('some error');
31
33
  expect(result).toContain('some error');
32
34
  });
33
35
  });
@@ -1,17 +1,78 @@
1
1
  import React from 'react';
2
- import { IntegrationTestHelper } from '@theforeman/test';
2
+ import { screen, waitFor } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { Provider } from 'react-redux';
5
+ import { applyMiddleware, combineReducers, createStore } from 'redux';
6
+ import thunk from 'redux-thunk';
7
+ import { rtlHelpers } from 'foremanReact/common/rtlTestHelpers';
8
+ import { API } from 'foremanReact/redux/API';
3
9
 
4
10
  import InsightsTab from '../index';
5
11
  import reducers from '../../ForemanRhCloudReducers';
6
- import { hostID } from './InsightsTab.fixtures';
12
+ import { INSIGHTS_HITS_SUCCESS } from '../InsightsTabConstants';
13
+ import { hostID, hits } from './InsightsTab.fixtures';
7
14
 
8
- describe('InsightsTab integration test', () => {
9
- it('should flow', async () => {
10
- const integrationTestHelper = new IntegrationTestHelper(reducers);
11
- const component = integrationTestHelper.mount(
15
+ const { renderWithI18n } = rtlHelpers;
16
+
17
+ jest.mock('foremanReact/redux/API');
18
+
19
+ const recommendationTitle =
20
+ 'New Ansible Engine packages are inaccessible when dedicated Ansible repo is not enabled';
21
+
22
+ const createTabStore = () =>
23
+ createStore(combineReducers({ ...reducers }), applyMiddleware(thunk));
24
+
25
+ const renderConnectedTab = (store = createTabStore()) =>
26
+ renderWithI18n(
27
+ <Provider store={store}>
12
28
  <InsightsTab hostID={hostID} />
29
+ </Provider>
30
+ );
31
+
32
+ describe('InsightsTab integration test', () => {
33
+ beforeEach(() => {
34
+ jest.clearAllMocks();
35
+ });
36
+
37
+ it('requests hits for the host and shows empty state when there are none', async () => {
38
+ let resolveHits;
39
+ API.get.mockReturnValue(
40
+ new Promise(resolve => {
41
+ resolveHits = resolve;
42
+ })
13
43
  );
14
- component.update();
15
- /** Create a Flow test */
44
+
45
+ const store = createTabStore();
46
+ store.dispatch({
47
+ type: INSIGHTS_HITS_SUCCESS,
48
+ payload: { hits },
49
+ });
50
+
51
+ renderConnectedTab(store);
52
+
53
+ expect(await screen.findByText(recommendationTitle)).toBeInTheDocument();
54
+
55
+ await waitFor(() => {
56
+ expect(API.get).toHaveBeenCalledWith(`/insights_cloud/hits/${hostID}`);
57
+ });
58
+
59
+ resolveHits({ data: { hits: [] } });
60
+
61
+ expect(
62
+ await screen.findByRole('heading', {
63
+ name: 'No recommendations were found for this host!',
64
+ })
65
+ ).toBeInTheDocument();
66
+ expect(screen.queryByText(recommendationTitle)).not.toBeInTheDocument();
67
+ });
68
+
69
+ it('shows recommendation titles from the hits API response', async () => {
70
+ API.get.mockResolvedValue({ data: { hits } });
71
+
72
+ renderConnectedTab();
73
+
74
+ expect(await screen.findByText(recommendationTitle)).toBeInTheDocument();
75
+
76
+ expect(API.get).toHaveBeenCalledWith(`/insights_cloud/hits/${hostID}`);
16
77
  });
17
78
  });
@@ -1,13 +1,11 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
- import * as ForemanContext from 'foremanReact/Root/Context/ForemanContext';
5
4
  import { ForemanHostsIndexActionsBarContext } from 'foremanReact/components/HostsIndex';
6
5
  import * as ConfigHooks from '../../common/Hooks/ConfigHooks';
7
6
  import InsightsVulnerabilityActionsBar from '../index';
8
7
 
9
8
  jest.mock('../../common/Hooks/ConfigHooks');
10
- jest.mock('foremanReact/Root/Context/ForemanContext');
11
9
  jest.mock('../InsightsVulnerabilityActionsBarActions');
12
10
 
13
11
  const mockContextValue = {
@@ -16,11 +14,9 @@ const mockContextValue = {
16
14
  setMenuOpen: jest.fn(),
17
15
  };
18
16
 
19
- const renderComponent = (contextOverrides = {}, orgId = 1) => {
17
+ const renderComponent = (contextOverrides = {}) => {
20
18
  const contextValue = { ...mockContextValue, ...contextOverrides };
21
19
 
22
- ForemanContext.useForemanOrganization.mockReturnValue({ id: orgId });
23
-
24
20
  return render(
25
21
  <ForemanHostsIndexActionsBarContext.Provider value={contextValue}>
26
22
  <InsightsVulnerabilityActionsBar />