foreman_openbolt 1.1.0 → 1.2.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -252
  3. data/Rakefile +0 -0
  4. data/app/controllers/foreman_openbolt/task_controller.rb +4 -1
  5. data/lib/foreman_openbolt/engine.rb +128 -24
  6. data/lib/foreman_openbolt/version.rb +1 -1
  7. data/lib/proxy_api/openbolt.rb +13 -3
  8. data/package.json +1 -1
  9. data/test/acceptance/acceptance_helper.rb +146 -0
  10. data/test/acceptance/docker/docker-compose.yml +69 -0
  11. data/test/acceptance/docker/foreman/Dockerfile +45 -0
  12. data/test/acceptance/docker/foreman/entrypoint.sh +26 -0
  13. data/test/acceptance/docker/target/Dockerfile +29 -0
  14. data/test/acceptance/docker/target/entrypoint.sh +11 -0
  15. data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.json +30 -0
  16. data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.sh +16 -0
  17. data/test/acceptance/fixtures/modules/acceptance/tasks/echo.json +13 -0
  18. data/test/acceptance/fixtures/modules/acceptance/tasks/echo.sh +3 -0
  19. data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.json +8 -0
  20. data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.sh +3 -0
  21. data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.json +8 -0
  22. data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.sh +2 -0
  23. data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.json +14 -0
  24. data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.sh +3 -0
  25. data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.json +13 -0
  26. data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.sh +9 -0
  27. data/test/acceptance/fixtures/openbolt.yml +7 -0
  28. data/test/acceptance/tests/error_handling_test.rb +40 -0
  29. data/test/acceptance/tests/host_selector_test.rb +31 -0
  30. data/test/acceptance/tests/launch_task_test.rb +96 -0
  31. data/test/acceptance/tests/parameter_table_test.rb +61 -0
  32. data/test/acceptance/tests/settings_test.rb +95 -0
  33. data/test/acceptance/tests/ssh_options_test.rb +77 -0
  34. data/test/acceptance/tests/task_execution_test.rb +40 -0
  35. data/test/acceptance/tests/task_history_test.rb +84 -0
  36. data/test/acceptance/tests/transport_options_test.rb +161 -0
  37. data/test/test_plugin_helper.rb +17 -0
  38. data/test/unit/controllers/task_controller_test.rb +426 -0
  39. data/test/unit/docker/Dockerfile +47 -0
  40. data/test/unit/docker/docker-compose.yml +33 -0
  41. data/test/unit/docker/entrypoint.sh +4 -0
  42. data/test/unit/factories/foreman_openbolt_factories.rb +39 -0
  43. data/test/unit/lib/actions/cleanup_proxy_artifacts_test.rb +51 -0
  44. data/test/unit/lib/actions/poll_task_status_test.rb +141 -0
  45. data/test/unit/lib/proxy_api/openbolt_test.rb +174 -0
  46. data/test/unit/models/task_job_test.rb +278 -0
  47. data/webpack/src/Components/LaunchTask/__tests__/ParameterField.test.js +45 -0
  48. data/webpack/src/Components/LaunchTask/hooks/__tests__/useOpenBoltOptions.test.js +1 -0
  49. data/webpack/src/Components/TaskExecution/__tests__/LoadingIndicator.test.js +1 -1
  50. data/webpack/src/Components/TaskExecution/hooks/__tests__/useJobPolling.test.js +1 -1
  51. metadata +39 -1
@@ -0,0 +1,426 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_plugin_helper'
4
+
5
+ class TaskControllerTest < ActionController::TestCase
6
+ tests ForemanOpenbolt::TaskController
7
+
8
+ setup do
9
+ @routes = ForemanOpenbolt::Engine.routes
10
+ @proxy = FactoryBot.create(:smart_proxy)
11
+ @session = set_session_user
12
+ WebMock.reset!
13
+ end
14
+
15
+ teardown do
16
+ WebMock.reset!
17
+ end
18
+
19
+ context 'fetch_tasks' do
20
+ test 'returns tasks from proxy as JSON' do
21
+ tasks = { 'mymod::install' => { 'description' => 'Install a package' } }
22
+ stub_request(:get, "#{@proxy.url}/openbolt/tasks")
23
+ .to_return(status: 200, body: tasks.to_json, headers: { 'Content-Type' => 'application/json' })
24
+
25
+ get :fetch_tasks, params: { proxy_id: @proxy.id }, session: @session
26
+ assert_response :success
27
+ assert_equal tasks, JSON.parse(response.body)
28
+ end
29
+
30
+ test 'returns error when proxy_id is missing' do
31
+ get :fetch_tasks, session: @session
32
+ assert_response :bad_request
33
+ body = JSON.parse(response.body)
34
+ assert_match(/Smart Proxy ID is required/, body['error'])
35
+ end
36
+
37
+ test 'returns error when proxy not found' do
38
+ get :fetch_tasks, params: { proxy_id: -1 }, session: @session
39
+ assert_response :not_found
40
+ end
41
+
42
+ test 'returns internal_server_error when proxy is unreachable' do
43
+ stub_request(:get, "#{@proxy.url}/openbolt/tasks").to_timeout
44
+
45
+ get :fetch_tasks, params: { proxy_id: @proxy.id }, session: @session
46
+ assert_response :internal_server_error
47
+ end
48
+ end
49
+
50
+ context 'reload_tasks' do
51
+ test 'returns reloaded tasks from proxy' do
52
+ tasks = { 'new::task' => {} }
53
+ stub_request(:get, "#{@proxy.url}/openbolt/tasks/reload")
54
+ .to_return(status: 200, body: tasks.to_json, headers: { 'Content-Type' => 'application/json' })
55
+
56
+ get :reload_tasks, params: { proxy_id: @proxy.id }, session: @session
57
+ assert_response :success
58
+ assert_equal tasks, JSON.parse(response.body)
59
+ end
60
+ end
61
+
62
+ context 'launch_task' do
63
+ setup do
64
+ @tasks = { 'mymod::install' => { 'description' => 'Install a package' } }
65
+ stub_request(:get, "#{@proxy.url}/openbolt/tasks")
66
+ .to_return(status: 200, body: @tasks.to_json, headers: { 'Content-Type' => 'application/json' })
67
+ stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
68
+ .to_return(status: 200, body: { 'id' => 'launched-job-1' }.to_json,
69
+ headers: { 'Content-Type' => 'application/json' })
70
+ ForemanTasks.stubs(:async_task)
71
+ end
72
+
73
+ test 'launches task and returns job_id' do
74
+ post :launch_task, params: {
75
+ proxy_id: @proxy.id,
76
+ task_name: 'mymod::install',
77
+ targets: 'host1.example.com',
78
+ params: { 'name' => 'nginx' },
79
+ options: { 'transport' => 'ssh' },
80
+ }, session: @session
81
+
82
+ assert_response :success
83
+ body = JSON.parse(response.body)
84
+ assert_equal 'launched-job-1', body['job_id']
85
+ end
86
+
87
+ test 'schedules polling after creating task' do
88
+ ForemanTasks.expects(:async_task).with(
89
+ Actions::ForemanOpenbolt::PollTaskStatus,
90
+ 'launched-job-1',
91
+ @proxy.id
92
+ )
93
+
94
+ post :launch_task, params: {
95
+ proxy_id: @proxy.id,
96
+ task_name: 'mymod::install',
97
+ targets: 'host1.example.com',
98
+ }, session: @session
99
+
100
+ assert_response :success
101
+ end
102
+
103
+ test 'creates a TaskJob record' do
104
+ assert_difference('ForemanOpenbolt::TaskJob.count', 1) do
105
+ post :launch_task, params: {
106
+ proxy_id: @proxy.id,
107
+ task_name: 'mymod::install',
108
+ targets: 'host1.example.com,host2.example.com',
109
+ }, session: @session
110
+ end
111
+
112
+ job = ForemanOpenbolt::TaskJob.last
113
+ assert_equal 'mymod::install', job.task_name
114
+ assert_equal %w[host1.example.com host2.example.com], job.targets
115
+ assert_equal 'pending', job.status
116
+ end
117
+
118
+ test 'returns error when task_name is missing' do
119
+ post :launch_task, params: { proxy_id: @proxy.id, targets: 'host1' }, session: @session
120
+ assert_response :bad_request
121
+ assert_match(/task_name/, JSON.parse(response.body)['error'])
122
+ end
123
+
124
+ test 'returns error when targets is missing' do
125
+ post :launch_task, params: { proxy_id: @proxy.id, task_name: 'test::task' }, session: @session
126
+ assert_response :bad_request
127
+ assert_match(/targets/, JSON.parse(response.body)['error'])
128
+ end
129
+
130
+ test 'returns error when proxy returns error in response' do
131
+ stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
132
+ .to_return(status: 200, body: { 'error' => 'Task not found' }.to_json,
133
+ headers: { 'Content-Type' => 'application/json' })
134
+
135
+ post :launch_task, params: {
136
+ proxy_id: @proxy.id,
137
+ task_name: 'missing::task',
138
+ targets: 'host1',
139
+ }, session: @session
140
+ assert_response :bad_request
141
+ assert_match(/Task execution failed/, JSON.parse(response.body)['error'])
142
+ end
143
+
144
+ test 'returns bad_gateway when proxy returns invalid JSON' do
145
+ stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
146
+ .to_return(status: 200, body: 'not valid json',
147
+ headers: { 'Content-Type' => 'application/json' })
148
+
149
+ post :launch_task, params: {
150
+ proxy_id: @proxy.id,
151
+ task_name: 'mymod::install',
152
+ targets: 'host1',
153
+ }, session: @session
154
+ assert_response :bad_gateway
155
+ assert_match(/Smart Proxy error/, JSON.parse(response.body)['error'])
156
+ end
157
+
158
+ test 'returns error when proxy returns no job ID' do
159
+ stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
160
+ .to_return(status: 200, body: { 'status' => 'ok' }.to_json,
161
+ headers: { 'Content-Type' => 'application/json' })
162
+
163
+ post :launch_task, params: {
164
+ proxy_id: @proxy.id,
165
+ task_name: 'test::task',
166
+ targets: 'host1',
167
+ }, session: @session
168
+ assert_response :bad_request
169
+ assert_match(/No job ID returned/, JSON.parse(response.body)['error'])
170
+ end
171
+ end
172
+
173
+ context 'job_status' do
174
+ test 'returns serialized task job' do
175
+ job = FactoryBot.create(:task_job, :running, smart_proxy: @proxy)
176
+
177
+ get :job_status, params: { job_id: job.job_id }, session: @session
178
+ assert_response :success
179
+
180
+ body = JSON.parse(response.body)
181
+ assert_equal 'running', body['status']
182
+ assert_equal job.task_name, body['task_name']
183
+ assert_equal job.targets, body['targets']
184
+ assert_equal @proxy.id, body['smart_proxy']['id']
185
+ assert_equal @proxy.name, body['smart_proxy']['name']
186
+ end
187
+
188
+ test 'returns error when job_id is missing' do
189
+ get :job_status, session: @session
190
+ assert_response :bad_request
191
+ end
192
+
193
+ test 'returns not_found when job does not exist' do
194
+ get :job_status, params: { job_id: 'nonexistent' }, session: @session
195
+ assert_response :not_found
196
+ end
197
+ end
198
+
199
+ context 'job_result' do
200
+ test 'returns result and log for completed job' do
201
+ ForemanTasks.stubs(:async_task)
202
+ job = FactoryBot.create(:task_job, :success, smart_proxy: @proxy)
203
+
204
+ get :job_result, params: { job_id: job.job_id }, session: @session
205
+ assert_response :success
206
+
207
+ body = JSON.parse(response.body)
208
+ assert_equal 'success', body['status']
209
+ assert_equal job.result, body['value']
210
+ assert_equal job.log, body['log']
211
+ assert_equal job.command, body['command']
212
+ end
213
+
214
+ test 'returns not_found when job does not exist' do
215
+ get :job_result, params: { job_id: 'nonexistent' }, session: @session
216
+ assert_response :not_found
217
+ end
218
+ end
219
+
220
+ context 'launch_task with encrypted options' do
221
+ setup do
222
+ @tasks = { 'mymod::install' => { 'description' => 'Install a package' } }
223
+ stub_request(:get, "#{@proxy.url}/openbolt/tasks")
224
+ .to_return(status: 200, body: @tasks.to_json, headers: { 'Content-Type' => 'application/json' })
225
+ stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
226
+ .to_return(status: 200, body: { 'id' => 'encrypted-job-1' }.to_json,
227
+ headers: { 'Content-Type' => 'application/json' })
228
+ ForemanTasks.stubs(:async_task)
229
+ end
230
+
231
+ test 'sends real encrypted value to proxy and scrubs it in database' do
232
+ Setting['openbolt_password'] = 'real-secret-password'
233
+
234
+ post :launch_task, params: {
235
+ proxy_id: @proxy.id,
236
+ task_name: 'mymod::install',
237
+ targets: 'host1.example.com',
238
+ options: { 'password' => '[Use saved encrypted default]', 'transport' => 'ssh' },
239
+ }, session: @session
240
+
241
+ assert_response :success
242
+
243
+ assert_requested(:post, "#{@proxy.url}/openbolt/launch/task") do |req|
244
+ sent_body = JSON.parse(req.body)
245
+ sent_body['options']['password'] == 'real-secret-password'
246
+ end
247
+
248
+ job = ForemanOpenbolt::TaskJob.find_by(job_id: 'encrypted-job-1')
249
+ assert_equal '*****', job.openbolt_options['password']
250
+ assert_equal 'ssh', job.openbolt_options['transport']
251
+ end
252
+
253
+ test 'returns error when encrypted placeholder used for nonexistent setting' do
254
+ post :launch_task, params: {
255
+ proxy_id: @proxy.id,
256
+ task_name: 'mymod::install',
257
+ targets: 'host1.example.com',
258
+ options: { 'nonexistent-option' => '[Use saved encrypted default]' },
259
+ }, session: @session
260
+
261
+ assert_response :bad_request
262
+ assert_match(/No saved value for encrypted option/, JSON.parse(response.body)['error'])
263
+ end
264
+
265
+ test 'passes non-encrypted options through unchanged' do
266
+ post :launch_task, params: {
267
+ proxy_id: @proxy.id,
268
+ task_name: 'mymod::install',
269
+ targets: 'host1.example.com',
270
+ options: { 'transport' => 'ssh', 'user' => 'admin' },
271
+ }, session: @session
272
+
273
+ assert_response :success
274
+ job = ForemanOpenbolt::TaskJob.find_by(job_id: 'encrypted-job-1')
275
+ assert_equal 'ssh', job.openbolt_options['transport']
276
+ assert_equal 'admin', job.openbolt_options['user']
277
+ end
278
+ end
279
+
280
+ context 'fetch_openbolt_options with settings defaults' do
281
+ test 'merges Foreman setting defaults into proxy options' do
282
+ Setting['openbolt_transport'] = 'winrm'
283
+ Setting['openbolt_user'] = 'admin'
284
+
285
+ proxy_options = {
286
+ 'transport' => { 'type' => %w[ssh winrm] },
287
+ 'user' => { 'type' => 'string' },
288
+ 'verbose' => { 'type' => 'boolean' },
289
+ }
290
+ stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
291
+ .to_return(status: 200, body: proxy_options.to_json, headers: { 'Content-Type' => 'application/json' })
292
+
293
+ get :fetch_openbolt_options, params: { proxy_id: @proxy.id }, session: @session
294
+ assert_response :success
295
+
296
+ body = JSON.parse(response.body)
297
+ assert_equal 'winrm', body['transport']['default']
298
+ assert_equal 'admin', body['user']['default']
299
+ assert_equal false, body['verbose']['default']
300
+ end
301
+
302
+ test 'shows encrypted placeholder instead of real value for encrypted settings' do
303
+ Setting['openbolt_password'] = 'secret-value'
304
+
305
+ proxy_options = {
306
+ 'password' => { 'type' => 'string', 'sensitive' => true },
307
+ }
308
+ stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
309
+ .to_return(status: 200, body: proxy_options.to_json, headers: { 'Content-Type' => 'application/json' })
310
+
311
+ get :fetch_openbolt_options, params: { proxy_id: @proxy.id }, session: @session
312
+ assert_response :success
313
+
314
+ body = JSON.parse(response.body)
315
+ assert_equal '[Use saved encrypted default]', body['password']['default']
316
+ end
317
+ end
318
+
319
+ context 'fetch_openbolt_options with Choria settings defaults' do
320
+ # Mirrors the real GET /openbolt/tasks/options response for Choria
321
+ # (see smart_proxy_openbolt/lib/smart_proxy_openbolt/main.rb OPENBOLT_OPTIONS).
322
+ def self.choria_proxy_options
323
+ {
324
+ 'choria-task-agent' => { 'type' => %w[bolt_tasks shell], 'transport' => ['choria'], 'sensitive' => false },
325
+ 'choria-config-file' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
326
+ 'choria-mcollective-certname' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
327
+ 'choria-ssl-ca' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
328
+ 'choria-ssl-cert' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
329
+ 'choria-ssl-key' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
330
+ 'choria-collective' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
331
+ 'choria-puppet-environment' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
332
+ 'choria-rpc-timeout' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
333
+ 'choria-task-timeout' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
334
+ 'choria-command-timeout' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
335
+ 'choria-brokers' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
336
+ 'choria-broker-timeout' => { 'type' => 'string', 'transport' => ['choria'], 'sensitive' => false },
337
+ }
338
+ end
339
+
340
+ test 'merges all Choria setting values into their option defaults' do
341
+ Setting['openbolt_choria-task-agent'] = 'shell'
342
+ Setting['openbolt_choria-config-file'] = '/etc/choria/client.conf'
343
+ Setting['openbolt_choria-mcollective-certname'] = 'primary.example.com'
344
+ Setting['openbolt_choria-ssl-ca'] = '/etc/choria/ca.pem'
345
+ Setting['openbolt_choria-ssl-cert'] = '/etc/choria/client.pem'
346
+ Setting['openbolt_choria-ssl-key'] = '/etc/choria/client.key'
347
+ Setting['openbolt_choria-collective'] = 'mcollective'
348
+ Setting['openbolt_choria-puppet-environment'] = 'production'
349
+ Setting['openbolt_choria-rpc-timeout'] = 60
350
+ Setting['openbolt_choria-task-timeout'] = 300
351
+ Setting['openbolt_choria-command-timeout'] = 120
352
+ Setting['openbolt_choria-brokers'] = 'broker.example.com:4222'
353
+ Setting['openbolt_choria-broker-timeout'] = 10
354
+
355
+ stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
356
+ .to_return(status: 200, body: self.class.choria_proxy_options.to_json,
357
+ headers: { 'Content-Type' => 'application/json' })
358
+
359
+ get :fetch_openbolt_options, params: { proxy_id: @proxy.id }, session: @session
360
+ assert_response :success
361
+
362
+ body = JSON.parse(response.body)
363
+ assert_equal 'shell', body['choria-task-agent']['default']
364
+ assert_equal '/etc/choria/client.conf', body['choria-config-file']['default']
365
+ assert_equal 'primary.example.com', body['choria-mcollective-certname']['default']
366
+ assert_equal '/etc/choria/ca.pem', body['choria-ssl-ca']['default']
367
+ assert_equal '/etc/choria/client.pem', body['choria-ssl-cert']['default']
368
+ assert_equal '/etc/choria/client.key', body['choria-ssl-key']['default']
369
+ assert_equal 'mcollective', body['choria-collective']['default']
370
+ assert_equal 'production', body['choria-puppet-environment']['default']
371
+ assert_equal 60, body['choria-rpc-timeout']['default']
372
+ assert_equal 300, body['choria-task-timeout']['default']
373
+ assert_equal 120, body['choria-command-timeout']['default']
374
+ assert_equal 'broker.example.com:4222', body['choria-brokers']['default']
375
+ assert_equal 10, body['choria-broker-timeout']['default']
376
+ end
377
+
378
+ test 'omits nil-default settings and keeps real defaults when Choria settings are not configured' do
379
+ stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
380
+ .to_return(status: 200, body: self.class.choria_proxy_options.to_json,
381
+ headers: { 'Content-Type' => 'application/json' })
382
+
383
+ get :fetch_openbolt_options, params: { proxy_id: @proxy.id }, session: @session
384
+ assert_response :success
385
+
386
+ body = JSON.parse(response.body)
387
+ assert_equal 'bolt_tasks', body['choria-task-agent']['default']
388
+ assert_not body['choria-config-file'].key?('default')
389
+ assert_not body['choria-mcollective-certname'].key?('default')
390
+ assert_not body['choria-ssl-key'].key?('default')
391
+ assert_not body['choria-brokers'].key?('default')
392
+ assert_not body['choria-broker-timeout'].key?('default')
393
+ end
394
+ end
395
+
396
+ context 'fetch_task_history' do
397
+ test 'returns paginated task history' do
398
+ 3.times { FactoryBot.create(:task_job, smart_proxy: @proxy) }
399
+
400
+ get :fetch_task_history, params: { page: 1, per_page: 2 }, session: @session
401
+ assert_response :success
402
+
403
+ body = JSON.parse(response.body)
404
+ assert_equal 2, body['results'].length
405
+ assert_equal 3, body['total']
406
+ assert_equal 1, body['page']
407
+ assert_equal 2, body['per_page']
408
+ end
409
+
410
+ test 'caps per_page at 100' do
411
+ get :fetch_task_history, params: { per_page: 200 }, session: @session
412
+ assert_response :success
413
+
414
+ body = JSON.parse(response.body)
415
+ assert_equal 100, body['per_page']
416
+ end
417
+
418
+ test 'defaults per_page to 20' do
419
+ get :fetch_task_history, session: @session
420
+ assert_response :success
421
+
422
+ body = JSON.parse(response.body)
423
+ assert_equal 20, body['per_page']
424
+ end
425
+ end
426
+ end
@@ -0,0 +1,47 @@
1
+ # Base test image: Foreman + system deps on Rocky Linux 9.
2
+ # This image is stable for a given Foreman version and does not
3
+ # depend on any foreman_openbolt code. All plugin wiring happens
4
+ # at runtime via the rake test:up task.
5
+ #
6
+ # Build:
7
+ # FOREMAN_BRANCH=3.18-stable docker compose -f test/unit/docker/docker-compose.yml build test
8
+
9
+ FROM rockylinux:9
10
+
11
+ # System packages -- cached across all Foreman versions.
12
+ RUN dnf module enable -y nodejs:22 postgresql:16 && \
13
+ dnf install -y \
14
+ ruby ruby-devel rubygem-bundler \
15
+ gcc gcc-c++ make \
16
+ postgresql-devel postgresql \
17
+ libffi-devel \
18
+ nodejs npm \
19
+ git-core \
20
+ redhat-rpm-config && \
21
+ dnf clean all
22
+
23
+ ARG FOREMAN_BRANCH=3.18-stable
24
+
25
+ # Clone Foreman at the specified branch.
26
+ RUN git init /opt/foreman
27
+
28
+ WORKDIR /opt/foreman
29
+
30
+ RUN git remote add origin https://github.com/theforeman/foreman.git && \
31
+ git fetch --depth=1 origin ${FOREMAN_BRANCH} && \
32
+ git checkout FETCH_HEAD
33
+
34
+ # Configure database for the test container.
35
+ RUN printf "test:\n adapter: postgresql\n host: db\n port: 5432\n username: foreman\n password: foreman\n database: foreman_test\n encoding: utf8\n pool: 5\n" \
36
+ > config/database.yml
37
+
38
+ # Install Foreman's own gems (without the plugin). This is the
39
+ # expensive layer but only changes when the Foreman version changes.
40
+ ENV BUNDLE_WITHOUT="release console journald libvirt ec2 openstack vmware telemetry"
41
+ ENV BUNDLE_PATH="/opt/bundle"
42
+ RUN bundle install --jobs=4
43
+
44
+ COPY test/unit/docker/entrypoint.sh /entrypoint.sh
45
+ RUN chmod +x /entrypoint.sh
46
+
47
+ ENTRYPOINT ["/entrypoint.sh"]
@@ -0,0 +1,33 @@
1
+ name: foreman-openbolt-unit
2
+
3
+ services:
4
+ db:
5
+ image: postgres:16-alpine
6
+ environment:
7
+ POSTGRES_USER: foreman
8
+ POSTGRES_PASSWORD: foreman
9
+ POSTGRES_DB: foreman_test
10
+ tmpfs: /var/lib/postgresql/data
11
+ healthcheck:
12
+ test: ["CMD-SHELL", "pg_isready -U foreman"]
13
+ interval: 2s
14
+ timeout: 5s
15
+ retries: 10
16
+
17
+ test:
18
+ image: foreman-openbolt-test:${FOREMAN_BRANCH:-3.18-stable}
19
+ build:
20
+ context: ../../..
21
+ dockerfile: test/unit/docker/Dockerfile
22
+ args:
23
+ FOREMAN_BRANCH: ${FOREMAN_BRANCH:-3.18-stable}
24
+ depends_on:
25
+ db:
26
+ condition: service_healthy
27
+ environment:
28
+ RAILS_ENV: test
29
+ DATABASE_URL: postgresql://foreman:foreman@db:5432/foreman_test
30
+ volumes:
31
+ - ../../..:/opt/foreman_openbolt
32
+ working_dir: /opt/foreman
33
+ command: ["tail", "-f", "/dev/null"]
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ exec "$@"
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :task_job, class: 'ForemanOpenbolt::TaskJob' do
5
+ sequence(:job_id) { |n| "test-job-#{n}" }
6
+ association :smart_proxy
7
+ task_name { 'mymod::install' }
8
+ task_description { 'Install a package on the target host' }
9
+ status { 'pending' }
10
+ targets { ['host1.example.com', 'host2.example.com'] }
11
+ submitted_at { Time.current }
12
+ task_parameters { { 'name' => 'nginx' } }
13
+ openbolt_options { { 'transport' => 'ssh' } }
14
+
15
+ trait :running do
16
+ status { 'running' }
17
+ end
18
+
19
+ trait :success do
20
+ status { 'success' }
21
+ completed_at { Time.current }
22
+ result { { 'items' => [{ 'target' => 'host1.example.com', 'status' => 'success' }] } }
23
+ log { 'Started: task mymod::install\nFinished: success' }
24
+ command { 'bolt task run mymod::install --targets host1.example.com' }
25
+ end
26
+
27
+ trait :failure do
28
+ status { 'failure' }
29
+ completed_at { Time.current }
30
+ result { { 'items' => [{ 'target' => 'host1.example.com', 'status' => 'failure' }] } }
31
+ log { 'Started: task mymod::install\nFinished: failure' }
32
+ end
33
+
34
+ trait :exception do
35
+ status { 'exception' }
36
+ completed_at { Time.current }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_plugin_helper'
4
+
5
+ class CleanupProxyArtifactsTest < ForemanOpenbolt::PluginTestCase
6
+ include Dynflow::Testing
7
+
8
+ context 'plan' do
9
+ test 'stores proxy_id and job_id in input' do
10
+ action = create_and_plan_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts, 42, 'job-123')
11
+ assert_equal 42, action.input[:proxy_id]
12
+ assert_equal 'job-123', action.input[:job_id]
13
+ end
14
+ end
15
+
16
+ context 'rescue_strategy' do
17
+ test 'uses Skip rescue strategy' do
18
+ action = create_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts)
19
+ assert_equal Dynflow::Action::Rescue::Skip, action.rescue_strategy
20
+ end
21
+ end
22
+
23
+ context 'run' do
24
+ test 'calls delete_job_artifacts when proxy exists' do
25
+ proxy = FactoryBot.create(:smart_proxy)
26
+ stub = stub_request(:delete, "#{proxy.url}/openbolt/job/job-456/artifacts")
27
+ .to_return(status: 200, body: '{"deleted": true}', headers: { 'Content-Type' => 'application/json' })
28
+
29
+ action = create_and_plan_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts, proxy.id, 'job-456')
30
+ run_action(action)
31
+
32
+ assert_requested(stub)
33
+ end
34
+
35
+ test 'does not call API when proxy not found' do
36
+ stub = stub_request(:delete, %r{openbolt/job})
37
+ action = create_and_plan_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts, -1, 'job-456')
38
+ run_action(action)
39
+ assert_not_requested(stub)
40
+ end
41
+
42
+ test 'does not raise when API call fails' do
43
+ proxy = FactoryBot.create(:smart_proxy)
44
+ stub_request(:delete, "#{proxy.url}/openbolt/job/job-456/artifacts")
45
+ .to_return(status: 500, body: 'Internal Server Error')
46
+
47
+ action = create_and_plan_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts, proxy.id, 'job-456')
48
+ assert_nothing_raised { run_action(action) }
49
+ end
50
+ end
51
+ end