Browse Source

Added database locking to prevent race conditions.

Martin Edenhofer 8 years ago
parent
commit
eceb9889b1

+ 7 - 4
app/controllers/application_controller.rb

@@ -535,11 +535,14 @@ class ApplicationController < ActionController::Base
     clean_params = object.param_association_lookup(params)
     clean_params = object.param_cleanup(clean_params, true)
 
-    # save object
-    generic_object.update_attributes!(clean_params)
+    generic_object.with_lock do
 
-    # set relations
-    generic_object.param_set_associations(params)
+      # set attributes
+      generic_object.update_attributes!(clean_params)
+
+      # set relations
+      generic_object.param_set_associations(params)
+    end
 
     if params[:expand]
       render json: generic_object.attributes_with_relation_names, status: :ok

+ 23 - 21
app/controllers/tickets_controller.rb

@@ -114,25 +114,26 @@ class TicketsController < ApplicationController
 
     # create ticket
     ticket.save!
+    ticket.with_lock do
+
+      # create tags if given
+      if params[:tags] && !params[:tags].empty?
+        tags = params[:tags].split(/,/)
+        tags.each { |tag|
+          Tag.tag_add(
+            object: 'Ticket',
+            o_id: ticket.id,
+            item: tag,
+            created_by_id: current_user.id,
+          )
+        }
+      end
 
-    # create tags if given
-    if params[:tags] && !params[:tags].empty?
-      tags = params[:tags].split(/,/)
-      tags.each { |tag|
-        Tag.tag_add(
-          object: 'Ticket',
-          o_id: ticket.id,
-          item: tag,
-          created_by_id: current_user.id,
-        )
-      }
-    end
-
-    # create article if given
-    if params[:article]
-      article_create(ticket, params[:article])
+      # create article if given
+      if params[:article]
+        article_create(ticket, params[:article])
+      end
     end
-
     # create links (e. g. in case of ticket split)
     # links: {
     #   Ticket: {
@@ -191,10 +192,11 @@ class TicketsController < ApplicationController
       }
     end
 
-    ticket.update_attributes!(clean_params)
-
-    if params[:article]
-      article_create(ticket, params[:article])
+    ticket.with_lock do
+      ticket.update_attributes!(clean_params)
+      if params[:article]
+        article_create(ticket, params[:article])
+      end
     end
 
     if params[:expand]

+ 28 - 24
app/controllers/users_controller.rb

@@ -254,29 +254,31 @@ class UsersController < ApplicationController
 
     # permission check
     permission_check_by_permission(params)
-    user.update_attributes(clean_params)
+    user.with_lock do
+      user.update_attributes(clean_params)
 
-    # only allow Admin's
-    if current_user.permissions?('admin.user') && (params[:role_ids] || params[:roles])
-      user.role_ids = params[:role_ids]
-      user.param_set_associations({ role_ids: params[:role_ids], roles: params[:roles] })
-    end
+      # only allow Admin's
+      if current_user.permissions?('admin.user') && (params[:role_ids] || params[:roles])
+        user.role_ids = params[:role_ids]
+        user.param_set_associations({ role_ids: params[:role_ids], roles: params[:roles] })
+      end
 
-    # only allow Admin's
-    if current_user.permissions?('admin.user') && (params[:group_ids] || params[:groups])
-      user.group_ids = params[:group_ids]
-      user.param_set_associations({ group_ids: params[:group_ids], groups: params[:groups] })
-    end
+      # only allow Admin's
+      if current_user.permissions?('admin.user') && (params[:group_ids] || params[:groups])
+        user.group_ids = params[:group_ids]
+        user.param_set_associations({ group_ids: params[:group_ids], groups: params[:groups] })
+      end
 
-    # only allow Admin's and Agent's
-    if current_user.permissions?(['admin.user', 'ticket.agent']) && (params[:organization_ids] || params[:organizations])
-      user.param_set_associations({ organization_ids: params[:organization_ids], organizations: params[:organizations] })
-    end
+      # only allow Admin's and Agent's
+      if current_user.permissions?(['admin.user', 'ticket.agent']) && (params[:organization_ids] || params[:organizations])
+        user.param_set_associations({ organization_ids: params[:organization_ids], organizations: params[:organizations] })
+      end
 
-    if params[:expand]
-      user = User.find(user.id).attributes_with_relation_names
-      render json: user, status: :ok
-      return
+      if params[:expand]
+        user = User.find(user.id).attributes_with_relation_names
+        render json: user, status: :ok
+        return
+      end
     end
 
     # get new data
@@ -704,7 +706,7 @@ curl http://localhost/api/v1/users/password_change.json -v -u #{login}:#{passwor
       render json: { message: 'failed', notice: ['Current password needed!'] }, status: :ok
       return
     end
-    user = User.authenticate( current_user.login, params[:password_old] )
+    user = User.authenticate(current_user.login, params[:password_old])
     if !user
       render json: { message: 'failed', notice: ['Current password is wrong!'] }, status: :ok
       return
@@ -763,10 +765,12 @@ curl http://localhost/api/v1/users/preferences.json -v -u #{login}:#{password} -
 
     if params[:user]
       user = User.find(current_user.id)
-      params[:user].each { |key, value|
-        user.preferences[key.to_sym] = value
-      }
-      user.save
+      user.with_lock do
+        params[:user].each { |key, value|
+          user.preferences[key.to_sym] = value
+        }
+        user.save
+      end
     end
     render json: { message: 'ok' }, status: :ok
   end

+ 43 - 42
app/models/channel/email_parser.rb

@@ -442,10 +442,10 @@ retrns
 
       # get ticket# based on email headers
       if mail[ 'x-zammad-ticket-id'.to_sym ]
-        ticket = Ticket.find_by( id: mail[ 'x-zammad-ticket-id'.to_sym ] )
+        ticket = Ticket.find_by(id: mail[ 'x-zammad-ticket-id'.to_sym ])
       end
       if mail[ 'x-zammad-ticket-number'.to_sym ]
-        ticket = Ticket.find_by( number: mail[ 'x-zammad-ticket-number'.to_sym ] )
+        ticket = Ticket.find_by(number: mail[ 'x-zammad-ticket-number'.to_sym ])
       end
 
       # set ticket state to open if not new
@@ -500,7 +500,6 @@ retrns
           priority_id: Ticket::Priority.find_by(name: '2 normal').id,
           preferences: preferences,
         )
-
         set_attributes_by_x_headers(ticket, 'ticket', mail)
 
         # create ticket
@@ -508,45 +507,47 @@ retrns
       end
 
       # set attributes
-      article = Ticket::Article.new(
-        ticket_id: ticket.id,
-        type_id: Ticket::Article::Type.find_by(name: 'email').id,
-        sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
-        content_type: mail[:content_type],
-        body: mail[:body],
-        from: mail[:from],
-        to: mail[:to],
-        cc: mail[:cc],
-        subject: mail[:subject],
-        message_id: mail[:message_id],
-        internal: false,
-      )
-
-      # x-headers lookup
-      set_attributes_by_x_headers(article, 'article', mail)
-
-      # create article
-      article.save!
-
-      # store mail plain
-      Store.add(
-        object: 'Ticket::Article::Mail',
-        o_id: article.id,
-        data: msg,
-        filename: "ticket-#{ticket.number}-#{article.id}.eml",
-        preferences: {}
-      )
-
-      # store attachments
-      if mail[:attachments]
-        mail[:attachments].each do |attachment|
-          Store.add(
-            object: 'Ticket::Article',
-            o_id: article.id,
-            data: attachment[:data],
-            filename: attachment[:filename],
-            preferences: attachment[:preferences]
-          )
+      ticket.with_lock do
+        article = Ticket::Article.new(
+          ticket_id: ticket.id,
+          type_id: Ticket::Article::Type.find_by(name: 'email').id,
+          sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
+          content_type: mail[:content_type],
+          body: mail[:body],
+          from: mail[:from],
+          to: mail[:to],
+          cc: mail[:cc],
+          subject: mail[:subject],
+          message_id: mail[:message_id],
+          internal: false,
+        )
+
+        # x-headers lookup
+        set_attributes_by_x_headers(article, 'article', mail)
+
+        # create article
+        article.save!
+
+        # store mail plain
+        Store.add(
+          object: 'Ticket::Article::Mail',
+          o_id: article.id,
+          data: msg,
+          filename: "ticket-#{ticket.number}-#{article.id}.eml",
+          preferences: {}
+        )
+
+        # store attachments
+        if mail[:attachments]
+          mail[:attachments].each do |attachment|
+            Store.add(
+              object: 'Ticket::Article',
+              o_id: article.id,
+              data: attachment[:data],
+              filename: attachment[:filename],
+              preferences: attachment[:preferences]
+            )
+          end
         end
       end
     end

+ 15 - 10
lib/sessions.rb

@@ -294,25 +294,30 @@ returns
   def self.send(client_id, data)
     path     = "#{@path}/#{client_id}/"
     filename = "send-#{Time.now.utc.to_f}"
+    location = "#{path}#{filename}"
     check    = true
     count    = 0
     while check
-      if File.exist?(path + filename)
+      if File.exist?(location)
         count += 1
-        filename = "#{filename}-#{count}"
+        location = "#{path}#{filename}-#{count}"
       else
         check = false
       end
     end
     return false if !File.directory? path
-    File.open(path + 'a-' + filename, 'wb') { |file|
-      file.flock(File::LOCK_EX)
-      file.write data.to_json
-      file.flock(File::LOCK_UN)
-      file.close
-    }
-    return false if !File.exist?(path + 'a-' + filename)
-    FileUtils.mv(path + 'a-' + filename, path + filename)
+    begin
+      File.open(location, 'wb') { |file|
+        file.flock(File::LOCK_EX)
+        file.write data.to_json
+        file.flock(File::LOCK_UN)
+        file.close
+      }
+    rescue => e
+      log('error', e.inspect)
+      log('error', "error in writing message file '#{location}'")
+      return false
+    end
     true
   end
 

+ 48 - 12
test/browser_test_helper.rb

@@ -125,7 +125,7 @@ class TestCase < Test::Unit::TestCase
   def screenshot(params)
     instance = params[:browser] || @browser
     comment = params[:comment] || ''
-    filename = "tmp/#{Time.zone.now.strftime('screenshot_%Y_%m_%d__%H_%M_%S')}_#{comment}_#{instance.hash}.png"
+    filename = "tmp/#{Time.zone.now.strftime('screenshot_%Y_%m_%d__%H_%M_%S_%L')}_#{comment}#{instance.hash}.png"
     log('screenshot', { filename: filename })
     instance.save_screenshot(filename)
   end
@@ -415,6 +415,7 @@ class TestCase < Test::Unit::TestCase
     log('click', params)
 
     instance = params[:browser] || @browser
+    screenshot(browser: instance, comment: 'click_before')
     if params[:css]
 
       begin
@@ -501,6 +502,7 @@ class TestCase < Test::Unit::TestCase
 
   modal_disappear(
     browser: browser1,
+    timeout: 12, # default 6
   )
 
 =end
@@ -514,7 +516,7 @@ class TestCase < Test::Unit::TestCase
     watch_for_disappear(
       browser: instance,
       css:     '.modal',
-      timeout: 6,
+      timeout: params[:timeout] || 6,
     )
   end
 
@@ -599,6 +601,7 @@ class TestCase < Test::Unit::TestCase
     log('set', params)
 
     instance = params[:browser] || @browser
+    screenshot(browser: instance, comment: 'set_before')
 
     element = instance.find_elements(css: params[:css])[0]
     if !params[:no_click]
@@ -616,11 +619,24 @@ class TestCase < Test::Unit::TestCase
       }
     end
 
+    # it's not working stable with ff via selenium, use js
+    if browser =~ /firefox/i && params[:css] =~ /\[data-name=/
+      log('set_ff_check', params)
+      value = instance.find_elements(css: params[:css])[0].text
+      if value != params[:value]
+        log('set_ff_check_failed_use_js', params)
+        value_quoted = quote(params[:value])
+        puts "DEBUG $('#{params[:css]}').html('#{value_quoted}').trigger('focusout')"
+        instance.execute_script("$('#{params[:css]}').html('#{value_quoted}').trigger('focusout')")
+      end
+    end
+
     if params[:blur]
       instance.execute_script("$('#{params[:css]}').blur()")
     end
 
     sleep 0.2
+    screenshot(browser: instance, comment: 'set_after')
   end
 
 =begin
@@ -639,6 +655,7 @@ class TestCase < Test::Unit::TestCase
     log('select', params)
 
     instance = params[:browser] || @browser
+    screenshot(browser: instance, comment: 'select_before')
 
     # searchable select
     element = instance.find_elements(css: "#{params[:css]}.js-shadow")[0]
@@ -677,6 +694,7 @@ class TestCase < Test::Unit::TestCase
       #puts "select2 - #{params.inspect}"
     end
     sleep 0.4
+    screenshot(browser: instance, comment: 'select_after')
   end
 
 =begin
@@ -695,6 +713,7 @@ class TestCase < Test::Unit::TestCase
     log('switch', params)
 
     instance = params[:browser] || @browser
+    screenshot(browser: instance, comment: 'switch_before')
 
     element = instance.find_elements(css: "#{params[:css]} input[type=checkbox]")[0]
     checked = element.attribute('checked')
@@ -720,6 +739,7 @@ class TestCase < Test::Unit::TestCase
         raise 'Switch not off!' if checked
       end
     end
+    screenshot(browser: instance, comment: 'switch_after')
   end
 
 =begin
@@ -736,11 +756,13 @@ class TestCase < Test::Unit::TestCase
     log('check', params)
 
     instance = params[:browser] || @browser
+    screenshot(browser: instance, comment: 'check_before')
 
     instance.execute_script("if (!$('#{params[:css]}').prop('checked')) { $('#{params[:css]}').click() }")
     #element = instance.find_elements(css: params[:css])[0]
     #checked = element.attribute('checked')
     #element.click if !checked
+    screenshot(browser: instance, comment: 'check_after')
   end
 
 =begin
@@ -757,11 +779,13 @@ class TestCase < Test::Unit::TestCase
     log('uncheck', params)
 
     instance = params[:browser] || @browser
+    screenshot(browser: instance, comment: 'uncheck_before')
 
     instance.execute_script("if ($('#{params[:css]}').prop('checked')) { $('#{params[:css]}').click() }")
     #element = instance.find_elements(css: params[:css])[0]
     #checked = element.attribute('checked')
     #element.click if checked
+    screenshot(browser: instance, comment: 'uncheck_after')
   end
 
 =begin
@@ -779,10 +803,12 @@ class TestCase < Test::Unit::TestCase
     log('sendkey', params)
 
     instance = params[:browser] || @browser
+    screenshot(browser: instance, comment: 'sendkey_before')
     if params[:value].class == Array
       params[:value].each { |key|
         instance.action.send_keys(key).perform
       }
+      screenshot(browser: instance, comment: 'sendkey_after')
       return
     end
     instance.action.send_keys(params[:value]).perform
@@ -791,6 +817,7 @@ class TestCase < Test::Unit::TestCase
     else
       sleep 0.2
     end
+    screenshot(browser: instance, comment: 'sendkey_after')
   end
 
 =begin
@@ -825,9 +852,11 @@ class TestCase < Test::Unit::TestCase
       end
       if params[:should_not_match]
         if success
+          screenshot(browser: instance, comment: 'match_failed')
           raise "should not match '#{params[:value]}' in select list, but is matching"
         end
       elsif !success
+        screenshot(browser: instance, comment: 'match_failed')
         raise "not matching '#{params[:value]}' in select list"
       end
 
@@ -871,9 +900,11 @@ class TestCase < Test::Unit::TestCase
 
     if match
       if params[:should_not_match]
+        screenshot(browser: instance, comment: 'match_failed')
         raise "matching '#{params[:value]}' in content '#{text}' but should not!"
       end
     elsif !params[:should_not_match]
+      screenshot(browser: instance, comment: 'match_failed')
       raise "not matching '#{params[:value]}' in content '#{text}' but should!"
     end
     sleep 0.2
@@ -1032,10 +1063,11 @@ class TestCase < Test::Unit::TestCase
         if title =~ /#{data[:title]}/i
           assert(true, "matching '#{data[:title]}' in title '#{title}'")
         else
+          screenshot(browser: instance, comment: 'verify_task_failed')
           raise "not matching '#{data[:title]}' in title '#{title}'"
         end
       end
-      puts "tv #{params.inspect}"
+
       # verify modified
       if data.key?(:modified)
         exists      = instance.find_elements(css: '.tasks .is-active')[0]
@@ -1051,15 +1083,19 @@ class TestCase < Test::Unit::TestCase
           if is_modified
             assert(true, "task '#{data[:title]}' is modifed")
           elsif !exists
+            screenshot(browser: instance, comment: 'verify_task_failed')
             raise "task '#{data[:title]}' not exists, should not modified"
           else
+            screenshot(browser: instance, comment: 'verify_task_failed')
             raise "task '#{data[:title]}' is not modifed"
           end
         elsif !is_modified
           assert(true, "task '#{data[:title]}' is modifed")
         elsif !exists
+          screenshot(browser: instance, comment: 'verify_task_failed')
           raise "task '#{data[:title]}' not exists, should be not modified"
         else
+          screenshot(browser: instance, comment: 'verify_task_failed')
           raise "task '#{data[:title]}' is modifed, but should not"
         end
       end
@@ -1285,12 +1321,14 @@ wait untill text in selector disabppears
     switch_window_focus(params)
     log('shortcut', params)
     instance = params[:browser] || @browser
+    screenshot(browser: instance, comment: 'shortcut_before')
     instance.action.key_down(:control)
             .key_down(:alt)
             .send_keys(params[:key])
             .key_up(:alt)
             .key_up(:control)
             .perform
+    screenshot(browser: instance, comment: 'shortcut_after')
   end
 
 =begin
@@ -1829,14 +1867,6 @@ wait untill text in selector disabppears
         clear:    true,
         mute_log: true,
       )
-
-      # it's not working stable via selenium, use js
-      value = instance.find_elements(css: '.content .newTicket div[data-name=body]')[0].text
-      #puts "V #{value.inspect}"
-      if value != data[:body]
-        body_quoted = quote(data[:body])
-        instance.execute_script("$('.content.active div[data-name=body]').html('#{body_quoted}').trigger('focusout')")
-      end
     end
     if data[:customer]
       element = instance.find_elements(css: '.active .newTicket input[name="customer_id_completion"]')[0]
@@ -2260,12 +2290,14 @@ wait untill text in selector disabppears
       browser: instance,
       js: '$(".content.active .sidebar").css("display", "block")',
     )
+    screenshot(browser: instance, comment: 'ticket_open_by_overview')
     instance.find_elements(css: ".content.active .sidebar a[href=\"#{params[:link]}\"]")[0].click
     sleep 1
     execute(
       browser: instance,
       js: '$(".content.active .sidebar").css("display", "none")',
     )
+    screenshot(browser: instance, comment: 'ticket_open_by_overview_search')
     instance.find_elements(partial_link_text: params[:number])[0].click
     sleep 1
     number = instance.find_elements(css: '.active .ticketZoom-header .ticket-number')[0].text
@@ -2310,8 +2342,9 @@ wait untill text in selector disabppears
     sleep 1
 
     # open ticket
+    screenshot(browser: instance, comment: 'ticket_open_by_search')
     #instance.find_element(partial_link_text: params[:number] } ).click
-    instance.execute_script("$(\".js-global-search-result a:contains('#{params[:value]}') .nav-tab-icon\").click()")
+    instance.execute_script("$(\".js-global-search-result a:contains('#{params[:number]}') .nav-tab-icon\").first().click()")
     sleep 1
     number = instance.find_elements(css: '.active .ticketZoom-header .ticket-number')[0].text
     if number !~ /#{params[:number]}/
@@ -2345,6 +2378,7 @@ wait untill text in selector disabppears
     sleep 3
 
     # open ticket
+    screenshot(browser: instance, comment: 'ticket_open_by_title_search')
     #instance.find_element(partial_link_text: params[:title] } ).click
     instance.execute_script("$(\".js-global-search-result a:contains('#{params[:title]}') .nav-tab-icon\").click()")
     sleep 1
@@ -2466,6 +2500,8 @@ wait untill text in selector disabppears
     element.clear
     element.send_keys(params[:value])
     sleep 3
+
+    screenshot(browser: instance, comment: 'user_open_by_search')
     #instance.find_element(partial_link_text: params[:value]).click
     instance.execute_script("$(\".js-global-search-result a:contains('#{params[:value]}') .nav-tab-icon\").click()")
     sleep 1