foreman-tasks 3.0.6 → 4.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/app/lib/actions/entry_action.rb +8 -4
  3. data/app/lib/actions/helpers/lock.rb +11 -5
  4. data/app/lib/actions/middleware/keep_current_request_id.rb +4 -1
  5. data/app/lib/actions/middleware/keep_current_user.rb +11 -1
  6. data/app/lib/actions/observable_action.rb +80 -0
  7. data/app/models/foreman_tasks/concerns/action_subject.rb +0 -6
  8. data/app/models/foreman_tasks/link.rb +60 -0
  9. data/app/models/foreman_tasks/lock.rb +30 -128
  10. data/app/models/foreman_tasks/task.rb +20 -7
  11. data/app/models/foreman_tasks/task/dynflow_task.rb +3 -8
  12. data/app/models/foreman_tasks/task/search.rb +6 -6
  13. data/app/views/foreman_tasks/api/locks/show.json.rabl +4 -0
  14. data/app/views/foreman_tasks/api/tasks/details.json.rabl +5 -3
  15. data/app/views/foreman_tasks/tasks/_lock_card.html.erb +10 -0
  16. data/db/migrate/20181206123910_create_foreman_tasks_links.foreman_tasks.rb +26 -0
  17. data/db/migrate/20181206124952_migrate_non_exclusive_locks_to_links.foreman_tasks.rb +14 -0
  18. data/db/migrate/20181206131436_drop_old_locks.foreman_tasks.rb +20 -0
  19. data/db/migrate/20181206131627_make_locks_exclusive.foreman_tasks.rb +25 -0
  20. data/lib/foreman_tasks/cleaner.rb +10 -0
  21. data/lib/foreman_tasks/engine.rb +5 -2
  22. data/lib/foreman_tasks/tasks/export_tasks.rake +2 -2
  23. data/lib/foreman_tasks/version.rb +1 -1
  24. data/package.json +6 -6
  25. data/test/controllers/tasks_controller_test.rb +1 -1
  26. data/test/unit/actions/action_with_sub_plans_test.rb +5 -2
  27. data/test/unit/cleaner_test.rb +4 -4
  28. data/test/unit/locking_test.rb +85 -0
  29. data/webpack/ForemanTasks/Components/TaskDetails/Components/Locks.js +2 -2
  30. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +3 -2
  31. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/Locks.test.js.snap +4 -4
  32. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/TaskInfo.test.js.snap +2 -0
  33. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +4 -1
  34. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +5 -1
  35. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +3 -0
  36. data/webpack/ForemanTasks/Components/TaskDetails/index.js +2 -0
  37. data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.scss +4 -3
  38. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +2 -2
  39. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/actionNameCellFormatter.test.js.snap +2 -3
  40. data/webpack/ForemanTasks/Components/TasksTable/formatters/actionNameCellFormatter.js +2 -3
  41. metadata +12 -4
  42. data/test/unit/lock_test.rb +0 -22
@@ -31,6 +31,7 @@ module ForemanTasks
31
31
  has_many :recurring_logic_task_groups, -> { where :type => 'ForemanTasks::TaskGroups::RecurringLogicTaskGroup' },
32
32
  :through => :task_group_members, :source => :task_group
33
33
  belongs_to :user
34
+ has_many :links, :dependent => :destroy
34
35
 
35
36
  scoped_search :on => :id, :complete_value => false
36
37
  scoped_search :on => :action, :complete_value => false
@@ -46,10 +47,16 @@ module ForemanTasks
46
47
 
47
48
  # Note: the following searches may return duplicates, this is due to
48
49
  # one task maybe having multiple locks (e.g. read/write) for the same resource_id
49
- scoped_search :relation => :locks, :on => :resource_id, :complete_value => false, :rename => 'location_id', :ext_method => :search_by_taxonomy, :only_explicit => true
50
- scoped_search :relation => :locks, :on => :resource_id, :complete_value => false, :rename => 'organization_id', :ext_method => :search_by_taxonomy, :only_explicit => true
51
- scoped_search :relation => :locks, :on => :resource_type, :complete_value => true, :rename => 'resource_type', :ext_method => :search_by_generic_resource, :only_explicit => true
52
- scoped_search :relation => :locks, :on => :resource_id, :complete_value => false, :rename => 'resource_id', :ext_method => :search_by_generic_resource, :only_explicit => true
50
+ scoped_search :relation => :locks, :on => :resource_id, :complete_value => false, :rename => 'locked_location_id', :ext_method => :search_by_taxonomy, :only_explicit => true
51
+ scoped_search :relation => :locks, :on => :resource_id, :complete_value => false, :rename => 'locked_organization_id', :ext_method => :search_by_taxonomy, :only_explicit => true
52
+ scoped_search :relation => :locks, :on => :resource_type, :complete_value => true, :rename => 'locked_resource_type', :ext_method => :search_by_generic_resource, :only_explicit => true
53
+ scoped_search :relation => :locks, :on => :resource_id, :complete_value => false, :rename => 'locked_resource_id', :ext_method => :search_by_generic_resource, :only_explicit => true
54
+
55
+ scoped_search :relation => :links, :on => :resource_id, :complete_value => false, :rename => 'location_id', :ext_method => :search_by_taxonomy, :only_explicit => true
56
+ scoped_search :relation => :links, :on => :resource_id, :complete_value => false, :rename => 'organization_id', :ext_method => :search_by_taxonomy, :only_explicit => true
57
+ scoped_search :relation => :links, :on => :resource_type, :complete_value => true, :rename => 'resource_type', :ext_method => :search_by_generic_resource, :only_explicit => true
58
+ scoped_search :relation => :links, :on => :resource_id, :complete_value => false, :rename => 'resource_id', :ext_method => :search_by_generic_resource, :only_explicit => true
59
+
53
60
  scoped_search :on => :user_id,
54
61
  :complete_value => true,
55
62
  :rename => 'user.id',
@@ -65,8 +72,8 @@ module ForemanTasks
65
72
  scope :running, -> { where("foreman_tasks_tasks.state NOT IN ('stopped', 'paused')") }
66
73
  scope :for_resource,
67
74
  (lambda do |resource|
68
- joins(:locks).where(:"foreman_tasks_locks.resource_id" => resource.id,
69
- :"foreman_tasks_locks.resource_type" => resource.class.name)
75
+ joins(:links).where(:"foreman_tasks_links.resource_id" => resource.id,
76
+ :"foreman_tasks_links.resource_type" => resource.class.name)
70
77
  end)
71
78
  scope :for_action_types, (->(action_types) { where('foreman_tasks_tasks.label IN (?)', Array(action_types)) })
72
79
 
@@ -82,7 +89,7 @@ module ForemanTasks
82
89
  property :ended_at, ActiveSupport::TimeWithZone, desc: 'Returns date with time the task ended at'
83
90
  end
84
91
  class Jail < Safemode::Jail
85
- allow :started_at, :ended_at, :result, :state, :label, :main_action
92
+ allow :started_at, :ended_at, :result, :state, :label, :main_action, :action_output
86
93
  end
87
94
 
88
95
  def input
@@ -242,6 +249,12 @@ module ForemanTasks
242
249
  parts.join(' ').strip
243
250
  end
244
251
 
252
+ def action_output
253
+ return unless main_action.is_a?(Actions::Helpers::WithContinuousOutput)
254
+ main_action.continuous_output.sort!
255
+ main_action.continuous_output.raw_outputs
256
+ end
257
+
245
258
  protected
246
259
 
247
260
  def generate_id
@@ -140,17 +140,12 @@ module ForemanTasks
140
140
 
141
141
  def main_action
142
142
  return @main_action if defined?(@main_action)
143
-
144
- @main_action = execution_plan && execution_plan.root_plan_step.try(:action, execution_plan)
145
143
  if active_job?
146
144
  job_data = active_job_data
147
- begin
148
- @main_action = active_job_action(job_data['job_class'], job_data['arguments'])
149
- rescue => e
150
- Foreman::Logging.exception("Failed to load ActiveJob for task #{id}", e, :logger => 'foreman-tasks')
151
- end
145
+ @main_action = active_job_action(job_data['job_class'], job_data['arguments'])
146
+ else
147
+ @main_action = execution_plan && execution_plan.root_plan_step.try(:action, execution_plan)
152
148
  end
153
- @main_action
154
149
  end
155
150
 
156
151
  # The class for ActiveJob jobs in Dynflow, JobWrapper is not expected to
@@ -5,9 +5,9 @@ module ForemanTasks
5
5
  key = 'resource_type' if key.blank?
6
6
  key_name = connection.quote_column_name(key.sub(/^.*\./, ''))
7
7
  value = value.split(',') if operator.index(/IN/i)
8
- condition = sanitize_sql_for_conditions(["foreman_tasks_locks.#{key_name} #{operator} (?)", value])
8
+ condition = sanitize_sql_for_conditions(["foreman_tasks_links.#{key_name} #{operator} (?)", value])
9
9
 
10
- { :conditions => condition, :joins => :locks }
10
+ { :conditions => condition, :joins => :links }
11
11
  end
12
12
 
13
13
  def search_by_taxonomy(key, operator, value)
@@ -15,12 +15,12 @@ module ForemanTasks
15
15
  resource_type = key == 'location_id' ? 'Location' : 'Organization'
16
16
 
17
17
  joins = <<-SQL
18
- LEFT JOIN foreman_tasks_locks AS foreman_tasks_locks_taxonomy#{uniq_suffix}
19
- ON (foreman_tasks_locks_taxonomy#{uniq_suffix}.task_id = foreman_tasks_tasks.id AND
20
- foreman_tasks_locks_taxonomy#{uniq_suffix}.resource_type = '#{resource_type}')
18
+ LEFT JOIN foreman_tasks_links AS foreman_tasks_links_taxonomy#{uniq_suffix}
19
+ ON (foreman_tasks_links_taxonomy#{uniq_suffix}.task_id = foreman_tasks_tasks.id AND
20
+ foreman_tasks_links_taxonomy#{uniq_suffix}.resource_type = '#{resource_type}')
21
21
  SQL
22
22
  # Select only those tasks which either have the correct taxonomy or are not related to any
23
- sql = "foreman_tasks_locks_taxonomy#{uniq_suffix}.resource_id #{operator} (?) OR foreman_tasks_locks_taxonomy#{uniq_suffix}.resource_id IS NULL"
23
+ sql = "foreman_tasks_links_taxonomy#{uniq_suffix}.resource_id #{operator} (?) OR foreman_tasks_links_taxonomy#{uniq_suffix}.resource_id IS NULL"
24
24
  value = value.split(',') if operator.index(/IN/i)
25
25
  { :conditions => sanitize_sql_for_conditions([sql, value]), :joins => joins }
26
26
  end
@@ -0,0 +1,4 @@
1
+ object @lock
2
+
3
+ attributes :name, :resource_type, :resource_id
4
+ node(:exclusive) { !locals[:link] }
@@ -9,10 +9,12 @@ node(:failed_steps) { @task.input_output_failed_steps }
9
9
  node(:running_steps) { @task.input_output_running_steps }
10
10
  node(:help) { troubleshooting_info_text }
11
11
  node(:has_sub_tasks) { @task.sub_tasks.any? }
12
+
12
13
  node(:locks) do
13
- @task.locks.map do |lock|
14
- { name: lock.name, exclusive: lock.exclusive, resource_type: lock.resource_type, resource_id: lock.resource_id }
15
- end
14
+ @task.locks.map { |lock| partial('foreman_tasks/api/locks/show', :object => lock) }
15
+ end
16
+ node(:links) do
17
+ @task.links.map { |link| partial('foreman_tasks/api/locks/show', :object => link, :locals => { :link => true }) }
16
18
  end
17
19
  node(:username_path) { username_link_task(@task.owner, @task.username) }
18
20
  node(:dynflow_enable_console) { Setting['dynflow_enable_console'] }
@@ -0,0 +1,10 @@
1
+ <div class="col-xs-6 col-sm-4 col-md-4">
2
+ <div class="card-pf card-pf-accented card-pf-aggregate-status">
3
+ <h2 class="card-pf-title">
4
+ <span class="fa <%= lock.is_a?(::ForemanTasks::Lock) ? 'fa-lock' : 'fa-unlock-alt' %>"></span> <%= lock.resource_type %>
5
+ </h2>
6
+ <div class="card-pf-body">
7
+ <%= format('id:%s', lock.resource_id) %><br>
8
+ </div>
9
+ </div>
10
+ </div>
@@ -0,0 +1,26 @@
1
+ class CreateForemanTasksLinks < ActiveRecord::Migration[4.2]
2
+ def change
3
+ # rubocop:disable Rails/CreateTableWithTimestamps
4
+ create_table :foreman_tasks_links do |t|
5
+ task_id_options = { :index => true, :null => true }
6
+ if on_postgresql?
7
+ t.uuid :task_id, task_id_options
8
+ else
9
+ t.string :task_id, task_id_options
10
+ end
11
+ t.string :resource_type
12
+ t.integer :resource_id
13
+ end
14
+ # rubocop:enable Rails/CreateTableWithTimestamps
15
+
16
+ add_index :foreman_tasks_links, [:resource_type, :resource_id]
17
+ add_index :foreman_tasks_links, [:task_id, :resource_type, :resource_id],
18
+ :unique => true, :name => 'foreman_tasks_links_unique_index'
19
+ end
20
+
21
+ private
22
+
23
+ def on_postgresql?
24
+ ActiveRecord::Base.connection.adapter_name.casecmp('postgresql').zero?
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ class MigrateNonExclusiveLocksToLinks < ActiveRecord::Migration[5.0]
2
+ def up
3
+ execute <<-SQL
4
+ INSERT INTO foreman_tasks_links(task_id, resource_type, resource_id)
5
+ SELECT DISTINCT locks.task_id, locks.resource_type, locks.resource_id
6
+ FROM foreman_tasks_locks AS locks
7
+ LEFT JOIN foreman_tasks_links AS links
8
+ ON links.task_id = locks.task_id
9
+ AND links.resource_type = locks.resource_type
10
+ AND links.resource_id = locks.resource_id
11
+ WHERE locks.exclusive = FALSE AND links.task_id IS NULL;
12
+ SQL
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ class DropOldLocks < ActiveRecord::Migration[5.0]
2
+ BATCH_SIZE = 10_000
3
+
4
+ # Delete all locks which are exclusive or have a stopped task or are orphaned
5
+ def up
6
+ scope = ForemanTasks::Lock.left_outer_joins(:task)
7
+ scope = scope.where(:exclusive => false)
8
+ .or(scope.where("#{ForemanTasks::Task.table_name}.state" => ['stopped', nil]))
9
+ scope.limit(BATCH_SIZE).delete_all while scope.any?
10
+
11
+ # For each group of locks, where each lock has the same task_id, resource_type and resource_id
12
+ # return the highest id within the group, if there is more than 1 lock in the group
13
+ scope = ForemanTasks::Lock.select("MAX(id) as id")
14
+ .group(:task_id, :resource_type, :resource_id)
15
+ .having("count(*) > 1")
16
+
17
+ # Make sure there is at most one lock per task and resource
18
+ ForemanTasks::Lock.where(:id => scope.limit(BATCH_SIZE)).delete_all while scope.any?
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ class MakeLocksExclusive < ActiveRecord::Migration[5.0]
2
+ BATCH_SIZE = 2
3
+
4
+ def up
5
+ change_table :foreman_tasks_locks do |t|
6
+ t.remove :exclusive
7
+ t.remove :name
8
+ t.remove_index :name => 'index_foreman_tasks_locks_on_resource_type_and_resource_id'
9
+ t.index [:resource_type, :resource_id], :unique => true
10
+ end
11
+ end
12
+
13
+ def down
14
+ change_table :foreman_tasks_locks do |t|
15
+ t.boolean :exclusive, index: true
16
+ t.string :name, index: true
17
+ t.remove_index :name => 'index_foreman_tasks_locks_on_resource_type_and_resource_id'
18
+ t.index [:resource_type, :resource_id]
19
+ end
20
+
21
+ scope = ForemanTasks::Lock.where(:name => nil)
22
+ while scope.limit(BATCH_SIZE).update_all(:name => 'lock') == BATCH_SIZE; end
23
+ change_column_null(:foreman_tasks_locks, :name, false)
24
+ end
25
+ end
@@ -103,6 +103,7 @@ module ForemanTasks
103
103
  end
104
104
  end
105
105
  delete_orphaned_locks
106
+ delete_orphaned_links
106
107
  delete_orphaned_dynflow_tasks
107
108
  end
108
109
 
@@ -156,6 +157,15 @@ module ForemanTasks
156
157
  end
157
158
  end
158
159
 
160
+ def delete_orphaned_links
161
+ orphaned_links = ForemanTasks::Link.left_outer_joins(:task).where(:'foreman_tasks_tasks.id' => nil)
162
+ with_noop(orphaned_links, 'orphaned task links') do |source, name|
163
+ with_batches(source, name) do |chunk|
164
+ ForemanTasks::Link.where(id: chunk.pluck(:id)).delete_all
165
+ end
166
+ end
167
+ end
168
+
159
169
  def delete_orphaned_dynflow_tasks
160
170
  with_noop(orphaned_dynflow_tasks, 'orphaned execution plans') do |source, name|
161
171
  with_batches(source, name) do |chunk|
@@ -34,7 +34,7 @@ module ForemanTasks
34
34
 
35
35
  initializer 'foreman_tasks.register_plugin', :before => :finisher_hook do |_app|
36
36
  Foreman::Plugin.register :"foreman-tasks" do
37
- requires_foreman '>= 2.2.0'
37
+ requires_foreman '>= 2.4.0'
38
38
  divider :top_menu, :parent => :monitor_menu, :last => true, :caption => N_('Foreman Tasks')
39
39
  menu :top_menu, :tasks,
40
40
  :url_hash => { :controller => 'foreman_tasks/tasks', :action => :index },
@@ -76,6 +76,9 @@ module ForemanTasks
76
76
 
77
77
  widget 'foreman_tasks/tasks/dashboard/tasks_status', :sizex => 6, :sizey => 1, :name => N_('Task Status')
78
78
  widget 'foreman_tasks/tasks/dashboard/latest_tasks_in_error_warning', :sizex => 6, :sizey => 1, :name => N_('Latest Warning/Error Tasks')
79
+
80
+ ForemanTasks.dynflow.eager_load_actions!
81
+ extend_observable_events(::Dynflow::Action.descendants.select { |klass| klass <= ::Actions::ObservableAction }.map(&:namespaced_event_names))
79
82
  end
80
83
  end
81
84
 
@@ -108,7 +111,7 @@ module ForemanTasks
108
111
  ForemanTasks.dynflow.require!
109
112
  ::ForemanTasks.dynflow.config.on_init(false) do |world|
110
113
  world.middleware.use Actions::Middleware::KeepCurrentTaxonomies
111
- world.middleware.use Actions::Middleware::KeepCurrentUser
114
+ world.middleware.use Actions::Middleware::KeepCurrentUser, :before => ::Dynflow::Middleware::Common::Transaction
112
115
  world.middleware.use Actions::Middleware::KeepCurrentTimezone
113
116
  world.middleware.use Actions::Middleware::KeepCurrentRequestID
114
117
  end
@@ -252,7 +252,7 @@ namespace :foreman_tasks do
252
252
  renderer = TaskRender.new
253
253
  total = tasks.count
254
254
 
255
- tasks.each_with_index do |task, count|
255
+ tasks.find_each.with_index do |task, count|
256
256
  File.open(File.join(tmp_dir, "#{task.id}.html"), 'w') { |file| file.write(PageHelper.pagify(renderer.render_task(task))) }
257
257
  puts "#{count + 1}/#{total}"
258
258
  end
@@ -264,7 +264,7 @@ namespace :foreman_tasks do
264
264
  elsif format == 'csv'
265
265
  CSV.open(export_filename, 'wb') do |csv|
266
266
  csv << %w[id state type label result parent_task_id started_at ended_at]
267
- tasks.each do |task|
267
+ tasks.find_each do |task|
268
268
  csv << [task.id, task.state, task.type, task.label, task.result,
269
269
  task.parent_task_id, task.started_at, task.ended_at]
270
270
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanTasks
2
- VERSION = '3.0.6'.freeze
2
+ VERSION = '4.0.0'.freeze
3
3
  end
data/package.json CHANGED
@@ -23,7 +23,7 @@
23
23
  "url": "http://projects.theforeman.org/projects/foreman-tasks/issues"
24
24
  },
25
25
  "peerDependencies": {
26
- "@theforeman/vendor": ">= 4.0.2"
26
+ "@theforeman/vendor": ">= 6.0.0"
27
27
  },
28
28
  "dependencies": {
29
29
  "c3": "^0.4.11",
@@ -32,11 +32,11 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@babel/core": "^7.7.0",
35
- "@theforeman/builder": "^4.0.2",
36
- "@theforeman/eslint-plugin-foreman": "4.0.5",
37
- "@theforeman/stories": "^4.0.2",
38
- "@theforeman/test": "^4.0.2",
39
- "@theforeman/vendor-dev": "^4.0.2",
35
+ "@theforeman/builder": "^6.0.0",
36
+ "@theforeman/eslint-plugin-foreman": "6.0.0",
37
+ "@theforeman/stories": "^6.0.0",
38
+ "@theforeman/test": "^6.0.0",
39
+ "@theforeman/vendor-dev": "^6.0.0",
40
40
  "babel-eslint": "^10.0.3",
41
41
  "eslint": "^6.7.2",
42
42
  "jed": "^1.1.1",
@@ -10,7 +10,7 @@ module ForemanTasks
10
10
  # rubocop:enable Naming/AccessorMethodName
11
11
 
12
12
  def linked_task(resource)
13
- FactoryBot.create(:some_task).tap { |t| ForemanTasks::Lock.link!(resource, t.id) }
13
+ FactoryBot.create(:some_task).tap { |t| ForemanTasks::Link.link!(resource, t) }
14
14
  end
15
15
 
16
16
  def in_taxonomy_scope(organization, location = nil)
@@ -34,8 +34,9 @@ module ForemanTasks
34
34
  describe Actions::ActionWithSubPlans do
35
35
  include ForemanTasks::TestHelpers::WithInThreadExecutor
36
36
 
37
+ let(:user) { FactoryBot.create(:user) }
38
+
37
39
  let(:task) do
38
- user = FactoryBot.create(:user)
39
40
  triggered = ForemanTasks.trigger(ParentAction, user)
40
41
  raise triggered.error if triggered.respond_to?(:error)
41
42
  triggered.finished.wait(30)
@@ -49,7 +50,9 @@ module ForemanTasks
49
50
 
50
51
  specify "the locks of the sub-plan don't colide with the locks of its parent" do
51
52
  child_task = task.sub_tasks.first
52
- assert(child_task.locks.any? { |lock| lock.name == 'write' }, "it's locks don't conflict with parent's")
53
+ assert_not(child_task.locks.any?, "the lock is ensured by the parent")
54
+ found = ForemanTasks::Link.for_resource(user).where(:task_id => child_task.id).any?
55
+ assert(found, "the action is linked properly")
53
56
  end
54
57
  end
55
58
  end
@@ -60,18 +60,18 @@ class TasksTest < ActiveSupport::TestCase
60
60
  task.started_at = task.ended_at = Time.zone.now
61
61
  task.save
62
62
  end]
63
- lock_to_delete = tasks_to_delete.first.locks.create(:name => 'read', :resource => User.current)
63
+ link_to_delete = tasks_to_delete.first.links.create(:resource => User.current)
64
64
 
65
65
  tasks_to_keep = [FactoryBot.create(:dynflow_task, :product_create_task)]
66
- lock_to_keep = tasks_to_keep.first.locks.create(:name => 'read', :resource => User.current)
66
+ link_to_keep = tasks_to_keep.first.links.create(:resource => User.current)
67
67
 
68
68
  cleaner.expects(:tasks_to_csv)
69
69
  cleaner.delete
70
70
  _(ForemanTasks::Task.where(id: tasks_to_delete)).must_be_empty
71
71
  _(ForemanTasks::Task.where(id: tasks_to_keep)).must_equal tasks_to_keep
72
72
 
73
- _(ForemanTasks::Lock.find_by(id: lock_to_delete.id)).must_be_nil
74
- _(ForemanTasks::Lock.find_by(id: lock_to_keep.id)).wont_be_nil
73
+ _(ForemanTasks::Link.find_by(id: link_to_delete.id)).must_be_nil
74
+ _(ForemanTasks::Link.find_by(id: link_to_keep.id)).wont_be_nil
75
75
  end
76
76
 
77
77
  it 'supports passing empty filter (just delete all)' do
@@ -0,0 +1,85 @@
1
+ require 'ostruct'
2
+ require 'foreman_tasks_test_helper'
3
+
4
+ module ForemanTasks
5
+ class LockTest < ::ActiveSupport::TestCase
6
+ describe ::ForemanTasks::Lock::LockConflict do
7
+ class FakeLockConflict < ForemanTasks::Lock::LockConflict
8
+ def _(val)
9
+ val.freeze
10
+ end
11
+ end
12
+
13
+ it 'does not modify frozen strings' do
14
+ required_lock = OpenStruct.new(:name => 'my_lock')
15
+ # Before #21770 the next line would raise
16
+ # RuntimeError: can't modify frozen String
17
+ conflict = FakeLockConflict.new(required_lock, [])
18
+ assert conflict._('this should be frozen').frozen?
19
+ end
20
+ end
21
+
22
+ describe 'locking and linking' do
23
+ before { [Lock, Link].each(&:destroy_all) }
24
+ let(:task1) { FactoryBot.create(:some_task) }
25
+ let(:task2) { FactoryBot.create(:some_task) }
26
+ let(:resource) { FactoryBot.create(:user) }
27
+
28
+ describe Lock do
29
+ it 'can lock a resource for a single task' do
30
+ Lock.lock!(resource, task1)
31
+ end
32
+
33
+ it 'can lock a resource for a single task only once' do
34
+ Lock.lock!(resource, task1)
35
+ _(Lock.for_resource(resource).count).must_equal 1
36
+ Lock.lock!(resource, task1)
37
+ _(Lock.for_resource(resource).count).must_equal 1
38
+ end
39
+
40
+ it 'cannot lock a resource for multiple tasks' do
41
+ lock = Lock.lock!(resource, task1)
42
+ _(Lock.colliding_locks(resource, task2)).must_equal [lock]
43
+ assert_raises Lock::LockConflict do
44
+ Lock.lock!(resource, task2)
45
+ end
46
+ end
47
+
48
+ it 'raises LockConflict when enforced by db' do
49
+ lock = Lock.lock!(resource, task1)
50
+ Lock.any_instance
51
+ .expects(:colliding_locks).twice.returns([], [lock])
52
+ exception = assert_raises Lock::LockConflict do
53
+ Lock.lock!(resource, task2)
54
+ end
55
+ _(exception.message).must_match(/#{lock.task_id}/)
56
+ end
57
+
58
+ it 'creates a link when creating a lock for a resource' do
59
+ Lock.lock!(resource, task1)
60
+ link = Link.for_resource(resource).first
61
+ _(link.task_id).must_equal task1.id
62
+ end
63
+ end
64
+
65
+ describe Link do
66
+ it 'can link a resource to a single task' do
67
+ Link.link!(resource, task1)
68
+ end
69
+
70
+ it 'can link a resource for a single task only once' do
71
+ Link.link!(resource, task1)
72
+ _(Link.for_resource(resource).count).must_equal 1
73
+ Link.link!(resource, task1)
74
+ _(Link.for_resource(resource).count).must_equal 1
75
+ end
76
+
77
+ it 'can link a resource to multiple tasks' do
78
+ Link.link!(resource, task1)
79
+ Link.link!(resource, task2)
80
+ _(Link.for_resource(resource).count).must_equal 2
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end