foreman_remote_execution 14.1.0 → 14.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/app/models/job_template.rb +1 -0
  3. data/app/views/template_invocations/show.js.erb +1 -1
  4. data/db/migrate/20240312133027_extend_template_invocation_events.rb +9 -0
  5. data/lib/foreman_remote_execution/engine.rb +1 -1
  6. data/lib/foreman_remote_execution/version.rb +1 -1
  7. data/lib/tasks/foreman_remote_execution_tasks.rake +3 -0
  8. data/test/benchmark/run_hosts_job_benchmark.rb +70 -0
  9. data/test/benchmark/targeting_benchmark.rb +31 -0
  10. data/test/factories/foreman_remote_execution_factories.rb +147 -0
  11. data/test/functional/api/v2/foreign_input_sets_controller_test.rb +58 -0
  12. data/test/functional/api/v2/job_invocations_controller_test.rb +446 -0
  13. data/test/functional/api/v2/job_templates_controller_test.rb +110 -0
  14. data/test/functional/api/v2/registration_controller_test.rb +73 -0
  15. data/test/functional/api/v2/remote_execution_features_controller_test.rb +34 -0
  16. data/test/functional/api/v2/template_invocations_controller_test.rb +33 -0
  17. data/test/functional/cockpit_controller_test.rb +16 -0
  18. data/test/functional/job_invocations_controller_test.rb +132 -0
  19. data/test/functional/job_templates_controller_test.rb +31 -0
  20. data/test/functional/ui_job_wizard_controller_test.rb +16 -0
  21. data/test/graphql/mutations/job_invocations/create_test.rb +58 -0
  22. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  23. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  24. data/test/helpers/remote_execution_helper_test.rb +46 -0
  25. data/test/support/remote_execution_helper.rb +5 -0
  26. data/test/test_plugin_helper.rb +9 -0
  27. data/test/unit/actions/run_host_job_test.rb +115 -0
  28. data/test/unit/actions/run_hosts_job_test.rb +214 -0
  29. data/test/unit/api_params_test.rb +25 -0
  30. data/test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb +29 -0
  31. data/test/unit/concerns/host_extensions_test.rb +219 -0
  32. data/test/unit/concerns/nic_extensions_test.rb +9 -0
  33. data/test/unit/execution_task_status_mapper_test.rb +92 -0
  34. data/test/unit/input_template_renderer_test.rb +503 -0
  35. data/test/unit/job_invocation_composer_test.rb +974 -0
  36. data/test/unit/job_invocation_report_template_test.rb +60 -0
  37. data/test/unit/job_invocation_test.rb +232 -0
  38. data/test/unit/job_template_effective_user_test.rb +37 -0
  39. data/test/unit/job_template_test.rb +316 -0
  40. data/test/unit/remote_execution_feature_test.rb +86 -0
  41. data/test/unit/remote_execution_provider_test.rb +298 -0
  42. data/test/unit/renderer_scope_input_test.rb +49 -0
  43. data/test/unit/targeting_test.rb +206 -0
  44. data/test/unit/template_invocation_input_value_test.rb +38 -0
  45. metadata +39 -2
@@ -0,0 +1,60 @@
1
+ require 'test_plugin_helper'
2
+
3
+ class JobReportTemplateTest < ActiveSupport::TestCase
4
+ class FakeTask < OpenStruct
5
+ class Jail < Safemode::Jail
6
+ allow :action_continuous_output, :result, :ended_at
7
+ end
8
+ end
9
+
10
+ context 'with valid job invocation report template' do
11
+ let(:job_invocation_template) do
12
+ file_path = File.read(File.expand_path(Rails.root + "app/views/unattended/report_templates/job_-_invocation_report.erb"))
13
+ template = ReportTemplate.import_without_save("Job Invocation Report Template", file_path)
14
+ template.save!
15
+ template
16
+ end
17
+
18
+ describe 'template setting' do
19
+ it 'in settings includes only report templates with job_id input' do
20
+ FactoryBot.create(:report_template, name: 'Template 1')
21
+ job_invocation_template
22
+ templates = ForemanRemoteExecution.job_invocation_report_templates_select
23
+
24
+ assert_include templates, 'Job Invocation Report Template'
25
+ end
26
+ end
27
+
28
+ describe 'task reporting' do
29
+ let(:fake_outputs) do
30
+ [
31
+ { 'output_type' => 'stderr', 'output' => "error" },
32
+ { 'output_type' => 'stdout', 'output' => "output" },
33
+ { 'output_type' => 'debug', 'output' => "debug" },
34
+ ]
35
+ end
36
+ let(:fake_task) { FakeTask.new(result: 'success', action_continuous_output: fake_outputs, :ended_at => Time.new(2020, 12, 1, 0, 0, 0).utc) }
37
+ let(:job_invocation) { FactoryBot.create(:job_invocation, :with_task) }
38
+ let(:host) { job_invocation.template_invocations.first.host }
39
+
40
+ it 'should render task outputs' do
41
+ JobInvocation.any_instance.expects(:sub_task_for_host).returns(fake_task)
42
+
43
+ input = job_invocation_template.template_inputs.first
44
+ composer_params = { template_id: job_invocation_template.id, input_values: { input.id.to_s => { value: job_invocation.id.to_s } } }
45
+ result = ReportComposer.new(composer_params).render
46
+
47
+ # parsing the CSV result
48
+ rows = CSV.parse(result.strip, headers: true)
49
+ assert_equal 1, rows.count
50
+ row = rows.first
51
+ assert_equal host.name, row['Host']
52
+ assert_equal 'success', row['Result']
53
+ assert_equal 'error', row['stderr']
54
+ assert_equal 'output', row['stdout']
55
+ assert_equal 'debug', row['debug']
56
+ assert_kind_of Time, Time.zone.parse(row['Finished']), 'Parsing of time column failed'
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,232 @@
1
+ require 'test_plugin_helper'
2
+
3
+ class JobInvocationTest < ActiveSupport::TestCase
4
+ let(:job_invocation) { FactoryBot.build(:job_invocation, :description => 'A text with "quotes"') }
5
+ let(:template) { FactoryBot.create(:job_template, :with_input) }
6
+
7
+ context 'search for job invocations' do
8
+ before do
9
+ job_invocation.save
10
+ end
11
+
12
+ it 'is able to perform search through job invocations' do
13
+ found_jobs = JobInvocation.search_for(%{job_category = "#{job_invocation.job_category}"}).paginate(:page => 1).order('job_invocations.id DESC')
14
+ assert_equal [job_invocation], found_jobs
15
+ end
16
+
17
+ it 'is able to auto complete description' do
18
+ expected = 'description = "A text with \"quotes\""'
19
+ assert_equal [expected], JobInvocation.complete_for('description = ')
20
+ end
21
+ end
22
+
23
+ context 'able to be created' do
24
+ it { assert job_invocation.save }
25
+ end
26
+
27
+ context 'requires targeting' do
28
+ before { job_invocation.targeting = nil }
29
+
30
+ it { refute_valid job_invocation }
31
+ end
32
+
33
+ context 'can delete a host' do
34
+ let(:host) do
35
+ FactoryBot.create(:host)
36
+ end
37
+
38
+ it 'can remove a host' do
39
+ job_invocation.template_invocations.build(:host_id => host.id, :template_id => template.id)
40
+ job_invocation.save!
41
+ host.destroy
42
+ job_invocation.reload
43
+ assert_empty job_invocation.template_invocations
44
+ end
45
+ end
46
+
47
+ context 'has template invocations with input values' do
48
+ let(:job_invocation) { FactoryBot.create(:job_invocation, :with_template) }
49
+
50
+ before do
51
+ input = job_invocation.pattern_template_invocations.first.template.template_inputs.create!(:name => 'foo', :required => true, :input_type => 'user')
52
+ input2 = job_invocation.pattern_template_invocations.first.template.template_inputs.create!(:name => 'bar', :required => true, :input_type => 'user')
53
+ FactoryBot.create(:template_invocation_input_value,
54
+ :template_invocation => job_invocation.pattern_template_invocations.first,
55
+ :template_input => input2)
56
+ @input_value = FactoryBot.create(:template_invocation_input_value,
57
+ :template_invocation => job_invocation.pattern_template_invocations.first,
58
+ :template_input => input)
59
+ job_invocation.reload
60
+ job_invocation.pattern_template_invocations.first.reload
61
+ end
62
+
63
+ it { assert_not job_invocation.reload.pattern_template_invocations.empty? }
64
+ it { assert_not job_invocation.reload.pattern_template_invocations.first.input_values.empty? }
65
+
66
+ it "can look up templates not belonging to user's organization" do
67
+ organization = job_invocation.pattern_template_invocations.first.template.organizations.first
68
+ Organization.current = organization
69
+ job_invocation.pattern_template_invocations.first.template.organizations = []
70
+ # The following line raises UndefinedMethod if the user can't look up the template
71
+ job_invocation.pattern_template_invocations.first.template.name
72
+
73
+ # Restore things to original state
74
+ job_invocation.pattern_template_invocations.first.template.organizations = [organization]
75
+ Organization.current = nil
76
+ end
77
+
78
+ it 'validates required inputs have values' do
79
+ assert job_invocation.valid?
80
+ @input_value.destroy
81
+ assert_not job_invocation.reload.valid?
82
+ end
83
+
84
+ describe 'descriptions' do
85
+ it 'generates description from input values' do
86
+ job_invocation.description_format = '%{job_category} - %{foo}'
87
+ job_invocation.generate_description
88
+ assert_equal "#{job_invocation.job_category} - #{@input_value.value}", job_invocation.description
89
+ end
90
+
91
+ it 'handles missing keys correctly' do
92
+ job_invocation.description_format = '%{job_category} - %{missing_key}'
93
+ job_invocation.generate_description
94
+ assert_equal "#{job_invocation.job_category} - ''", job_invocation.description
95
+ end
96
+
97
+ it 'truncates generated description to 255 characters' do
98
+ column_limit = 255 # There is a 255 character limit on the database level
99
+ expected_result = 'a' * column_limit
100
+ job_invocation.description_format = '%{job_category}'
101
+ job_invocation.job_category = 'a' * 1000
102
+ job_invocation.generate_description
103
+ assert_equal expected_result, job_invocation.description
104
+ end
105
+ end
106
+ end
107
+
108
+ context 'future execution' do
109
+ it 'can report host count' do
110
+ assert_equal 'N/A', job_invocation.total_hosts_count
111
+ job_invocation.targeting.expects(:resolved_at).returns(Time.now.getlocal)
112
+ assert_equal 0, job_invocation.total_hosts_count
113
+ end
114
+
115
+ # task does not exist
116
+ specify { assert_equal HostStatus::ExecutionStatus::QUEUED, job_invocation.status }
117
+ specify { assert_equal HostStatus::ExecutionStatus::STATUS_NAMES[HostStatus::ExecutionStatus::QUEUED], job_invocation.status_label }
118
+ specify { assert_equal 0, job_invocation.progress }
119
+ end
120
+
121
+ context 'with task' do
122
+ let(:task) { ForemanTasks::Task.new }
123
+ let(:progress_report_without_sub_tasks) do
124
+ {
125
+ :error => 0,
126
+ :warning => 0,
127
+ :total => 0,
128
+ :success => 0,
129
+ :cancelled => 0,
130
+ :failed => 0,
131
+ :pending => 0,
132
+ :progress => 0,
133
+ :running => 0,
134
+ }
135
+ end
136
+ before { job_invocation.task = task }
137
+
138
+ context 'which is scheduled' do
139
+ before { task.state = 'scheduled' }
140
+
141
+ specify { assert_equal HostStatus::ExecutionStatus::QUEUED, job_invocation.status }
142
+ specify { assert job_invocation.queued? }
143
+ specify { assert_equal 0, job_invocation.progress }
144
+ specify { assert_equal progress_report_without_sub_tasks, job_invocation.progress_report }
145
+ end
146
+
147
+ context 'with cancelled task' do
148
+ before do
149
+ task.state = 'stopped'
150
+ task.result = 'error'
151
+ end
152
+
153
+ it 'calculates the progress correctly' do
154
+ job_invocation.targeting.stubs(:resolved?).returns(true)
155
+ task.expects(:sub_tasks_counts).never
156
+ assert_equal progress_report_without_sub_tasks, job_invocation.progress_report
157
+ end
158
+ end
159
+
160
+ context 'with succeeded task' do
161
+ before do
162
+ task.state = 'stopped'
163
+ task.result = 'success'
164
+ end
165
+
166
+ specify { assert_equal HostStatus::ExecutionStatus::OK, job_invocation.status }
167
+ specify { refute job_invocation.queued? }
168
+
169
+ it 'calculates the progress correctly' do
170
+ sub_tasks = [ForemanTasks::Task.new]
171
+ job_invocation.targeting.expects(:resolved?).returns(true)
172
+ job_invocation.targeting.expects(:hosts).returns([1])
173
+ sub_tasks.expects(:where).with(:result => %w(success warning error)).returns(sub_tasks)
174
+ job_invocation.stubs(:sub_tasks).returns(sub_tasks)
175
+
176
+ assert_equal 100, job_invocation.progress
177
+ end
178
+ end
179
+ end
180
+
181
+ describe '#finished?' do
182
+ let(:task) { ForemanTasks::Task.new }
183
+ before { job_invocation.task = task }
184
+
185
+ it 'returns false if task state is pending' do
186
+ job_invocation.task.expects(:pending?).returns(true)
187
+ assert_not job_invocation.finished?
188
+ end
189
+
190
+ it 'returns true if task is not pending' do
191
+ job_invocation.task.expects(:pending?).returns(false)
192
+ assert job_invocation.finished?
193
+ end
194
+ end
195
+
196
+ describe '#failed_hosts' do
197
+ let(:invocation) do
198
+ invocation = FactoryBot.build(:job_invocation, :with_template, :with_task, :with_failed_task, :with_unplanned_host)
199
+ invocation.template_invocations.each { |ti| invocation.targeting.hosts << ti.host }
200
+ invocation.save!
201
+ invocation
202
+ end
203
+
204
+ it 'returns only failed hosts when not #finished?' do
205
+ invocation.stubs(:finished?).returns(false)
206
+ assert_equal 1, invocation.failed_hosts.count
207
+ end
208
+
209
+ it 'returns failed hosts and hosts without task when #finished?' do
210
+ invocation.stubs(:finished?).returns(true)
211
+ assert_equal 2, invocation.failed_hosts.count
212
+ end
213
+
214
+ describe '#failed_template_invocations' do
215
+ it 'finds only failed template invocations' do
216
+ template_invocations = invocation.send(:failed_template_invocations)
217
+ assert_equal 1, template_invocations.count
218
+ template_invocation = template_invocations.first
219
+ assert_equal 'error', template_invocation.run_host_job_task.result
220
+ end
221
+ end
222
+
223
+ describe '#not_failed_template_invocations' do
224
+ it 'finds only non-failed template invocations' do
225
+ template_invocations = invocation.send(:not_failed_template_invocations)
226
+ assert_equal 1, template_invocations.count
227
+ template_invocation = template_invocations.first
228
+ assert_equal 'success', template_invocation.run_host_job_task.result
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,37 @@
1
+ require 'test_plugin_helper'
2
+
3
+ class JobTemplateEffectiveUserTest < ActiveSupport::TestCase
4
+ let(:job_template) { FactoryBot.build(:job_template, :job_category => '') }
5
+ let(:effective_user) { job_template.effective_user }
6
+
7
+ describe 'by default' do
8
+ it 'is overridable' do
9
+ assert effective_user.overridable?
10
+ end
11
+
12
+ it 'does not use the current user' do
13
+ assert_not effective_user.current_user?
14
+ end
15
+ end
16
+
17
+ describe 'compute value' do
18
+ it 'computes the value based on the current user when current_user set to true' do
19
+ user = FactoryBot.create(:user)
20
+ User.current = user
21
+ effective_user.current_user = true
22
+ assert_equal user.login, effective_user.compute_value
23
+ end
24
+
25
+ it 'returns the value when not current user is set to true' do
26
+ effective_user.current_user = false
27
+ effective_user.value = 'testuser'
28
+ assert_equal 'testuser', effective_user.compute_value
29
+ end
30
+
31
+ it 'returns a default value when no value is specified for the user' do
32
+ effective_user.value = ''
33
+ Setting[:remote_execution_effective_user] = 'myuser'
34
+ assert_equal 'myuser', effective_user.compute_value
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,316 @@
1
+ require 'test_plugin_helper'
2
+
3
+ class JobTemplateTest < ActiveSupport::TestCase
4
+ context 'when creating a template' do
5
+ let(:job_template) { FactoryBot.build(:job_template, :job_category => '') }
6
+ let(:template_with_inputs) do
7
+ FactoryBot.build(:job_template, :template => 'test').tap do |template|
8
+ template.template_inputs << FactoryBot.build(:template_input, :name => 'command', :input_type => 'user')
9
+ template.save!
10
+ end
11
+ end
12
+
13
+ it 'has a unique name' do
14
+ template1 = FactoryBot.create(:job_template)
15
+ template2 = FactoryBot.build(:job_template, :name => template1.name)
16
+ assert_not template2.valid?
17
+ end
18
+
19
+ it 'needs a job_category' do
20
+ assert_not job_template.valid?
21
+ end
22
+
23
+ it 'does not need a job_category if it is a snippet' do
24
+ job_template.snippet = true
25
+ assert job_template.valid?
26
+ end
27
+
28
+ it 'validates the inputs are uniq in the template' do
29
+ job_template.job_category = 'Miscellaneous'
30
+ job_template.foreign_input_sets << FactoryBot.build(:foreign_input_set, :target_template => template_with_inputs)
31
+ job_template.foreign_input_sets << FactoryBot.build(:foreign_input_set, :target_template => template_with_inputs)
32
+ assert_not job_template.valid?
33
+ assert_includes job_template.errors.full_messages.first, 'Duplicated inputs detected: ["command"]'
34
+ end
35
+ end
36
+
37
+ context 'description format' do
38
+ let(:template_with_description) { FactoryBot.build(:job_template, :with_description_format, :job_category => 'test job') }
39
+ let(:template) { FactoryBot.build(:job_template, :with_input, :job_category => 'test job') }
40
+ let(:minimal_template) { FactoryBot.build(:job_template) }
41
+
42
+ it 'uses the description_format attribute if set' do
43
+ assert_equal template_with_description.generate_description_format, template_with_description.description_format
44
+ end
45
+
46
+ it 'uses the job name as description_format if not set or blank and has no inputs' do
47
+ assert_equal minimal_template.generate_description_format, '%{template_name}'
48
+ minimal_template.description_format = ''
49
+ assert_equal minimal_template.generate_description_format, '%{template_name}'
50
+ end
51
+
52
+ it 'generates the description_format if not set or blank and has inputs' do
53
+ input_name = template.template_inputs.first.name
54
+ expected_result = %(%{template_name} with inputs #{input_name}="%{#{input_name}}")
55
+ assert_equal template.generate_description_format, expected_result
56
+ template.description_format = ''
57
+ assert_equal template.generate_description_format, expected_result
58
+ end
59
+ end
60
+
61
+ context 'cloning' do
62
+ let(:job_template) { FactoryBot.build(:job_template, :with_input) }
63
+
64
+ describe '#dup' do
65
+ it 'duplicates also template inputs' do
66
+ duplicate = job_template.dup
67
+ refute_equal duplicate, job_template
68
+ refute_empty duplicate.template_inputs
69
+ refute_equal duplicate.template_inputs.first, job_template.template_inputs.first
70
+ assert_equal duplicate.template_inputs.first.name, job_template.template_inputs.first.name
71
+ end
72
+ end
73
+ end
74
+
75
+ context 'importing a new template' do
76
+ let(:remote_execution_feature) do
77
+ FactoryBot.create(:remote_execution_feature)
78
+ end
79
+
80
+ let(:template) do
81
+ template = <<-END_TEMPLATE
82
+ <%#
83
+ kind: job_template
84
+ name: Service Restart
85
+ job_category: Service Restart
86
+ provider_type: SSH
87
+ feature: #{remote_execution_feature.label}
88
+ template_inputs:
89
+ - name: service_name
90
+ input_type: user
91
+ required: true
92
+ - name: verbose
93
+ input_type: user
94
+ %>
95
+
96
+ service <%= input("service_name") %> restart
97
+
98
+ <%# test comment %>
99
+ END_TEMPLATE
100
+
101
+ JobTemplate.import_raw!(template, :default => true)
102
+ end
103
+
104
+ let(:template_with_input_sets) do
105
+ template_with_input_sets = <<-END_TEMPLATE
106
+ <%#
107
+ kind: job_template
108
+ name: Service Restart - Custom
109
+ job_category: Service Restart
110
+ provider_type: SSH
111
+ foreign_input_sets:
112
+ - template: #{template.name}
113
+ exclude: verbose
114
+ %>
115
+
116
+ service <%= input("service_name") %> restart
117
+ END_TEMPLATE
118
+
119
+ JobTemplate.import_raw!(template_with_input_sets, :default => true)
120
+ end
121
+
122
+ it 'sets the name' do
123
+ assert_equal template.name, 'Service Restart'
124
+ end
125
+
126
+ it 'has a template' do
127
+ assert_equal template.template.squish, 'service <%= input("service_name") %> restart <%# test comment %>'
128
+ end
129
+
130
+ it 'imports inputs' do
131
+ assert_equal template.template_inputs.first.name, 'service_name'
132
+ end
133
+
134
+ it 'imports input sets' do
135
+ assert_equal template_with_input_sets.foreign_input_sets.first.target_template, template
136
+ assert_equal template_with_input_sets.template_inputs_with_foreign.map(&:name), ['service_name']
137
+ end
138
+
139
+ it 'imports feature' do
140
+ template # let is lazy
141
+ remote_execution_feature.reload
142
+ assert_equal remote_execution_feature.job_template, template
143
+ end
144
+
145
+ it 'sets additional options' do
146
+ assert_equal template.default, true
147
+ end
148
+ end
149
+
150
+ context 'importing an existing template' do
151
+ let(:included) do
152
+ template = <<-END_TEMPLATE
153
+ <%#
154
+ kind: job_template
155
+ name: Banner
156
+ job_category: Commands
157
+ provider_type: SSH
158
+ template_inputs:
159
+ - name: banner_message
160
+ input_type: user
161
+ required: true
162
+ %>
163
+
164
+ echo input(:banner_message)
165
+ END_TEMPLATE
166
+
167
+ JobTemplate.import_raw!(template, :default => true)
168
+ end
169
+
170
+ let(:existing) do
171
+ template = <<-END_TEMPLATE
172
+ <%#
173
+ kind: job_template
174
+ name: Ping a Thing
175
+ job_category: Commands
176
+ provider_type: SSH
177
+ template_inputs:
178
+ - name: hostname
179
+ input_type: user
180
+ options: "www.google.com"
181
+ required: true
182
+ foreign_input_sets:
183
+ - template: #{included.name}
184
+ %>
185
+
186
+ ping -c 5 <%= input("hostname") %>
187
+ END_TEMPLATE
188
+
189
+ JobTemplate.import_raw!(template, :default => true)
190
+ end
191
+
192
+ let(:updated) do
193
+ <<-END_TEMPLATE
194
+ <%#
195
+ kind: job_template
196
+ name: Ping a Thing
197
+ job_category: Commands
198
+ provider_type: SSH
199
+ template_inputs:
200
+ - name: hostname
201
+ input_type: user
202
+ options: 'www.redhat.com'
203
+ required: true
204
+ - name: count
205
+ input_type: user
206
+ required: true
207
+ foreign_input_sets:
208
+ - template: #{included.name}
209
+ exclude: banner_message
210
+ %>
211
+
212
+ ping -c <%= input('count') %> <%= input('hostname') %>
213
+ END_TEMPLATE
214
+ end
215
+
216
+ it 'will not overwrite by default' do
217
+ existing
218
+ assert_not JobTemplate.import_raw!(updated)
219
+ end
220
+
221
+ let(:synced_template) do
222
+ existing
223
+ JobTemplate.import_raw!(updated, :update => true)
224
+ existing.reload
225
+ end
226
+
227
+ it 'syncs inputs' do
228
+ hostname = synced_template.template_inputs.find { |input| input.name == 'hostname' }
229
+ assert_equal hostname.options, 'www.redhat.com'
230
+ end
231
+
232
+ it 'syncs content' do
233
+ assert_match(/ping -c <%= input\('count'\) %> <%= input\('hostname'\) %>/m, synced_template.template)
234
+ end
235
+
236
+ it 'syncs input sets' do
237
+ assert_equal synced_template.foreign_input_sets.first.target_template, included
238
+ assert_equal synced_template.template_inputs_with_foreign.map(&:name), ['hostname', 'count']
239
+ end
240
+ end
241
+
242
+ context 'template export' do
243
+ let(:exportable_template) do
244
+ FactoryBot.create(:job_template, :with_input)
245
+ end
246
+
247
+ let(:erb) do
248
+ exportable_template.to_erb
249
+ end
250
+
251
+ it 'exports name' do
252
+ assert_match(/^name: #{exportable_template.name}$/, erb)
253
+ end
254
+
255
+ it 'includes template inputs' do
256
+ assert_match(/^template_inputs:$/, erb)
257
+ end
258
+
259
+ it 'includes template contents' do
260
+ assert_includes erb, exportable_template.template
261
+ end
262
+
263
+ it 'is importable' do
264
+ erb
265
+ old_name = exportable_template.name
266
+ exportable_template.update(:name => "#{old_name}_renamed")
267
+
268
+ imported = JobTemplate.import_raw!(erb)
269
+ assert_equal imported.name, old_name
270
+ assert_equal imported.template_inputs.first.to_export, exportable_template.template_inputs.first.to_export
271
+ end
272
+
273
+ it 'has taxonomies in metadata' do
274
+ assert_equal 'Organization 1', exportable_template.to_export["organizations"].first
275
+ assert_equal 'Location 1', exportable_template.to_export["locations"].first
276
+ end
277
+ end
278
+
279
+ context 'there is existing template invocation of a job template' do
280
+ let(:job_invocation) { FactoryBot.create(:job_invocation, :with_template) }
281
+ let(:job_template) { job_invocation.pattern_template_invocations.first.template }
282
+
283
+ describe 'job template deletion' do
284
+ it 'succeeds' do
285
+ refute_empty job_template.pattern_template_invocations
286
+ assert job_template.destroy
287
+ end
288
+ end
289
+ end
290
+
291
+ context 'template locked' do
292
+ it 'inputs cannot be changed' do
293
+ job_template = FactoryBot.create(:job_template, :with_input, :locked => true)
294
+ Foreman.expects(:in_rake?).returns(false).at_least_once
295
+ assert_valid job_template
296
+ job_template.template_inputs.first.name = 'something else'
297
+ refute_valid job_template
298
+ end
299
+ end
300
+
301
+ context 'rendering' do
302
+ it 'renders nested template as a non-admin user' do
303
+ inner = FactoryBot.create(:job_template)
304
+ template_invocation = FactoryBot.create(:template_invocation)
305
+ template_invocation.template.template = "<wrap><%= render_template('#{inner.name}') %></wrap>"
306
+ template_invocation.template.save!
307
+
308
+ setup_user('view', 'job_templates')
309
+ renderer = InputTemplateRenderer.new template_invocation.template,
310
+ template_invocation.host,
311
+ template_invocation
312
+ result = renderer.render
313
+ assert_equal result, "<wrap>#{inner.template}</wrap>"
314
+ end
315
+ end
316
+ end