foreman_rh_cloud 12.2.16 → 12.2.18

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: 49dae3d74466145bf0eb7704cdc6c8b47e7633747f02e377561bed5d462bffa5
4
- data.tar.gz: 35a46941cf4ab3f58820b7df16131ae52d0f7ab6a5100ecc98b08a736dc75c6c
3
+ metadata.gz: 79c93d459f33a153d6d5ef0477cddc3b0f4b992716bb1a9b9352d0d8274b9185
4
+ data.tar.gz: 0f7d20917c23789936882787bcb492353a56ac4061b27fcabb138a93dc9c9909
5
5
  SHA512:
6
- metadata.gz: 40989fd8eeb177bf99fba7014aa3add78d5789e94419cf9be07e4ba6a1e0acbf90b0d1cb4f0dd3a5d68eb168a0556519b12bac8f9254ada0f26a0a0789e992d0
7
- data.tar.gz: c156603a5ebef7081bd6d96f37b426b377ca39637a4b08aaa932f7008514591fb5c63aa3b5140043c8cf596606d9eb0d23057dcdbe0a62612d60752c98e911da
6
+ metadata.gz: 61917464374fddd39409ba0e4dd5c8f2b40a8eb18091a0e4af381b78930a460186c334f5e7b93a5b052b6dff98101551da59390f82ee94880601abe08bf645dc
7
+ data.tar.gz: 49af34b52d989a0558c538f5da8e027d26953261d69cbea8ed5535c11ae407ab50b0b7c2dcf78302b7281cfff2a6f1c74dc93cf0f8cb942a15d141263000a93d
@@ -21,7 +21,8 @@ module RhCloudHost
21
21
  scoped_search :relation => :inventory_sync_status_object, :on => :status, :rename => :insights_inventory_sync_status,
22
22
  :complete_value => { :disconnect => ::InventorySync::InventoryStatus::DISCONNECT,
23
23
  :sync => ::InventorySync::InventoryStatus::SYNC }
24
- scoped_search :relation => :insights, :on => :uuid, :only_explicit => true, :rename => :insights_uuid
24
+ scoped_search :on => :id, :rename => :insights_uuid, :only_explicit => true,
25
+ :ext_method => :search_by_insights_uuid, :complete_value => false
25
26
 
26
27
  def insights_facet
27
28
  insights
@@ -41,4 +42,33 @@ module RhCloudHost
41
42
  insights_facet.update!(uuid: subscription_facet.uuid)
42
43
  end
43
44
  end
45
+
46
+ module ClassMethods
47
+ def search_by_insights_uuid(_key, operator, value)
48
+ # Determine which facet table to search based on IoP mode
49
+ facet_table = ForemanRhCloud.with_iop_smart_proxy? ? Katello::Host::SubscriptionFacet.table_name : InsightsFacet.table_name
50
+
51
+ # Build SQL condition
52
+ if ['IN', 'NOT IN'].include?(operator)
53
+ # For IN/NOT IN, value may be an array or comma-separated string
54
+ # Convert to array and build placeholders for each value
55
+ values = value.is_a?(Array) ? value : value.to_s.split(',').map(&:strip)
56
+ placeholders = (['?'] * values.size).join(',')
57
+ condition = sanitize_sql_for_conditions(
58
+ ["#{facet_table}.uuid #{operator} (#{placeholders})", *values]
59
+ )
60
+ else
61
+ # For other operators (=, !=, LIKE, etc.), use value_to_sql for proper SQL formatting
62
+ condition = sanitize_sql_for_conditions(
63
+ ["#{facet_table}.uuid #{operator} ?", value_to_sql(operator, value)]
64
+ )
65
+ end
66
+
67
+ # Return search parameters with LEFT JOIN to include hosts without facets
68
+ {
69
+ joins: "LEFT JOIN #{facet_table} ON #{facet_table}.host_id = #{Host::Managed.table_name}.id",
70
+ conditions: condition,
71
+ }
72
+ end
73
+ end
44
74
  end
@@ -7,9 +7,15 @@ module ForemanInventoryUpload
7
7
 
8
8
  def run
9
9
  organization = ::Organization.find(input[:organization_id])
10
- hosts_without_facets = ::ForemanInventoryUpload::Generators::Queries.for_org(organization, hosts_query: 'null? insights_uuid')
10
+ # Find hosts with subscription facets but without insights facets
11
+ # Note: We can't use scoped_search 'null? insights_uuid' because the null? operator
12
+ # doesn't work with ext_methods - it would check hosts.id IS NULL instead of the facet
13
+ hosts_without_facets = ::ForemanInventoryUpload::Generators::Queries.for_org(organization, use_batches: false)
14
+ .left_outer_joins(:insights)
15
+ .where(insights_facets: { id: nil })
16
+
11
17
  facet_count = 0
12
- hosts_without_facets.each do |batch|
18
+ hosts_without_facets.in_batches(of: ForemanInventoryUpload.slice_size) do |batch|
13
19
  facets = batch.pluck(:id, 'katello_subscription_facets.uuid').map do |host_id, uuid|
14
20
  {
15
21
  host_id: host_id,
@@ -95,8 +95,8 @@ module ForemanInventoryUpload
95
95
  begin
96
96
  upload_file(cer_path)
97
97
  progress_output.write_line("Upload completed successfully")
98
- move_to_done_folder
99
- progress_output.write_line("Uploaded file moved to done/ folder")
98
+ done_path = move_to_done_folder
99
+ progress_output.write_line("Uploaded report moved to #{done_path}")
100
100
  progress_output.status = "pid #{Process.pid} exit 0"
101
101
  rescue StandardError => e
102
102
  progress_output.write_line("Upload failed: #{e.message}")
@@ -139,6 +139,7 @@ module ForemanInventoryUpload
139
139
  done_file = ForemanInventoryUpload.done_file_path(File.basename(filename))
140
140
  FileUtils.mv(filename, done_file)
141
141
  logger.debug("Moved #{filename} to #{done_file}")
142
+ done_file
142
143
  end
143
144
 
144
145
  def certificate
@@ -32,7 +32,18 @@ module ForemanInventoryUpload
32
32
 
33
33
  def self.report_file_paths(organization_id)
34
34
  filename = facts_archive_name(organization_id)
35
- Dir[ForemanInventoryUpload.uploads_file_path(filename), ForemanInventoryUpload.done_file_path(filename)]
35
+ # Report files start in generated
36
+ # They are then MOVED (not copied) to uploads, then done.
37
+ # When they are moved to the new folder, they overwrite any file with the same name.
38
+ # If it's a generate-only, it will be in generated
39
+ # Failed or incomplete uploads will be in uploads
40
+ # Completed uploads will be in done
41
+ # The ordering here ensures we get the correct file path every time.
42
+ Dir[
43
+ ForemanInventoryUpload.generated_reports_file_path(filename),
44
+ ForemanInventoryUpload.uploads_file_path(filename),
45
+ ForemanInventoryUpload.done_file_path(filename)
46
+ ]
36
47
  end
37
48
 
38
49
  def self.generated_reports_folder
@@ -44,6 +55,10 @@ module ForemanInventoryUpload
44
55
  )
45
56
  end
46
57
 
58
+ def self.generated_reports_file_path(filename)
59
+ File.join(ForemanInventoryUpload.generated_reports_folder, filename)
60
+ end
61
+
47
62
  def self.outputs_folder
48
63
  @outputs_folder ||= ensure_folder(File.join(ForemanInventoryUpload.base_folder, 'outputs/'))
49
64
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanRhCloud
2
- VERSION = '12.2.16'.freeze
2
+ VERSION = '12.2.18'.freeze
3
3
  end
@@ -46,6 +46,10 @@ namespace :rh_cloud_inventory do
46
46
  filter,
47
47
  false # don't upload; the user ran report:generate and not report:generate_upload
48
48
  )
49
+ unless Setting[:subscription_connection_enabled]
50
+ report_paths = ForemanInventoryUpload.report_file_paths(organization_id)
51
+ puts "Report saved to #{report_paths.first}" if report_paths.any?
52
+ end
49
53
  end
50
54
  puts "Check the Uploading tab for report uploading status." if Setting[:subscription_connection_enabled]
51
55
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_rh_cloud",
3
- "version": "12.2.16",
3
+ "version": "12.2.18",
4
4
  "description": "Inventory Upload =============",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -5,6 +5,49 @@ class AccountsControllerTest < ActionController::TestCase
5
5
 
6
6
  include FolderIsolation
7
7
 
8
+ test 'report_file_paths finds report in generated_reports folder' do
9
+ test_org = FactoryBot.create(:organization)
10
+ filename = ForemanInventoryUpload.facts_archive_name(test_org.id)
11
+
12
+ generated_path = ForemanInventoryUpload.generated_reports_file_path(filename)
13
+ FileUtils.mkdir_p(File.dirname(generated_path))
14
+ FileUtils.touch(generated_path)
15
+
16
+ paths = ForemanInventoryUpload.report_file_paths(test_org.id)
17
+ assert_includes paths, generated_path
18
+ end
19
+
20
+ test 'report_file_paths finds report in uploads folder' do
21
+ test_org = FactoryBot.create(:organization)
22
+ filename = ForemanInventoryUpload.facts_archive_name(test_org.id)
23
+
24
+ uploads_path = ForemanInventoryUpload.uploads_file_path(filename)
25
+ FileUtils.mkdir_p(File.dirname(uploads_path))
26
+ FileUtils.touch(uploads_path)
27
+
28
+ paths = ForemanInventoryUpload.report_file_paths(test_org.id)
29
+ assert_includes paths, uploads_path
30
+ end
31
+
32
+ test 'report_file_paths finds report in done folder' do
33
+ test_org = FactoryBot.create(:organization)
34
+ filename = ForemanInventoryUpload.facts_archive_name(test_org.id)
35
+
36
+ done_path = ForemanInventoryUpload.done_file_path(filename)
37
+ FileUtils.mkdir_p(File.dirname(done_path))
38
+ FileUtils.touch(done_path)
39
+
40
+ paths = ForemanInventoryUpload.report_file_paths(test_org.id)
41
+ assert_includes paths, done_path
42
+ end
43
+
44
+ test 'report_file_paths returns empty when no report exists' do
45
+ test_org = FactoryBot.create(:organization)
46
+
47
+ paths = ForemanInventoryUpload.report_file_paths(test_org.id)
48
+ assert_empty paths
49
+ end
50
+
8
51
  test 'Returns statuses for each process type' do
9
52
  test_org = FactoryBot.create(:organization)
10
53
 
@@ -31,6 +31,7 @@ module FolderIsolation
31
31
  ForemanInventoryUpload.stubs(:base_folder).returns(@tmpdir)
32
32
  ForemanInventoryUpload.instance_variable_set(:@outputs_folder, nil)
33
33
  ForemanInventoryUpload.instance_variable_set(:@uploads_folders, nil)
34
+ ForemanInventoryUpload.instance_variable_set(:@generated_reports_folder, nil)
34
35
  end
35
36
 
36
37
  teardown do
@@ -188,4 +188,158 @@ class RhCloudHostTest < ActiveSupport::TestCase
188
188
  assert_equal local_uuid, @host.insights_uuid
189
189
  end
190
190
  end
191
+
192
+ context 'scoped search on insights_uuid' do
193
+ setup do
194
+ @org = FactoryBot.create(:organization)
195
+ end
196
+
197
+ teardown do
198
+ ForemanRhCloud.unstub(:with_iop_smart_proxy?)
199
+ end
200
+
201
+ test 'searches insights_facet.uuid in non-IoP mode with = operator' do
202
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
203
+ host1 = FactoryBot.create(:host, :managed, organization: @org)
204
+ host1.insights = FactoryBot.create(:insights_facet, host_id: host1.id, uuid: 'insights-uuid-123')
205
+ host2 = FactoryBot.create(:host, :managed, organization: @org)
206
+ host2.insights = FactoryBot.create(:insights_facet, host_id: host2.id, uuid: 'insights-uuid-456')
207
+
208
+ results = Host::Managed.search_for('insights_uuid = insights-uuid-123')
209
+
210
+ assert_includes results, host1
211
+ assert_not_includes results, host2
212
+ end
213
+
214
+ test 'searches subscription_facet.uuid in IoP mode with = operator' do
215
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
216
+ host1 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
217
+ host2 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
218
+
219
+ # Even if insights_facet has different UUID, should use subscription_facet UUID
220
+ host1.insights = FactoryBot.create(:insights_facet, host_id: host1.id, uuid: 'stale-123')
221
+
222
+ results = Host::Managed.search_for("insights_uuid = #{host1.subscription_facet.uuid}")
223
+
224
+ assert_includes results, host1
225
+ assert_not_includes results, host2
226
+ end
227
+
228
+ test 'searches with ^ operator (IN) in non-IoP mode' do
229
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
230
+ host1 = FactoryBot.create(:host, :managed, organization: @org)
231
+ host1.insights = FactoryBot.create(:insights_facet, host_id: host1.id, uuid: 'uuid-1')
232
+ host2 = FactoryBot.create(:host, :managed, organization: @org)
233
+ host2.insights = FactoryBot.create(:insights_facet, host_id: host2.id, uuid: 'uuid-2')
234
+ host3 = FactoryBot.create(:host, :managed, organization: @org)
235
+ host3.insights = FactoryBot.create(:insights_facet, host_id: host3.id, uuid: 'uuid-3')
236
+
237
+ results = Host::Managed.search_for('insights_uuid ^ (uuid-1,uuid-2)')
238
+
239
+ assert_includes results, host1
240
+ assert_includes results, host2
241
+ assert_not_includes results, host3
242
+ end
243
+
244
+ test 'searches with ^ operator (IN) in IoP mode - THE BUG FIX' do
245
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
246
+ host1 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
247
+ host2 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
248
+ host3 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
249
+
250
+ # Create insights facets with stale UUIDs to verify we're using subscription_facet
251
+ host1.insights = FactoryBot.create(:insights_facet, host_id: host1.id, uuid: 'stale-1')
252
+ host2.insights = FactoryBot.create(:insights_facet, host_id: host2.id, uuid: 'stale-2')
253
+ host3.insights = FactoryBot.create(:insights_facet, host_id: host3.id, uuid: 'stale-3')
254
+
255
+ uuid1 = host1.subscription_facet.uuid
256
+ uuid2 = host2.subscription_facet.uuid
257
+
258
+ # This is the search query that remediation modal creates
259
+ results = Host::Managed.search_for("insights_uuid ^ (#{uuid1},#{uuid2})")
260
+
261
+ # Should find hosts by subscription_facet UUID, not insights_facet UUID
262
+ assert_includes results, host1
263
+ assert_includes results, host2
264
+ assert_not_includes results, host3
265
+ end
266
+
267
+ test 'searches with !^ operator (NOT IN) in non-IoP mode' do
268
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
269
+ host1 = FactoryBot.create(:host, :managed, organization: @org)
270
+ host1.insights = FactoryBot.create(:insights_facet, host_id: host1.id, uuid: 'uuid-1')
271
+ host2 = FactoryBot.create(:host, :managed, organization: @org)
272
+ host2.insights = FactoryBot.create(:insights_facet, host_id: host2.id, uuid: 'uuid-2')
273
+ host3 = FactoryBot.create(:host, :managed, organization: @org)
274
+ host3.insights = FactoryBot.create(:insights_facet, host_id: host3.id, uuid: 'uuid-3')
275
+
276
+ results = Host::Managed.search_for('insights_uuid !^ (uuid-1,uuid-2)')
277
+
278
+ assert_not_includes results, host1
279
+ assert_not_includes results, host2
280
+ assert_includes results, host3
281
+ end
282
+
283
+ test 'searches with !^ operator (NOT IN) in IoP mode' do
284
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
285
+ host1 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
286
+ host2 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
287
+ host3 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
288
+
289
+ uuid1 = host1.subscription_facet.uuid
290
+ uuid2 = host2.subscription_facet.uuid
291
+
292
+ results = Host::Managed.search_for("insights_uuid !^ (#{uuid1},#{uuid2})")
293
+
294
+ assert_not_includes results, host1
295
+ assert_not_includes results, host2
296
+ assert_includes results, host3
297
+ end
298
+
299
+ test 'handles hosts without facets in non-IoP mode' do
300
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
301
+ host_without_facet = FactoryBot.create(:host, :managed, organization: @org)
302
+ host_with_facet = FactoryBot.create(:host, :managed, organization: @org)
303
+ host_with_facet.insights = FactoryBot.create(:insights_facet, host_id: host_with_facet.id, uuid: 'uuid-1')
304
+
305
+ results = Host::Managed.search_for('insights_uuid = uuid-1')
306
+
307
+ assert_includes results, host_with_facet
308
+ assert_not_includes results, host_without_facet
309
+ end
310
+
311
+ test 'handles hosts without subscription_facet in IoP mode' do
312
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
313
+ host_without_sub = FactoryBot.create(:host, :managed, organization: @org)
314
+ host_with_sub = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
315
+
316
+ uuid = host_with_sub.subscription_facet.uuid
317
+
318
+ results = Host::Managed.search_for("insights_uuid = #{uuid}")
319
+
320
+ assert_includes results, host_with_sub
321
+ assert_not_includes results, host_without_sub
322
+ end
323
+
324
+ test 'mode changes are reflected in searches' do
325
+ host1 = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
326
+ host1.insights = FactoryBot.create(:insights_facet, host_id: host1.id, uuid: 'insights-uuid-abc')
327
+ insights_uuid = 'insights-uuid-abc'
328
+ subscription_uuid = host1.subscription_facet.uuid
329
+
330
+ # Non-IoP mode: should find by insights_facet UUID
331
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
332
+ results = Host::Managed.search_for("insights_uuid = #{insights_uuid}")
333
+ assert_includes results, host1
334
+
335
+ # IoP mode: should find by subscription_facet UUID
336
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
337
+ results = Host::Managed.search_for("insights_uuid = #{subscription_uuid}")
338
+ assert_includes results, host1
339
+
340
+ # Should NOT find by old insights_facet UUID in IoP mode
341
+ results = Host::Managed.search_for("insights_uuid = #{insights_uuid}")
342
+ assert_not_includes results, host1
343
+ end
344
+ end
191
345
  end
@@ -32,13 +32,28 @@ const NewHostDetailsTab = ({ hostName, router }) => {
32
32
  const hits = useSelector(selectHits);
33
33
  const isIop = useIopConfig();
34
34
 
35
- useEffect(() => () => router.replace({ search: null }), [router]);
35
+ useEffect(
36
+ () => () => {
37
+ // Preserve hash when clearing search params to prevent tab navigation bugs
38
+ if (router && typeof router.replace === 'function') {
39
+ const replaceOptions = { search: null };
40
+ if (router.location && router.location.hash) {
41
+ replaceOptions.hash = router.location.hash;
42
+ }
43
+ router.replace(replaceOptions);
44
+ }
45
+ },
46
+ [router]
47
+ );
36
48
 
37
49
  const onSearch = q => dispatch(fetchInsights({ query: q, page: 1 }));
38
50
 
39
51
  const [isDropdownOpen, setIsDropdownOpen] = useState(false);
40
- const onSatInsightsClick = () =>
41
- router.push({ pathname: '/foreman_rh_cloud/insights_cloud' });
52
+ const onSatInsightsClick = () => {
53
+ if (router && typeof router.push === 'function') {
54
+ router.push({ pathname: '/foreman_rh_cloud/insights_cloud' });
55
+ }
56
+ };
42
57
 
43
58
  const dropdownItems = [
44
59
  <DropdownItem key="insights-link" ouiaId="insights-link">
@@ -0,0 +1,154 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { Provider } from 'react-redux';
5
+ import configureMockStore from 'redux-mock-store';
6
+ import thunk from 'redux-thunk';
7
+ import NewHostDetailsTab from '../NewHostDetailsTab';
8
+
9
+ jest.mock('../../common/Hooks/ConfigHooks', () => ({
10
+ useIopConfig: jest.fn(() => false),
11
+ }));
12
+
13
+ jest.mock('foremanReact/common/I18n', () => ({
14
+ translate: jest.fn(str => str),
15
+ }));
16
+
17
+ const mockStore = configureMockStore([thunk]);
18
+
19
+ describe('NewHostDetailsTab', () => {
20
+ let store;
21
+ let mockRouter;
22
+
23
+ beforeEach(() => {
24
+ mockRouter = {
25
+ push: jest.fn(),
26
+ replace: jest.fn(),
27
+ location: {
28
+ pathname: '/new/hosts/test-host.example.com',
29
+ search: '?page=1&per_page=20',
30
+ hash: '#/Insights',
31
+ query: { page: '1', per_page: '20' },
32
+ },
33
+ };
34
+
35
+ store = mockStore({
36
+ API: {},
37
+ ForemanRhCloud: {
38
+ InsightsCloudSync: {
39
+ table: {
40
+ selectedIds: {},
41
+ isAllSelected: false,
42
+ showSelectAllAlert: false,
43
+ },
44
+ },
45
+ },
46
+ insightsHostDetailsTab: {
47
+ query: '',
48
+ hits: [],
49
+ selectedIds: {},
50
+ error: null,
51
+ },
52
+ router: {
53
+ location: {
54
+ pathname: '/new/hosts/test-host.example.com',
55
+ search: '?page=1&per_page=20',
56
+ hash: '#/Insights',
57
+ query: { page: '1', per_page: '20' },
58
+ },
59
+ },
60
+ });
61
+ });
62
+
63
+ afterEach(() => {
64
+ jest.clearAllMocks();
65
+ });
66
+
67
+ describe('cleanup effect', () => {
68
+ it('should preserve hash when clearing search params on unmount', () => {
69
+ const { unmount } = render(
70
+ <Provider store={store}>
71
+ <NewHostDetailsTab
72
+ hostName="test-host.example.com"
73
+ router={mockRouter}
74
+ />
75
+ </Provider>
76
+ );
77
+
78
+ // Unmount the component to trigger cleanup
79
+ unmount();
80
+
81
+ // Verify router.replace was called with both search: null AND the existing hash
82
+ expect(mockRouter.replace).toHaveBeenCalledWith({
83
+ search: null,
84
+ hash: '#/Insights',
85
+ });
86
+ });
87
+
88
+ it('should only clear search params when no hash exists', () => {
89
+ mockRouter.location.hash = '';
90
+
91
+ const { unmount } = render(
92
+ <Provider store={store}>
93
+ <NewHostDetailsTab
94
+ hostName="test-host.example.com"
95
+ router={mockRouter}
96
+ />
97
+ </Provider>
98
+ );
99
+
100
+ unmount();
101
+
102
+ // When there's no hash, should only pass search: null
103
+ expect(mockRouter.replace).toHaveBeenCalledWith({
104
+ search: null,
105
+ });
106
+ });
107
+
108
+ it('should handle router.location being undefined gracefully', () => {
109
+ const routerWithoutLocation = {
110
+ push: jest.fn(),
111
+ replace: jest.fn(),
112
+ location: undefined,
113
+ };
114
+
115
+ const { unmount } = render(
116
+ <Provider store={store}>
117
+ <NewHostDetailsTab
118
+ hostName="test-host.example.com"
119
+ router={routerWithoutLocation}
120
+ />
121
+ </Provider>
122
+ );
123
+
124
+ unmount();
125
+
126
+ // Should still call replace with search: null even if location is undefined
127
+ expect(routerWithoutLocation.replace).toHaveBeenCalledWith({
128
+ search: null,
129
+ });
130
+ });
131
+
132
+ it('should use the latest hash value at unmount time, not a stale captured value', () => {
133
+ const { unmount } = render(
134
+ <Provider store={store}>
135
+ <NewHostDetailsTab
136
+ hostName="test-host.example.com"
137
+ router={mockRouter}
138
+ />
139
+ </Provider>
140
+ );
141
+
142
+ // Change the hash after mount, before unmount
143
+ mockRouter.location.hash = '#/Overview';
144
+
145
+ unmount();
146
+
147
+ // Verify router.replace was called with the UPDATED hash, not the initial '#/Insights'
148
+ expect(mockRouter.replace).toHaveBeenCalledWith({
149
+ search: null,
150
+ hash: '#/Overview',
151
+ });
152
+ });
153
+ });
154
+ });
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: 12.2.16
4
+ version: 12.2.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Red Hat Cloud team
@@ -629,6 +629,7 @@ files:
629
629
  - webpack/InsightsHostDetailsTab/__tests__/InsightsTabReducer.test.js
630
630
  - webpack/InsightsHostDetailsTab/__tests__/InsightsTabSelectors.test.js
631
631
  - webpack/InsightsHostDetailsTab/__tests__/InsightsTotalRiskChart.test.js
632
+ - webpack/InsightsHostDetailsTab/__tests__/NewHostDetailsTab.test.js
632
633
  - webpack/InsightsHostDetailsTab/__tests__/__snapshots__/InsightsTab.test.js.snap
633
634
  - webpack/InsightsHostDetailsTab/__tests__/__snapshots__/InsightsTabActions.test.js.snap
634
635
  - webpack/InsightsHostDetailsTab/__tests__/__snapshots__/InsightsTabReducer.test.js.snap