foreman_openbolt 1.0.0 → 1.1.1
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 +4 -4
- data/README.md +190 -19
- data/Rakefile +17 -93
- data/app/controllers/foreman_openbolt/task_controller.rb +61 -49
- data/app/lib/actions/foreman_openbolt/cleanup_proxy_artifacts.rb +11 -10
- data/app/lib/actions/foreman_openbolt/poll_task_status.rb +70 -60
- data/app/models/foreman_openbolt/task_job.rb +16 -17
- data/config/routes.rb +0 -1
- data/lib/foreman_openbolt/engine.rb +11 -11
- data/lib/foreman_openbolt/version.rb +1 -1
- data/lib/proxy_api/openbolt.rb +25 -9
- data/lib/tasks/foreman_openbolt_tasks.rake +1 -22
- data/locale/gemspec.rb +1 -1
- data/package.json +11 -15
- data/test/acceptance/acceptance_helper.rb +146 -0
- data/test/acceptance/docker/docker-compose.yml +69 -0
- data/test/acceptance/docker/foreman/Dockerfile +45 -0
- data/test/acceptance/docker/foreman/entrypoint.sh +26 -0
- data/test/acceptance/docker/target/Dockerfile +29 -0
- data/test/acceptance/docker/target/entrypoint.sh +11 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.json +30 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.sh +16 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/echo.json +13 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/echo.sh +3 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.json +8 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.sh +3 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.json +8 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.sh +2 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.json +14 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.sh +3 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.json +13 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.sh +9 -0
- data/test/acceptance/fixtures/openbolt.yml +7 -0
- data/test/acceptance/tests/error_handling_test.rb +40 -0
- data/test/acceptance/tests/host_selector_test.rb +31 -0
- data/test/acceptance/tests/launch_task_test.rb +96 -0
- data/test/acceptance/tests/parameter_table_test.rb +61 -0
- data/test/acceptance/tests/settings_test.rb +95 -0
- data/test/acceptance/tests/ssh_options_test.rb +77 -0
- data/test/acceptance/tests/task_execution_test.rb +40 -0
- data/test/acceptance/tests/task_history_test.rb +84 -0
- data/test/acceptance/tests/transport_options_test.rb +121 -0
- data/test/test_plugin_helper.rb +12 -3
- data/test/unit/controllers/task_controller_test.rb +351 -0
- data/test/unit/docker/Dockerfile +47 -0
- data/test/unit/docker/docker-compose.yml +33 -0
- data/test/unit/docker/entrypoint.sh +4 -0
- data/test/unit/factories/foreman_openbolt_factories.rb +39 -0
- data/test/unit/lib/actions/cleanup_proxy_artifacts_test.rb +51 -0
- data/test/unit/lib/actions/poll_task_status_test.rb +141 -0
- data/test/unit/lib/proxy_api/openbolt_test.rb +174 -0
- data/test/unit/models/task_job_test.rb +278 -0
- data/webpack/__mocks__/foremanReact/common/I18n.js +15 -0
- data/webpack/__mocks__/foremanReact/components/ToastsList/index.js +6 -0
- data/webpack/__mocks__/foremanReact/redux/API/index.js +11 -0
- data/webpack/src/Components/LaunchTask/FieldTable.js +8 -5
- data/webpack/src/Components/LaunchTask/HostSelector/SearchSelect.js +74 -62
- data/webpack/src/Components/LaunchTask/HostSelector/SelectedChips.js +11 -13
- data/webpack/src/Components/LaunchTask/HostSelector/index.js +28 -33
- data/webpack/src/Components/LaunchTask/OpenBoltOptionsSection.js +3 -2
- data/webpack/src/Components/LaunchTask/ParameterField.js +2 -0
- data/webpack/src/Components/LaunchTask/SmartProxySelect.js +2 -1
- data/webpack/src/Components/LaunchTask/TaskSelect.js +3 -3
- data/webpack/src/Components/LaunchTask/__tests__/EmptyContent.test.js +10 -0
- data/webpack/src/Components/LaunchTask/__tests__/LaunchTask.test.js +83 -0
- data/webpack/src/Components/LaunchTask/__tests__/ParameterField.test.js +86 -0
- data/webpack/src/Components/LaunchTask/__tests__/ParametersSection.test.js +50 -0
- data/webpack/src/Components/LaunchTask/__tests__/SmartProxySelect.test.js +63 -0
- data/webpack/src/Components/LaunchTask/__tests__/TaskSelect.test.js +39 -0
- data/webpack/src/Components/LaunchTask/hooks/__tests__/useOpenBoltOptions.test.js +90 -0
- data/webpack/src/Components/LaunchTask/hooks/__tests__/useSmartProxies.test.js +69 -0
- data/webpack/src/Components/LaunchTask/hooks/__tests__/useTasksData.test.js +103 -0
- data/webpack/src/Components/LaunchTask/hooks/useOpenBoltOptions.js +9 -11
- data/webpack/src/Components/LaunchTask/hooks/useSmartProxies.js +12 -13
- data/webpack/src/Components/LaunchTask/hooks/useTasksData.js +6 -13
- data/webpack/src/Components/LaunchTask/index.js +9 -27
- data/webpack/src/Components/TaskExecution/ExecutionDetails.js +29 -29
- data/webpack/src/Components/TaskExecution/ExecutionDisplay.js +9 -10
- data/webpack/src/Components/TaskExecution/LoadingIndicator.js +7 -2
- data/webpack/src/Components/TaskExecution/ResultDisplay.js +13 -17
- data/webpack/src/Components/TaskExecution/TaskDetails.js +58 -67
- data/webpack/src/Components/TaskExecution/__tests__/ExecutionDetails.test.js +47 -0
- data/webpack/src/Components/TaskExecution/__tests__/ExecutionDisplay.test.js +29 -0
- data/webpack/src/Components/TaskExecution/__tests__/LoadingIndicator.test.js +25 -0
- data/webpack/src/Components/TaskExecution/__tests__/ResultDisplay.test.js +28 -0
- data/webpack/src/Components/TaskExecution/__tests__/TaskDetails.test.js +38 -0
- data/webpack/src/Components/TaskExecution/__tests__/TaskExecution.test.js +80 -0
- data/webpack/src/Components/TaskExecution/hooks/__tests__/useJobPolling.test.js +177 -0
- data/webpack/src/Components/TaskExecution/hooks/useJobPolling.js +34 -33
- data/webpack/src/Components/TaskExecution/index.js +10 -12
- data/webpack/src/Components/TaskHistory/TaskPopover.js +9 -12
- data/webpack/src/Components/TaskHistory/__tests__/TaskHistory.test.js +109 -0
- data/webpack/src/Components/TaskHistory/__tests__/TaskPopover.test.js +26 -0
- data/webpack/src/Components/TaskHistory/index.js +21 -29
- data/webpack/src/Components/common/HostsPopover.js +12 -3
- data/webpack/src/Components/common/__tests__/HostsPopover.test.js +20 -0
- data/webpack/src/Components/common/__tests__/helpers.test.js +135 -0
- data/webpack/src/Components/common/helpers.js +34 -5
- data/webpack/test_setup.js +34 -11
- metadata +65 -87
- data/test/factories/foreman_openbolt_factories.rb +0 -7
- data/test/unit/foreman_openbolt_test.rb +0 -13
- data/webpack/global_test_setup.js +0 -11
- data/webpack/webpack.config.js +0 -7
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import ParametersSection from '../ParametersSection';
|
|
4
|
+
|
|
5
|
+
describe('ParametersSection', () => {
|
|
6
|
+
test('shows empty state when no task is selected', () => {
|
|
7
|
+
render(
|
|
8
|
+
<ParametersSection
|
|
9
|
+
selectedTask=""
|
|
10
|
+
taskMetadata={{}}
|
|
11
|
+
taskParameters={{}}
|
|
12
|
+
onParameterChange={jest.fn()}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
expect(
|
|
16
|
+
screen.getByText('Select a task to see parameters')
|
|
17
|
+
).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('shows empty state when task has no parameters', () => {
|
|
21
|
+
render(
|
|
22
|
+
<ParametersSection
|
|
23
|
+
selectedTask="test::task"
|
|
24
|
+
taskMetadata={{ 'test::task': { parameters: {} } }}
|
|
25
|
+
taskParameters={{}}
|
|
26
|
+
onParameterChange={jest.fn()}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
expect(screen.getByText('This task has no parameters')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('renders parameter fields when task has parameters', () => {
|
|
33
|
+
const metadata = {
|
|
34
|
+
'test::task': {
|
|
35
|
+
parameters: {
|
|
36
|
+
name: { type: 'String', description: 'Package name' },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
render(
|
|
41
|
+
<ParametersSection
|
|
42
|
+
selectedTask="test::task"
|
|
43
|
+
taskMetadata={metadata}
|
|
44
|
+
taskParameters={{ name: 'nginx' }}
|
|
45
|
+
onParameterChange={jest.fn()}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
expect(screen.getByText('name')).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import SmartProxySelect from '../SmartProxySelect';
|
|
4
|
+
|
|
5
|
+
const proxies = [
|
|
6
|
+
{ id: 1, name: 'proxy-one' },
|
|
7
|
+
{ id: 2, name: 'proxy-two' },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
describe('SmartProxySelect', () => {
|
|
11
|
+
test('renders proxy options', () => {
|
|
12
|
+
render(
|
|
13
|
+
<SmartProxySelect
|
|
14
|
+
smartProxies={proxies}
|
|
15
|
+
selectedProxy=""
|
|
16
|
+
onProxyChange={jest.fn()}
|
|
17
|
+
isLoading={false}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
expect(screen.getByText('proxy-one')).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByText('proxy-two')).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('shows loading placeholder when loading', () => {
|
|
25
|
+
render(
|
|
26
|
+
<SmartProxySelect
|
|
27
|
+
smartProxies={[]}
|
|
28
|
+
selectedProxy=""
|
|
29
|
+
onProxyChange={jest.fn()}
|
|
30
|
+
isLoading
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('disables select when loading', () => {
|
|
37
|
+
const { container } = render(
|
|
38
|
+
<SmartProxySelect
|
|
39
|
+
smartProxies={proxies}
|
|
40
|
+
selectedProxy=""
|
|
41
|
+
onProxyChange={jest.fn()}
|
|
42
|
+
isLoading
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
const select = container.querySelector('select');
|
|
46
|
+
expect(select).toBeDisabled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('calls onProxyChange when selection changes', () => {
|
|
50
|
+
const handleChange = jest.fn();
|
|
51
|
+
const { container } = render(
|
|
52
|
+
<SmartProxySelect
|
|
53
|
+
smartProxies={proxies}
|
|
54
|
+
selectedProxy=""
|
|
55
|
+
onProxyChange={handleChange}
|
|
56
|
+
isLoading={false}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
const select = container.querySelector('select');
|
|
60
|
+
fireEvent.change(select, { target: { value: '1' } });
|
|
61
|
+
expect(handleChange).toHaveBeenCalledWith(expect.anything(), '1');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import TaskSelect from '../TaskSelect';
|
|
4
|
+
|
|
5
|
+
const defaultProps = {
|
|
6
|
+
taskNames: ['mymod::install', 'mymod::mytask'],
|
|
7
|
+
selectedTask: '',
|
|
8
|
+
onTaskChange: jest.fn(),
|
|
9
|
+
onReloadTasks: jest.fn(),
|
|
10
|
+
isLoading: false,
|
|
11
|
+
isDisabled: false,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe('TaskSelect', () => {
|
|
15
|
+
test('renders task options', () => {
|
|
16
|
+
render(<TaskSelect {...defaultProps} />);
|
|
17
|
+
expect(screen.getByText('mymod::install')).toBeInTheDocument();
|
|
18
|
+
expect(screen.getByText('mymod::mytask')).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('shows loading placeholder when loading', () => {
|
|
22
|
+
render(<TaskSelect {...defaultProps} isLoading />);
|
|
23
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('disables select and button when disabled', () => {
|
|
27
|
+
const { container } = render(<TaskSelect {...defaultProps} isDisabled />);
|
|
28
|
+
const select = container.querySelector('select');
|
|
29
|
+
expect(select).toBeDisabled();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('calls onReloadTasks when reload button is clicked', () => {
|
|
33
|
+
const handleReload = jest.fn();
|
|
34
|
+
render(<TaskSelect {...defaultProps} onReloadTasks={handleReload} />);
|
|
35
|
+
const reloadButton = screen.getByLabelText('Reload tasks from OpenBolt');
|
|
36
|
+
fireEvent.click(reloadButton);
|
|
37
|
+
expect(handleReload).toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderHook, act } from '@testing-library/react-hooks';
|
|
3
|
+
import { Provider } from 'react-redux';
|
|
4
|
+
import { createStore } from 'redux';
|
|
5
|
+
import { API } from 'foremanReact/redux/API';
|
|
6
|
+
import { addToast } from 'foremanReact/components/ToastsList';
|
|
7
|
+
import { useOpenBoltOptions } from '../useOpenBoltOptions';
|
|
8
|
+
|
|
9
|
+
const mockStore = createStore(() => ({}));
|
|
10
|
+
const wrapper = ({ children }) => (
|
|
11
|
+
<Provider store={mockStore}>{children}</Provider>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('useOpenBoltOptions', () => {
|
|
19
|
+
test('returns null and does not call API when proxyId is falsy', async () => {
|
|
20
|
+
const { result } = renderHook(() => useOpenBoltOptions(), { wrapper });
|
|
21
|
+
|
|
22
|
+
let returnValue;
|
|
23
|
+
await act(async () => {
|
|
24
|
+
returnValue = await result.current.fetchOpenBoltOptions(null);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(returnValue).toBeNull();
|
|
28
|
+
expect(API.get).not.toHaveBeenCalled();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('fetches options and extracts defaults', async () => {
|
|
32
|
+
const options = {
|
|
33
|
+
transport: { type: 'string', default: 'ssh' },
|
|
34
|
+
user: { type: 'string' },
|
|
35
|
+
verbose: { type: 'boolean', default: false },
|
|
36
|
+
};
|
|
37
|
+
API.get.mockResolvedValue({ data: options });
|
|
38
|
+
|
|
39
|
+
const { result } = renderHook(() => useOpenBoltOptions(), { wrapper });
|
|
40
|
+
|
|
41
|
+
await act(async () => {
|
|
42
|
+
await result.current.fetchOpenBoltOptions(42);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result.current.openBoltOptionsMetadata).toEqual(options);
|
|
46
|
+
expect(result.current.openBoltOptions).toEqual({
|
|
47
|
+
transport: 'ssh',
|
|
48
|
+
verbose: false,
|
|
49
|
+
});
|
|
50
|
+
expect(result.current.openBoltOptions.user).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('resets state before fetching', async () => {
|
|
54
|
+
API.get.mockResolvedValue({ data: { transport: { default: 'ssh' } } });
|
|
55
|
+
|
|
56
|
+
const { result } = renderHook(() => useOpenBoltOptions(), { wrapper });
|
|
57
|
+
|
|
58
|
+
await act(async () => {
|
|
59
|
+
await result.current.fetchOpenBoltOptions(1);
|
|
60
|
+
});
|
|
61
|
+
expect(result.current.openBoltOptions).toEqual({ transport: 'ssh' });
|
|
62
|
+
|
|
63
|
+
API.get.mockResolvedValue({ data: { user: { default: 'root' } } });
|
|
64
|
+
await act(async () => {
|
|
65
|
+
await result.current.fetchOpenBoltOptions(2);
|
|
66
|
+
});
|
|
67
|
+
expect(result.current.openBoltOptions).toEqual({ user: 'root' });
|
|
68
|
+
expect(result.current.openBoltOptions.transport).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('handles errors and shows toast', async () => {
|
|
72
|
+
API.get.mockRejectedValue({ message: 'Connection refused' });
|
|
73
|
+
|
|
74
|
+
const { result } = renderHook(() => useOpenBoltOptions(), { wrapper });
|
|
75
|
+
|
|
76
|
+
let returnValue;
|
|
77
|
+
await act(async () => {
|
|
78
|
+
returnValue = await result.current.fetchOpenBoltOptions(42);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(returnValue).toBeNull();
|
|
82
|
+
expect(result.current.isLoadingOptions).toBe(false);
|
|
83
|
+
expect(addToast).toHaveBeenCalledWith(
|
|
84
|
+
expect.objectContaining({
|
|
85
|
+
type: 'danger',
|
|
86
|
+
message: expect.stringContaining('Failed to load OpenBolt options'),
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderHook } from '@testing-library/react-hooks';
|
|
3
|
+
import { Provider } from 'react-redux';
|
|
4
|
+
import { createStore } from 'redux';
|
|
5
|
+
import { API } from 'foremanReact/redux/API';
|
|
6
|
+
import { addToast } from 'foremanReact/components/ToastsList';
|
|
7
|
+
import { useSmartProxies } from '../useSmartProxies';
|
|
8
|
+
|
|
9
|
+
const mockStore = createStore(() => ({}));
|
|
10
|
+
const wrapper = ({ children }) => (
|
|
11
|
+
<Provider store={mockStore}>{children}</Provider>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('useSmartProxies', () => {
|
|
19
|
+
test('fetches proxies on mount and returns results', async () => {
|
|
20
|
+
const proxies = [
|
|
21
|
+
{ id: 1, name: 'proxy1' },
|
|
22
|
+
{ id: 2, name: 'proxy2' },
|
|
23
|
+
];
|
|
24
|
+
API.get.mockResolvedValue({ data: { results: proxies } });
|
|
25
|
+
|
|
26
|
+
const { result, waitForNextUpdate } = renderHook(() => useSmartProxies(), {
|
|
27
|
+
wrapper,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(result.current.isLoadingProxies).toBe(true);
|
|
31
|
+
|
|
32
|
+
await waitForNextUpdate();
|
|
33
|
+
|
|
34
|
+
expect(result.current.isLoadingProxies).toBe(false);
|
|
35
|
+
expect(result.current.smartProxies).toEqual(proxies);
|
|
36
|
+
expect(API.get).toHaveBeenCalledWith(
|
|
37
|
+
expect.stringContaining('/api/smart_proxies')
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('returns empty array when no proxies found', async () => {
|
|
42
|
+
API.get.mockResolvedValue({ data: { results: [] } });
|
|
43
|
+
|
|
44
|
+
const { result, waitForNextUpdate } = renderHook(() => useSmartProxies(), {
|
|
45
|
+
wrapper,
|
|
46
|
+
});
|
|
47
|
+
await waitForNextUpdate();
|
|
48
|
+
|
|
49
|
+
expect(result.current.smartProxies).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('handles fetch error and shows toast', async () => {
|
|
53
|
+
API.get.mockRejectedValue({ message: 'Network error' });
|
|
54
|
+
|
|
55
|
+
const { result, waitForNextUpdate } = renderHook(() => useSmartProxies(), {
|
|
56
|
+
wrapper,
|
|
57
|
+
});
|
|
58
|
+
await waitForNextUpdate();
|
|
59
|
+
|
|
60
|
+
expect(result.current.isLoadingProxies).toBe(false);
|
|
61
|
+
expect(result.current.smartProxies).toEqual([]);
|
|
62
|
+
expect(addToast).toHaveBeenCalledWith(
|
|
63
|
+
expect.objectContaining({
|
|
64
|
+
type: 'danger',
|
|
65
|
+
message: expect.stringContaining('Failed to load Smart Proxies'),
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderHook, act } from '@testing-library/react-hooks';
|
|
3
|
+
import { Provider } from 'react-redux';
|
|
4
|
+
import { createStore } from 'redux';
|
|
5
|
+
import { API } from 'foremanReact/redux/API';
|
|
6
|
+
import { addToast } from 'foremanReact/components/ToastsList';
|
|
7
|
+
import { useTasksData } from '../useTasksData';
|
|
8
|
+
|
|
9
|
+
const mockStore = createStore(() => ({}));
|
|
10
|
+
const wrapper = ({ children }) => (
|
|
11
|
+
<Provider store={mockStore}>{children}</Provider>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('useTasksData', () => {
|
|
19
|
+
test('fetchTasks returns null and does not call API when proxyId is falsy', async () => {
|
|
20
|
+
const { result } = renderHook(() => useTasksData(), { wrapper });
|
|
21
|
+
|
|
22
|
+
let returnValue;
|
|
23
|
+
await act(async () => {
|
|
24
|
+
returnValue = await result.current.fetchTasks(null);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(returnValue).toBeNull();
|
|
28
|
+
expect(API.get).not.toHaveBeenCalled();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('fetchTasks fetches tasks from the normal endpoint', async () => {
|
|
32
|
+
const tasks = { 'mymod::install': { description: 'Install a package' } };
|
|
33
|
+
API.get.mockResolvedValue({ data: tasks });
|
|
34
|
+
|
|
35
|
+
const { result } = renderHook(() => useTasksData(), { wrapper });
|
|
36
|
+
|
|
37
|
+
await act(async () => {
|
|
38
|
+
await result.current.fetchTasks(42);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(API.get).toHaveBeenCalledWith(
|
|
42
|
+
expect.stringContaining('fetch_tasks')
|
|
43
|
+
);
|
|
44
|
+
expect(API.get).toHaveBeenCalledWith(
|
|
45
|
+
expect.stringContaining('proxy_id=42')
|
|
46
|
+
);
|
|
47
|
+
expect(result.current.taskMetadata).toEqual(tasks);
|
|
48
|
+
expect(result.current.isLoadingTasks).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('fetchTasks uses reload endpoint when forceReload is true', async () => {
|
|
52
|
+
API.get.mockResolvedValue({ data: {} });
|
|
53
|
+
|
|
54
|
+
const { result } = renderHook(() => useTasksData(), { wrapper });
|
|
55
|
+
|
|
56
|
+
await act(async () => {
|
|
57
|
+
await result.current.fetchTasks(42, true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(API.get).toHaveBeenCalledWith(
|
|
61
|
+
expect.stringContaining('reload_tasks')
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('fetchTasks resets state before fetching', async () => {
|
|
66
|
+
API.get.mockResolvedValue({ data: { 'task::one': {} } });
|
|
67
|
+
|
|
68
|
+
const { result } = renderHook(() => useTasksData(), { wrapper });
|
|
69
|
+
|
|
70
|
+
// First fetch
|
|
71
|
+
await act(async () => {
|
|
72
|
+
await result.current.fetchTasks(1);
|
|
73
|
+
});
|
|
74
|
+
expect(Object.keys(result.current.taskMetadata)).toHaveLength(1);
|
|
75
|
+
|
|
76
|
+
// Second fetch should reset
|
|
77
|
+
API.get.mockResolvedValue({ data: { 'task::two': {}, 'task::three': {} } });
|
|
78
|
+
await act(async () => {
|
|
79
|
+
await result.current.fetchTasks(2);
|
|
80
|
+
});
|
|
81
|
+
expect(Object.keys(result.current.taskMetadata)).toHaveLength(2);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('fetchTasks handles errors and shows toast', async () => {
|
|
85
|
+
API.get.mockRejectedValue({ message: 'Network error' });
|
|
86
|
+
|
|
87
|
+
const { result } = renderHook(() => useTasksData(), { wrapper });
|
|
88
|
+
|
|
89
|
+
let returnValue;
|
|
90
|
+
await act(async () => {
|
|
91
|
+
returnValue = await result.current.fetchTasks(42);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(returnValue).toBeNull();
|
|
95
|
+
expect(result.current.isLoadingTasks).toBe(false);
|
|
96
|
+
expect(addToast).toHaveBeenCalledWith(
|
|
97
|
+
expect.objectContaining({
|
|
98
|
+
type: 'danger',
|
|
99
|
+
message: expect.stringContaining('Failed to load tasks'),
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react';
|
|
2
|
-
import { translate as __ } from 'foremanReact/common/I18n';
|
|
2
|
+
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
|
3
3
|
import { API } from 'foremanReact/redux/API';
|
|
4
4
|
import { ROUTES } from '../../common/constants';
|
|
5
|
-
import { useShowMessage } from '../../common/helpers';
|
|
5
|
+
import { useShowMessage, extractErrorMessage } from '../../common/helpers';
|
|
6
6
|
|
|
7
7
|
export const useOpenBoltOptions = () => {
|
|
8
8
|
const showMessage = useShowMessage();
|
|
@@ -20,17 +20,10 @@ export const useOpenBoltOptions = () => {
|
|
|
20
20
|
setOpenBoltOptions({});
|
|
21
21
|
|
|
22
22
|
try {
|
|
23
|
-
const { data
|
|
23
|
+
const { data } = await API.get(
|
|
24
24
|
`${ROUTES.API.FETCH_OPENBOLT_OPTIONS}?proxy_id=${proxyId}`
|
|
25
25
|
);
|
|
26
26
|
|
|
27
|
-
if (status !== 200) {
|
|
28
|
-
const error = data
|
|
29
|
-
? data.error || JSON.stringify(data)
|
|
30
|
-
: 'Unknown error';
|
|
31
|
-
throw new Error(`HTTP ${status} - ${error}`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
27
|
setOpenBoltOptionsMetadata(data || {});
|
|
35
28
|
|
|
36
29
|
// Set defaults
|
|
@@ -44,7 +37,12 @@ export const useOpenBoltOptions = () => {
|
|
|
44
37
|
|
|
45
38
|
return data;
|
|
46
39
|
} catch (error) {
|
|
47
|
-
showMessage(
|
|
40
|
+
showMessage(
|
|
41
|
+
sprintf(
|
|
42
|
+
__('Failed to load OpenBolt options: %s'),
|
|
43
|
+
extractErrorMessage(error)
|
|
44
|
+
)
|
|
45
|
+
);
|
|
48
46
|
return null;
|
|
49
47
|
} finally {
|
|
50
48
|
setIsLoadingOptions(false);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
|
-
import { translate as __ } from 'foremanReact/common/I18n';
|
|
2
|
+
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
|
3
3
|
import { API } from 'foremanReact/redux/API';
|
|
4
|
-
import { useShowMessage } from '../../common/helpers';
|
|
4
|
+
import { useShowMessage, extractErrorMessage } from '../../common/helpers';
|
|
5
5
|
|
|
6
6
|
export const useSmartProxies = () => {
|
|
7
7
|
const showMessage = useShowMessage();
|
|
@@ -16,18 +16,12 @@ export const useSmartProxies = () => {
|
|
|
16
16
|
per_page: 'all',
|
|
17
17
|
search: 'feature=OpenBolt',
|
|
18
18
|
})}`;
|
|
19
|
-
const { data
|
|
19
|
+
const { data } = await API.get(endpoint);
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
? data.error || JSON.stringify(data)
|
|
24
|
-
: 'Unknown error';
|
|
25
|
-
throw new Error(`HTTP ${status} - ${error}`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
setSmartProxies(data.results || []);
|
|
21
|
+
const results = data.results || [];
|
|
22
|
+
setSmartProxies(results);
|
|
29
23
|
|
|
30
|
-
if (
|
|
24
|
+
if (results.length === 0) {
|
|
31
25
|
showMessage(
|
|
32
26
|
__(
|
|
33
27
|
'No Smart Proxies found. Please check that one or more proxy has the smart_proxy_openbolt package installed and enabled.'
|
|
@@ -35,7 +29,12 @@ export const useSmartProxies = () => {
|
|
|
35
29
|
);
|
|
36
30
|
}
|
|
37
31
|
} catch (error) {
|
|
38
|
-
showMessage(
|
|
32
|
+
showMessage(
|
|
33
|
+
sprintf(
|
|
34
|
+
__('Failed to load Smart Proxies: %s'),
|
|
35
|
+
extractErrorMessage(error)
|
|
36
|
+
)
|
|
37
|
+
);
|
|
39
38
|
} finally {
|
|
40
39
|
setIsLoadingProxies(false);
|
|
41
40
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react';
|
|
2
|
-
import { translate as __ } from 'foremanReact/common/I18n';
|
|
2
|
+
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
|
3
3
|
import { API } from 'foremanReact/redux/API';
|
|
4
4
|
import { ROUTES } from '../../common/constants';
|
|
5
|
-
import { useShowMessage } from '../../common/helpers';
|
|
5
|
+
import { useShowMessage, extractErrorMessage } from '../../common/helpers';
|
|
6
6
|
|
|
7
7
|
export const useTasksData = () => {
|
|
8
8
|
const showMessage = useShowMessage();
|
|
@@ -24,16 +24,7 @@ export const useTasksData = () => {
|
|
|
24
24
|
const endpoint = forceReload
|
|
25
25
|
? ROUTES.API.RELOAD_TASKS
|
|
26
26
|
: ROUTES.API.FETCH_TASKS;
|
|
27
|
-
const { data
|
|
28
|
-
`${endpoint}?proxy_id=${proxyId}`
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
if (status !== 200) {
|
|
32
|
-
const error = data
|
|
33
|
-
? data.error || JSON.stringify(data)
|
|
34
|
-
: 'Unknown error';
|
|
35
|
-
throw new Error(`HTTP ${status} - ${error}`);
|
|
36
|
-
}
|
|
27
|
+
const { data } = await API.get(`${endpoint}?proxy_id=${proxyId}`);
|
|
37
28
|
|
|
38
29
|
setTaskMetadata(data || {});
|
|
39
30
|
|
|
@@ -43,7 +34,9 @@ export const useTasksData = () => {
|
|
|
43
34
|
|
|
44
35
|
return data;
|
|
45
36
|
} catch (error) {
|
|
46
|
-
showMessage(
|
|
37
|
+
showMessage(
|
|
38
|
+
sprintf(__('Failed to load tasks: %s'), extractErrorMessage(error))
|
|
39
|
+
);
|
|
47
40
|
return null;
|
|
48
41
|
} finally {
|
|
49
42
|
setIsLoadingTasks(false);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// TODO: More a11y tags
|
|
2
1
|
import React, { useState, useCallback } from 'react';
|
|
3
2
|
import { useHistory } from 'react-router-dom';
|
|
4
|
-
import { translate as __ } from 'foremanReact/common/I18n';
|
|
3
|
+
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
|
5
4
|
|
|
6
5
|
import { API } from 'foremanReact/redux/API';
|
|
7
6
|
import {
|
|
@@ -28,7 +27,7 @@ import HostSelector from './HostSelector';
|
|
|
28
27
|
import { useSmartProxies } from './hooks/useSmartProxies';
|
|
29
28
|
import { useTasksData } from './hooks/useTasksData';
|
|
30
29
|
import { useOpenBoltOptions } from './hooks/useOpenBoltOptions';
|
|
31
|
-
import { useShowMessage } from '../common/helpers';
|
|
30
|
+
import { useShowMessage, extractErrorMessage } from '../common/helpers';
|
|
32
31
|
|
|
33
32
|
const LaunchTask = () => {
|
|
34
33
|
const history = useHistory();
|
|
@@ -96,7 +95,7 @@ const LaunchTask = () => {
|
|
|
96
95
|
defaults[paramName] = paramMeta.default;
|
|
97
96
|
} else if (
|
|
98
97
|
['boolean', 'optional[boolean]'].includes(
|
|
99
|
-
paramMeta.type
|
|
98
|
+
paramMeta.type?.toLowerCase()
|
|
100
99
|
)
|
|
101
100
|
) {
|
|
102
101
|
defaults[paramName] = false;
|
|
@@ -174,36 +173,18 @@ const LaunchTask = () => {
|
|
|
174
173
|
options: visibleOptions,
|
|
175
174
|
};
|
|
176
175
|
|
|
177
|
-
const { data
|
|
178
|
-
|
|
179
|
-
// TODO: On non-200, the post above automatically throws an exception, so
|
|
180
|
-
// figure out how to handle it instead to extract the message in the
|
|
181
|
-
// response body.
|
|
182
|
-
if (status !== 200) {
|
|
183
|
-
const error = data
|
|
184
|
-
? data.error || JSON.stringify(data)
|
|
185
|
-
: 'Unknown error';
|
|
186
|
-
throw new Error(`HTTP ${status} - ${error}`);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const selectedProxyData = smartProxies.find(
|
|
190
|
-
p => p.id.toString() === selectedProxy.toString()
|
|
191
|
-
);
|
|
176
|
+
const { data } = await API.post(ROUTES.API.LAUNCH_TASK, body);
|
|
192
177
|
|
|
193
178
|
history.push({
|
|
194
179
|
pathname: ROUTES.PAGES.TASK_EXECUTION,
|
|
195
180
|
search: new URLSearchParams({
|
|
196
|
-
proxy_id: selectedProxy,
|
|
197
181
|
job_id: data.job_id,
|
|
198
|
-
proxy_name: selectedProxyData?.name || 'Unknown',
|
|
199
182
|
}).toString(),
|
|
200
183
|
});
|
|
201
184
|
} catch (error) {
|
|
202
|
-
|
|
203
|
-
error
|
|
204
|
-
|
|
205
|
-
__('Unknown error occurred');
|
|
206
|
-
showMessage(__('Failed to launch task: ') + errorMessage);
|
|
185
|
+
showMessage(
|
|
186
|
+
sprintf(__('Failed to launch task: %s'), extractErrorMessage(error))
|
|
187
|
+
);
|
|
207
188
|
} finally {
|
|
208
189
|
setIsSubmitting(false);
|
|
209
190
|
}
|
|
@@ -265,7 +246,8 @@ const LaunchTask = () => {
|
|
|
265
246
|
isDisabled={!isFormValid}
|
|
266
247
|
isLoading={isSubmitting}
|
|
267
248
|
>
|
|
268
|
-
|
|
249
|
+
<span aria-hidden="true">🚀</span>
|
|
250
|
+
{` ${__('Launch Task')}`}
|
|
269
251
|
</Button>
|
|
270
252
|
</FlexItem>
|
|
271
253
|
</Flex>
|