foreman_webhooks 4.0.2 → 5.0.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 (24) hide show
  1. checksums.yaml +4 -4
  2. data/lib/foreman_webhooks/engine.rb +1 -1
  3. data/lib/foreman_webhooks/version.rb +1 -1
  4. data/test/integration/webhooks_test.rb +234 -0
  5. data/webpack/ForemanWebhooks/Routes/Webhooks/Components/WebhookForm/Components/FieldConstructor.js +321 -0
  6. data/webpack/ForemanWebhooks/Routes/Webhooks/Components/WebhookForm/Components/WebhookFormTabs.css +23 -4
  7. data/webpack/ForemanWebhooks/Routes/Webhooks/Components/WebhookForm/Components/WebhookFormTabs.js +191 -157
  8. data/webpack/ForemanWebhooks/Routes/Webhooks/Components/WebhookForm/Components/__tests__/FieldConstructor.test.js +216 -0
  9. data/webpack/ForemanWebhooks/Routes/Webhooks/Components/WebhookForm/WebhookForm.js +55 -26
  10. data/webpack/ForemanWebhooks/Routes/Webhooks/Components/WebhookForm/__tests__/WebhookForm.test.js +253 -37
  11. data/webpack/ForemanWebhooks/Routes/Webhooks/Components/WebhookForm/index.js +3 -4
  12. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhookCreateModal.js +30 -19
  13. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhookDeleteModal.js +52 -18
  14. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhookEditModal.js +37 -26
  15. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhookTestModal.js +57 -32
  16. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/WebhooksTable.js +24 -11
  17. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/__tests__/__snapshots__/WebhooksTable.test.js.snap +64 -0
  18. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/index.js +21 -22
  19. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/WebhooksIndexPage.js +9 -15
  20. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/__tests__/integration.test.js +0 -3
  21. metadata +7 -5
  22. data/webpack/ForemanWebhooks/Routes/Webhooks/Components/WebhookForm/Components/ForemanFormikField.js +0 -152
  23. data/webpack/ForemanWebhooks/Routes/Webhooks/Components/WebhookForm/__tests__/__snapshots__/WebhookForm.test.js.snap +0 -512
  24. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/__tests__/__snapshots__/integration.test.js.snap +0 -31
@@ -4,13 +4,12 @@ import { Tabs, Tab, TabTitleText } from '@patternfly/react-core';
4
4
 
5
5
  import { sprintf, translate as __ } from 'foremanReact/common/I18n';
6
6
 
7
- import ForemanFormikField from './ForemanFormikField';
8
-
9
7
  import './WebhookFormTabs.css';
8
+ import FieldConstructor from './FieldConstructor';
10
9
 
11
10
  const WebhookFormTabs = ({
12
- formProps,
13
- disabled,
11
+ inputValues,
12
+ setInputValues,
14
13
  activeTab,
15
14
  handleTabClick,
16
15
  webhookTemplates,
@@ -19,160 +18,198 @@ const WebhookFormTabs = ({
19
18
  isTemplatesLoading,
20
19
  isEventsLoading,
21
20
  isPasswordDisabled,
22
- setIsPasswordDisabled,
23
- }) => (
24
- <Tabs
25
- ouiaId="webhook-form-tabs"
26
- activeKey={activeTab}
27
- onSelect={handleTabClick}
28
- isFilled
29
- >
30
- <Tab
31
- ouiaId="webhook-form-tab-general"
32
- className="webhook-form-tab"
33
- eventKey={0}
34
- title={<TabTitleText>{__('General')}</TabTitleText>}
21
+ urlValidated,
22
+ }) => {
23
+ const updateFieldValue = (key, value) => {
24
+ setInputValues(prev => ({ ...prev, [key]: value }));
25
+ };
26
+
27
+ const URL_ERR_STRING = __('Enter valid URL');
28
+
29
+ return (
30
+ <Tabs
31
+ activeKey={activeTab}
32
+ onSelect={handleTabClick}
33
+ isFilled
34
+ ouiaId="webhook-form-tabs"
35
35
  >
36
- <div className="webhook-form-tab-content">
37
- <ForemanFormikField
38
- name="event"
39
- type="select"
40
- label={__('Subscribe to')}
41
- required
42
- allowClear={false}
43
- options={availableEvents}
44
- isLoading={isEventsLoading}
45
- />
46
- <ForemanFormikField
47
- name="name"
48
- type="text"
49
- required
50
- label={__('Name')}
51
- />
52
- <ForemanFormikField
53
- name="target_url"
54
- type="text"
55
- required
56
- label={__('Target URL')}
57
- labelHelp={
58
- <>
59
- <div>
60
- {__(
61
- 'Target URL that should be called by Foreman (ERB allowed).'
62
- )}
63
- </div>
36
+ <Tab
37
+ ouiaId="webhook-form-tab-general"
38
+ className="webhook-form-tab"
39
+ eventKey={0}
40
+ title={<TabTitleText>{__('General')}</TabTitleText>}
41
+ >
42
+ <div className="webhook-form-tab-content">
43
+ <FieldConstructor
44
+ name="event"
45
+ value={inputValues.event}
46
+ setValue={updateFieldValue}
47
+ type="select"
48
+ label={__('Subscribe to')}
49
+ required
50
+ allowClear={false}
51
+ options={availableEvents}
52
+ isLoading={isEventsLoading}
53
+ placeholder={__('Start typing to search...')}
54
+ />
55
+ <FieldConstructor
56
+ name="name"
57
+ value={inputValues.name}
58
+ setValue={updateFieldValue}
59
+ type="text"
60
+ required
61
+ label={__('Name')}
62
+ />
63
+ <FieldConstructor
64
+ name="target_url"
65
+ value={inputValues.target_url}
66
+ setValue={updateFieldValue}
67
+ validated={urlValidated()}
68
+ errMsg={urlValidated() === 'error' ? URL_ERR_STRING : null}
69
+ type="text"
70
+ required
71
+ label={__('Target URL')}
72
+ labelHelp={
64
73
  <div>
65
- {sprintf(
66
- __('Example: %s'),
67
- 'https://host.example.com/inventory/<%= @object.id %>'
68
- )}
74
+ <div>
75
+ {__(
76
+ 'Target URL that should be called by Foreman (ERB allowed).'
77
+ )}
78
+ </div>
79
+ <div>
80
+ {sprintf(
81
+ __('Example: %s'),
82
+ 'https://host.example.com/inventory/<%= @object.id %>'
83
+ )}
84
+ </div>
69
85
  </div>
70
- </>
71
- }
72
- />
73
- <ForemanFormikField
74
- name="webhook_template_id"
75
- type="select"
76
- label={__('Template')}
77
- required
78
- allowClear={false}
79
- options={webhookTemplates}
80
- isLoading={isTemplatesLoading}
81
- />
82
- <ForemanFormikField
83
- name="http_method"
84
- type="select"
85
- label={__('HTTP Method')}
86
- required
87
- allowClear={false}
88
- options={httpMethods}
89
- />
90
- <ForemanFormikField
91
- name="enabled"
92
- type="checkbox"
93
- label={__('Enabled')}
94
- labelHelp={__('If unchecked, the webhook will be inactive')}
95
- />
96
- </div>
97
- </Tab>
98
- <Tab
99
- ouiaId="webhook-form-tab-creds"
100
- className="webhook-form-tab"
101
- eventKey={1}
102
- title={<TabTitleText>{__('Credentials')}</TabTitleText>}
103
- >
104
- <div className="webhook-form-tab-content">
105
- <ForemanFormikField
106
- name="user"
107
- type="text"
108
- label={__('User')}
109
- labelHelp={__('Authentication credentials')}
110
- />
111
- <ForemanFormikField
112
- name="password"
113
- type="password"
114
- label={__('Password')}
115
- labelHelp={__('Authentication credentials')}
116
- disabled={isPasswordDisabled}
117
- setDisabled={setIsPasswordDisabled}
118
- />
119
- <ForemanFormikField
120
- name="verify_ssl"
121
- type="checkbox"
122
- label={__('Verify SSL')}
123
- labelHelp={__(
124
- "Uncheck this option to disable validation of the receiver's SSL certificate"
125
- )}
126
- />
127
- <ForemanFormikField
128
- name="proxy_authorization"
129
- type="checkbox"
130
- label={__('Proxy Authorization')}
131
- labelHelp={__(
132
- 'Authorize with Foreman client certificate and validate smart-proxy CA from Settings'
133
- )}
134
- />
135
- <ForemanFormikField
136
- name="ssl_ca_certs"
137
- type="textarea"
138
- label={__('X509 Certification Authorities')}
139
- placeholder={__(
140
- "Optional CAs in PEM format concatenated to verify the receiver's SSL certificate"
141
- )}
142
- inputSizeClass="col-md-8"
143
- rows={8}
144
- />
145
- </div>
146
- </Tab>
147
- <Tab
148
- ouiaId="webhook-form-tab-add"
149
- className="webhook-form-tab"
150
- eventKey={2}
151
- title={<TabTitleText>{__('Additional')}</TabTitleText>}
152
- >
153
- <div className="webhook-form-tab-content">
154
- <ForemanFormikField
155
- name="http_content_type"
156
- type="text"
157
- label={__('HTTP Content Type')}
158
- />
159
- <ForemanFormikField
160
- name="http_headers"
161
- type="textarea"
162
- label={__('HTTP Headers')}
163
- labelHelp={__('Optional. Must be a JSON object (ERB allowed)')}
164
- placeholder='{&#13;&#10;"X-Shellhook-Arg-1": "value",&#13;&#10;"X-Shellhook-Arg-2": "<%= @object.id %>"&#13;&#10;}'
165
- inputSizeClass="col-md-8"
166
- rows={8}
167
- />
168
- </div>
169
- </Tab>
170
- </Tabs>
171
- );
86
+ }
87
+ />
88
+ <FieldConstructor
89
+ name="webhook_template_id"
90
+ value={inputValues.webhook_template_id}
91
+ setValue={updateFieldValue}
92
+ type="select"
93
+ label={__('Template')}
94
+ required
95
+ allowClear={false}
96
+ options={webhookTemplates}
97
+ isLoading={isTemplatesLoading}
98
+ placeholder={__('Start typing to search...')}
99
+ />
100
+ <FieldConstructor
101
+ name="http_method"
102
+ value={inputValues.http_method}
103
+ setValue={updateFieldValue}
104
+ type="select"
105
+ label={__('HTTP Method')}
106
+ required
107
+ allowClear={false}
108
+ options={httpMethods}
109
+ placeholder={__('Start typing to search...')}
110
+ />
111
+ <FieldConstructor
112
+ name="enabled"
113
+ value={inputValues.enabled}
114
+ setValue={updateFieldValue}
115
+ type="checkbox"
116
+ label={__('Enabled')}
117
+ labelHelp={__('If unchecked, the webhook will be inactive')}
118
+ />
119
+ </div>
120
+ </Tab>
121
+ <Tab
122
+ ouiaId="webhook-form-tab-creds"
123
+ className="webhook-form-tab"
124
+ eventKey={1}
125
+ title={<TabTitleText>{__('Credentials')}</TabTitleText>}
126
+ >
127
+ <div className="webhook-form-tab-content">
128
+ <FieldConstructor
129
+ name="user"
130
+ value={inputValues.user}
131
+ type="text"
132
+ label={__('User')}
133
+ labelHelp={__('Authentication credentials')}
134
+ setValue={updateFieldValue}
135
+ />
136
+ <FieldConstructor
137
+ name="password"
138
+ type="password"
139
+ value={inputValues.password}
140
+ label={__('Password')}
141
+ labelHelp={__('Authentication credentials')}
142
+ disabled={isPasswordDisabled}
143
+ setValue={updateFieldValue}
144
+ />
145
+ <FieldConstructor
146
+ name="verify_ssl"
147
+ value={inputValues.verify_ssl}
148
+ type="checkbox"
149
+ label={__('Verify SSL')}
150
+ labelHelp={__(
151
+ "Uncheck this option to disable validation of the receiver's SSL certificate"
152
+ )}
153
+ setValue={updateFieldValue}
154
+ />
155
+ <FieldConstructor
156
+ name="proxy_authorization"
157
+ type="checkbox"
158
+ value={inputValues.proxy_authorization}
159
+ label={__('Proxy Authorization')}
160
+ labelHelp={__(
161
+ 'Authorize with Foreman client certificate and validate smart-proxy CA from Settings'
162
+ )}
163
+ setValue={updateFieldValue}
164
+ />
165
+ <FieldConstructor
166
+ name="ssl_ca_certs"
167
+ value={inputValues.ssl_ca_certs}
168
+ type="textarea"
169
+ label={__('X509 Certificate Authorities')}
170
+ placeholder={__(
171
+ "Optional CAs in PEM format concatenated to verify the receiver's SSL certificate"
172
+ )}
173
+ inputSizeClass="col-md-8"
174
+ rows={8}
175
+ setValue={updateFieldValue}
176
+ />
177
+ </div>
178
+ </Tab>
179
+ <Tab
180
+ ouiaId="webhook-form-tab-add"
181
+ className="webhook-form-tab"
182
+ eventKey={2}
183
+ title={<TabTitleText>{__('Additional')}</TabTitleText>}
184
+ >
185
+ <div className="webhook-form-tab-content">
186
+ <FieldConstructor
187
+ value={inputValues.http_content_type}
188
+ name="http_content_type"
189
+ type="text"
190
+ label={__('HTTP Content Type')}
191
+ setValue={updateFieldValue}
192
+ />
193
+ <FieldConstructor
194
+ name="http_headers"
195
+ type="textarea"
196
+ value={inputValues.http_headers}
197
+ label={__('HTTP Headers')}
198
+ labelHelp={__('Optional. Must be a JSON object (ERB allowed)')}
199
+ placeholder='{&#13;&#10;"X-Shellhook-Arg-1": "value",&#13;&#10;"X-Shellhook-Arg-2": "<%= @object.id %>"&#13;&#10;}'
200
+ inputSizeClass="col-md-8"
201
+ rows={8}
202
+ setValue={updateFieldValue}
203
+ />
204
+ </div>
205
+ </Tab>
206
+ </Tabs>
207
+ );
208
+ };
172
209
 
173
210
  WebhookFormTabs.propTypes = {
174
- formProps: PropTypes.object,
175
- disabled: PropTypes.bool,
211
+ inputValues: PropTypes.object.isRequired,
212
+ setInputValues: PropTypes.func.isRequired,
176
213
  activeTab: PropTypes.number.isRequired,
177
214
  handleTabClick: PropTypes.func.isRequired,
178
215
  webhookTemplates: PropTypes.array.isRequired,
@@ -181,14 +218,11 @@ WebhookFormTabs.propTypes = {
181
218
  isTemplatesLoading: PropTypes.bool.isRequired,
182
219
  isEventsLoading: PropTypes.bool.isRequired,
183
220
  isPasswordDisabled: PropTypes.bool,
184
- setIsPasswordDisabled: PropTypes.func,
221
+ urlValidated: PropTypes.func.isRequired,
185
222
  };
186
223
 
187
224
  WebhookFormTabs.defaultProps = {
188
- disabled: false,
189
- formProps: {},
190
225
  isPasswordDisabled: false,
191
- setIsPasswordDisabled: undefined,
192
226
  };
193
227
 
194
228
  export default WebhookFormTabs;
@@ -0,0 +1,216 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import '@testing-library/jest-dom';
5
+ import FieldConstructor from '../FieldConstructor';
6
+
7
+ const defaultProps = {
8
+ name: 'test-field',
9
+ setValue: jest.fn(),
10
+ label: 'Test Field',
11
+ value: '',
12
+ type: 'text',
13
+ };
14
+
15
+ describe('FieldConstructor RTL Tests', () => {
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+ });
19
+
20
+ describe('Text Input Fields', () => {
21
+ test('renders text input field', () => {
22
+ render(<FieldConstructor {...defaultProps} type="text" />);
23
+
24
+ expect(screen.getByText('Test Field')).toBeInTheDocument();
25
+ expect(document.getElementById('id-test-field')).toHaveAttribute(
26
+ 'type',
27
+ 'text'
28
+ );
29
+ });
30
+
31
+ test('renders password input field', () => {
32
+ render(<FieldConstructor {...defaultProps} type="password" />);
33
+
34
+ expect(screen.getByText('Test Field')).toBeInTheDocument();
35
+ expect(document.getElementById('id-test-field')).toHaveAttribute(
36
+ 'type',
37
+ 'password'
38
+ );
39
+ });
40
+
41
+ test('calls setValue when text input changes', () => {
42
+ render(<FieldConstructor {...defaultProps} type="text" />);
43
+
44
+ const input = document.getElementById('id-test-field');
45
+ fireEvent.change(input, { target: { value: 'test value' } });
46
+
47
+ expect(defaultProps.setValue).toHaveBeenCalledWith(
48
+ 'test-field',
49
+ 'test value'
50
+ );
51
+ });
52
+
53
+ test('renders with initial value', () => {
54
+ render(
55
+ <FieldConstructor {...defaultProps} type="text" value="initial value" />
56
+ );
57
+
58
+ expect(document.getElementById('id-test-field')).toHaveValue(
59
+ 'initial value'
60
+ );
61
+ });
62
+
63
+ test('renders as disabled when loading', () => {
64
+ render(<FieldConstructor {...defaultProps} type="text" isLoading />);
65
+
66
+ expect(document.getElementById('id-test-field')).toBeDisabled();
67
+ });
68
+ });
69
+
70
+ describe('Textarea Fields', () => {
71
+ test('renders textarea field', () => {
72
+ render(<FieldConstructor {...defaultProps} type="textarea" />);
73
+
74
+ expect(screen.getByText('Test Field')).toBeInTheDocument();
75
+ expect(document.getElementById('id-test-field').tagName).toBe('TEXTAREA');
76
+ });
77
+
78
+ test('calls setValue when textarea changes', () => {
79
+ render(<FieldConstructor {...defaultProps} type="textarea" />);
80
+
81
+ const textarea = document.getElementById('id-test-field');
82
+ fireEvent.change(textarea, { target: { value: 'textarea content' } });
83
+
84
+ expect(defaultProps.setValue).toHaveBeenCalledWith(
85
+ 'test-field',
86
+ 'textarea content'
87
+ );
88
+ });
89
+
90
+ test('renders with specified rows', () => {
91
+ render(<FieldConstructor {...defaultProps} type="textarea" rows={5} />);
92
+
93
+ expect(document.getElementsByTagName('textarea')).toHaveLength(1);
94
+ });
95
+ });
96
+
97
+ describe('Checkbox Fields', () => {
98
+ test('renders checkbox field', () => {
99
+ render(<FieldConstructor {...defaultProps} type="checkbox" />);
100
+
101
+ expect(screen.getByText('Test Field')).toBeInTheDocument();
102
+ expect(document.getElementById('id-test-field')).toHaveAttribute(
103
+ 'type',
104
+ 'checkbox'
105
+ );
106
+ });
107
+
108
+ test('renders unchecked checkbox', () => {
109
+ render(
110
+ <FieldConstructor {...defaultProps} type="checkbox" value={false} />
111
+ );
112
+
113
+ expect(document.getElementById('id-test-field')).not.toBeChecked();
114
+ });
115
+ });
116
+
117
+ describe('Autocomplete render test', () => {
118
+ test('renders autocomplete', () => {
119
+ const props = {
120
+ ...defaultProps,
121
+ onChange: defaultProps.setValue,
122
+ options: [
123
+ { value: 'host_created.event.foreman', label: 'Host Created' },
124
+ { value: 'host_updated.event.foreman', label: 'Host Updated' },
125
+ ],
126
+ };
127
+ render(<FieldConstructor {...props} type="select" />);
128
+
129
+ expect(screen.getByRole('combobox')).toBeInTheDocument();
130
+ });
131
+ });
132
+
133
+ describe('Label Help', () => {
134
+ test('renders help icon when labelHelp is provided', () => {
135
+ render(
136
+ <FieldConstructor {...defaultProps} labelHelp="This is help text" />
137
+ );
138
+
139
+ expect(screen.getByRole('button')).toBeInTheDocument();
140
+ });
141
+
142
+ test('does not render help icon when labelHelp is not provided', () => {
143
+ render(<FieldConstructor {...defaultProps} />);
144
+
145
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
146
+ });
147
+
148
+ test('renders help icon with JSX content', async () => {
149
+ const helpContent = <div>JSX help content</div>;
150
+ render(<FieldConstructor {...defaultProps} labelHelp={helpContent} />);
151
+
152
+ const button = screen.getByRole('button');
153
+ expect(button).toBeInTheDocument();
154
+
155
+ userEvent.click(button);
156
+
157
+ expect(await screen.findByText('JSX help content')).toBeInTheDocument();
158
+ });
159
+ });
160
+
161
+ describe('Validation', () => {
162
+ test('renders error message when validated is error', async () => {
163
+ render(
164
+ <>
165
+ <FieldConstructor {...defaultProps} required type="text" />
166
+ <button>Dummy</button>
167
+ </>
168
+ );
169
+
170
+ const input = document.getElementById('id-test-field');
171
+ await userEvent.click(input);
172
+ expect(input).toHaveFocus();
173
+
174
+ await userEvent.tab();
175
+ expect(input).not.toHaveFocus();
176
+
177
+ expect(screen.getByText('Field is required')).toBeInTheDocument();
178
+ });
179
+
180
+ test('does not render error message when validated is not error', () => {
181
+ render(
182
+ <FieldConstructor {...defaultProps} type="text" validated="success" />
183
+ );
184
+
185
+ expect(screen.queryByText('Field is required')).not.toBeInTheDocument();
186
+ });
187
+ });
188
+
189
+ describe('Edge Cases', () => {
190
+ test('handles undefined value', () => {
191
+ render(
192
+ <FieldConstructor {...defaultProps} type="text" value={undefined} />
193
+ );
194
+
195
+ expect(document.getElementById('id-test-field')).toHaveValue('');
196
+ });
197
+
198
+ test('handles null value', () => {
199
+ render(<FieldConstructor {...defaultProps} type="text" value={null} />);
200
+
201
+ expect(document.getElementById('id-test-field')).toHaveValue('');
202
+ });
203
+
204
+ test('handles numeric value', () => {
205
+ render(<FieldConstructor {...defaultProps} type="text" value={123} />);
206
+
207
+ expect(document.getElementById('id-test-field')).toHaveValue('123');
208
+ });
209
+
210
+ test('handles boolean value for checkbox', () => {
211
+ render(<FieldConstructor {...defaultProps} type="checkbox" value />);
212
+
213
+ expect(document.getElementById('id-test-field')).toBeChecked();
214
+ });
215
+ });
216
+ });