Browse Source

Migrated remaining Minitest controller tests to RSpec.

Rolf Schmidt 6 years ago
parent
commit
14437f92f9

+ 0 - 50
.gitlab-ci.yml

@@ -91,9 +91,7 @@ test:rspec:postgresql:
     - rake zammad:db:init
     - rake test:units
     - ruby -I test/ test/integration/object_manager_test.rb
-    - ruby -I test/ test/integration/object_manager_attributes_controller_test.rb
     - ruby -I test/ test/integration/package_test.rb
-    - ruby -I test/ test/integration/monitoring_controller_test.rb
     - rake db:drop
 
 test:unit:mysql:
@@ -168,18 +166,6 @@ test:integration:user_agent:
     - rake db:drop
   allow_failure: true
 
-test:integration:user_device:
-  <<: *artifacts_error
-  stage: test
-  variables:
-    RAILS_ENV: "test"
-  tags:
-    - core
-  script:
-    - rake zammad:db:unseeded
-    - ruby -I test/ test/integration/user_device_controller_test.rb
-    - rake db:drop
-
 test:integration:slack:
   <<: *artifacts_error
   stage: test
@@ -207,42 +193,6 @@ test:integration:clearbit:
     - rake db:drop
   allow_failure: true
 
-test:integration:telegram:
-  <<: *artifacts_error
-  stage: test
-  variables:
-    RAILS_ENV: "test"
-  tags:
-    - core
-  script:
-    - rake zammad:db:init
-    - ruby -I test test/integration/telegram_controller_test.rb
-    - rake db:drop
-
-test:integration:twilio:
-  <<: *artifacts_error
-  stage: test
-  variables:
-    RAILS_ENV: "test"
-  tags:
-    - core
-  script:
-    - rake zammad:db:init
-    - ruby -I test test/integration/twilio_sms_controller_test.rb
-    - rake db:drop
-
-test:integration:idoit:
-  <<: *artifacts_error
-  stage: test
-  variables:
-    RAILS_ENV: "test"
-  tags:
-    - core
-  script:
-    - rake zammad:db:init
-    - ruby -I test test/integration/idoit_controller_test.rb
-    - rake db:drop
-
 ### Elasticsearch
 
 .script_elasticsearch_template: &script_elasticsearch_definition

+ 1 - 1
.rubocop.yml

@@ -193,7 +193,7 @@ Lint/BooleanSymbol:
   Enabled: true
   Exclude:
     - "db/seeds/object_manager_attributes.rb"
-    - "test/integration/object_manager_attributes_controller_test.rb"
+    - "spec/requests/integration/object_manager_attributes_spec.rb"
     - "test/integration/object_manager_test.rb"
 
 Lint/InterpolationCheck:

+ 18 - 0
spec/factories/user_device.rb

@@ -0,0 +1,18 @@
+FactoryBot.define do
+  sequence :fingerprint do |n|
+    "fingerprint#{n}"
+  end
+end
+
+FactoryBot.define do
+
+  factory :user_device do
+    user_id 1
+    name 'test 1'
+    location 'some location'
+    user_agent 'some user agent'
+    ip '127.0.0.1'
+    fingerprint { generate(:fingerprint) }
+  end
+
+end

+ 154 - 0
spec/requests/integration/idoit_spec.rb

@@ -0,0 +1,154 @@
+require 'rails_helper'
+
+RSpec.describe 'Idoit', type: :request do
+
+  let!(:admin_user) do
+    create(:admin_user, groups: Group.all)
+  end
+  let!(:agent_user) do
+    create(:agent_user, groups: Group.all)
+  end
+  let!(:customer_user) do
+    create(:customer_user)
+  end
+  let!(:token) do
+    'some_token'
+  end
+  let!(:endpoint) do
+    'https://idoit.example.com/i-doit/'
+  end
+
+  before(:each) do
+    Setting.set('idoit_integration', true)
+    Setting.set('idoit_config', {
+                  api_token: token,
+                  endpoint: endpoint,
+                  client_id: '',
+                })
+  end
+
+  describe 'request handling' do
+
+    it 'does unclear urls' do
+
+      params = {
+        api_token: token,
+        endpoint: endpoint,
+        client_id: '',
+      }
+      authenticated_as(agent_user)
+      post '/api/v1/integration/idoit/verify', params: params, as: :json
+      expect(response).to have_http_status(401)
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response).to_not be_blank
+      expect(json_response['error']).to eq('Not authorized (user)!')
+
+      stub_request(:post, "#{endpoint}src/jsonrpc.php")
+        .with(body: "{\"method\":\"cmdb.object_types\",\"params\":{\"apikey\":\"#{token}\"},\"version\":\"2.0\"}")
+        .to_return(status: 200, body: read_message('object_types_response'), headers: {})
+
+      params = {
+        api_token: token,
+        endpoint: endpoint,
+        client_id: '',
+      }
+      authenticated_as(admin_user)
+      post '/api/v1/integration/idoit/verify', params: params, as: :json
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response).to_not be_blank
+      expect(json_response['result']).to eq('ok')
+      expect(json_response['response']).to be_truthy
+      expect(json_response['response']['jsonrpc']).to eq('2.0')
+      expect(json_response['response']['result']).to be_truthy
+
+      params = {
+        api_token: token,
+        endpoint: " #{endpoint}/",
+        client_id: '',
+      }
+      post '/api/v1/integration/idoit/verify', params: params, as: :json
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response).to_not be_blank
+      expect(json_response['result']).to eq('ok')
+      expect(json_response['response']).to be_truthy
+      expect(json_response['response']['jsonrpc']).to eq('2.0')
+      expect(json_response['response']['result']).to be_truthy
+
+    end
+
+    it 'does list all object types' do
+
+      stub_request(:post, "#{endpoint}src/jsonrpc.php")
+        .with(body: "{\"method\":\"cmdb.object_types\",\"params\":{\"apikey\":\"#{token}\"},\"version\":\"2.0\"}")
+        .to_return(status: 200, body: read_message('object_types_response'), headers: {})
+
+      params = {
+        method: 'cmdb.object_types',
+      }
+      authenticated_as(agent_user)
+      post '/api/v1/integration/idoit', params: params, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response).to_not be_blank
+      expect(json_response['result']).to eq('ok')
+      expect(json_response['response']).to be_truthy
+      expect(json_response['response']['jsonrpc']).to eq('2.0')
+      expect(json_response['response']['result']).to be_truthy
+      expect(json_response['response']['result'][0]['id']).to eq('1')
+      expect(json_response['response']['result'][0]['title']).to eq('System service')
+
+      params = {
+        method: 'cmdb.object_types',
+      }
+      authenticated_as(admin_user)
+      post '/api/v1/integration/idoit', params: params, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response).to_not be_blank
+      expect(json_response['result']).to eq('ok')
+      expect(json_response['response']).to be_truthy
+      expect(json_response['response']['jsonrpc']).to eq('2.0')
+      expect(json_response['response']['result']).to be_truthy
+      expect(json_response['response']['result'][0]['id']).to eq('1')
+      expect(json_response['response']['result'][0]['title']).to eq('System service')
+
+    end
+
+    it 'does query objects' do
+
+      stub_request(:post, "#{endpoint}src/jsonrpc.php")
+        .with(body: "{\"method\":\"cmdb.objects\",\"params\":{\"apikey\":\"#{token}\",\"filter\":{\"ids\":[\"33\"]}},\"version\":\"2.0\"}")
+        .to_return(status: 200, body: read_message('object_types_filter_response'), headers: {})
+
+      params = {
+        method: 'cmdb.objects',
+        filter: {
+          ids: ['33']
+        },
+      }
+      authenticated_as(agent_user)
+      post '/api/v1/integration/idoit', params: params, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response).to_not be_blank
+      expect(json_response['result']).to eq('ok')
+      expect(json_response['response']).to be_truthy
+      expect(json_response['response']['jsonrpc']).to eq('2.0')
+      expect(json_response['response']['result']).to be_truthy
+      expect(json_response['response']['result'][0]['id']).to eq('26')
+      expect(json_response['response']['result'][0]['title']).to eq('demo.example.com')
+      expect(json_response['response']['result'][0]['type_title']).to eq('Virtual server')
+      expect(json_response['response']['result'][0]['cmdb_status_title']).to eq('in operation')
+
+    end
+
+    def read_message(file)
+      File.read(Rails.root.join('test', 'data', 'idoit', "#{file}.json"))
+    end
+  end
+end

+ 680 - 0
spec/requests/integration/monitoring_spec.rb

@@ -0,0 +1,680 @@
+require 'rails_helper'
+
+RSpec.describe 'Monitoring', type: :request do
+
+  let!(:admin_user) do
+    create(:admin_user, groups: Group.all)
+  end
+  let!(:agent_user) do
+    create(:agent_user, groups: Group.all)
+  end
+  let!(:customer_user) do
+    create(:customer_user)
+  end
+  let!(:token) do
+    SecureRandom.urlsafe_base64(64)
+  end
+
+  before(:each) do
+    Setting.set('monitoring_token', token)
+
+    # channel cleanup
+    Channel.where.not(area: 'Email::Notification').destroy_all
+    Channel.all.each do |channel|
+      channel.status_in  = 'ok'
+      channel.status_out = 'ok'
+      channel.last_log_in = nil
+      channel.last_log_out = nil
+      channel.save!
+    end
+    dir = Rails.root.join('tmp', 'unprocessable_mail')
+    Dir.glob("#{dir}/*.eml") do |entry|
+      File.delete(entry)
+    end
+
+    Scheduler.where(active: true).each do |scheduler|
+      scheduler.last_run = Time.zone.now
+      scheduler.save!
+    end
+
+    permission = Permission.find_by(name: 'admin.monitoring')
+    permission.active = true
+    permission.save!
+  end
+
+  describe 'request handling' do
+
+    it 'does monitoring without token' do
+
+      # health_check
+      get '/api/v1/monitoring/health_check', params: {}, as: :json
+      expect(response).to have_http_status(401)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['healthy']).to be_falsey
+      expect(json_response['error']).to eq('Not authorized')
+
+      # status
+      get '/api/v1/monitoring/status', params: {}, as: :json
+      expect(response).to have_http_status(401)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['agents']).to be_falsey
+      expect(json_response['last_login']).to be_falsey
+      expect(json_response['counts']).to be_falsey
+      expect(json_response['last_created_at']).to be_falsey
+      expect(json_response['error']).to eq('Not authorized')
+
+      # token
+      post '/api/v1/monitoring/token', params: {}, as: :json
+      expect(response).to have_http_status(401)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['token']).to be_falsey
+      expect(json_response['error']).to eq('authentication failed')
+
+    end
+
+    it 'does monitoring with wrong token' do
+
+      # health_check
+      get '/api/v1/monitoring/health_check?token=abc', params: {}, as: :json
+      expect(response).to have_http_status(401)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['healthy']).to be_falsey
+      expect(json_response['error']).to eq('Not authorized')
+
+      # status
+      get '/api/v1/monitoring/status?token=abc', params: {}, as: :json
+      expect(response).to have_http_status(401)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['agents']).to be_falsey
+      expect(json_response['last_login']).to be_falsey
+      expect(json_response['counts']).to be_falsey
+      expect(json_response['last_created_at']).to be_falsey
+      expect(json_response['error']).to eq('Not authorized')
+
+      # token
+      post '/api/v1/monitoring/token', params: { token: 'abc' }, as: :json
+      expect(response).to have_http_status(401)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['token']).to be_falsey
+      expect(json_response['error']).to eq('authentication failed')
+
+    end
+
+    it 'does monitoring with correct token' do
+
+      # test storage usage
+      string = ''
+      10.times do
+        string += 'Some Text Some Text Some Text Some Text Some Text Some Text Some Text Some Text'
+      end
+      Store.add(
+        object: 'User',
+        o_id: 1,
+        data: string,
+        filename: 'filename.txt',
+        created_by_id: 1,
+      )
+
+      # health_check
+      get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['error']).to be_falsey
+      expect(json_response['healthy']).to eq(true)
+      expect(json_response['message']).to eq('success')
+
+      # status
+      get "/api/v1/monitoring/status?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['error']).to be_falsey
+      expect(json_response.key?('agents')).to be_truthy
+      expect(json_response.key?('last_login')).to be_truthy
+      expect(json_response.key?('counts')).to be_truthy
+      expect(json_response.key?('last_created_at')).to be_truthy
+
+      if ActiveRecord::Base.connection_config[:adapter] == 'postgresql'
+        expect(json_response['storage']).to be_truthy
+        expect(json_response['storage'].key?('kB')).to be_truthy
+        expect(json_response['storage'].key?('MB')).to be_truthy
+        expect(json_response['storage'].key?('GB')).to be_truthy
+      else
+        expect(json_response['storage']).to be_falsey
+      end
+
+      # token
+      post '/api/v1/monitoring/token', params: { token: token }, as: :json
+      expect(response).to have_http_status(401)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['token']).to be_falsey
+      expect(json_response['error']).to eq('authentication failed')
+
+    end
+
+    it 'does monitoring with admin user' do
+
+      # health_check
+      authenticated_as(admin_user)
+      get '/api/v1/monitoring/health_check', params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['error']).to be_falsey
+      expect(json_response['healthy']).to eq(true)
+      expect(json_response['message']).to eq('success')
+
+      # status
+      get '/api/v1/monitoring/status', params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['error']).to be_falsey
+      expect(json_response.key?('agents')).to be_truthy
+      expect(json_response.key?('last_login')).to be_truthy
+      expect(json_response.key?('counts')).to be_truthy
+      expect(json_response.key?('last_created_at')).to be_truthy
+
+      # token
+      post '/api/v1/monitoring/token', params: { token: token }, as: :json
+      expect(response).to have_http_status(201)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['token']).to be_truthy
+      token = json_response['token']
+      expect(json_response['error']).to be_falsey
+
+    end
+
+    it 'does monitoring with agent user' do
+
+      # health_check
+      authenticated_as(agent_user)
+      get '/api/v1/monitoring/health_check', params: {}, as: :json
+      expect(response).to have_http_status(401)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['healthy']).to be_falsey
+      expect(json_response['error']).to eq('Not authorized (user)!')
+
+      # status
+      get '/api/v1/monitoring/status', params: {}, as: :json
+      expect(response).to have_http_status(401)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['agents']).to be_falsey
+      expect(json_response['last_login']).to be_falsey
+      expect(json_response['counts']).to be_falsey
+      expect(json_response['last_created_at']).to be_falsey
+      expect(json_response['error']).to eq('Not authorized (user)!')
+
+      # token
+      post '/api/v1/monitoring/token', params: { token: token }, as: :json
+      expect(response).to have_http_status(401)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['token']).to be_falsey
+      expect(json_response['error']).to eq('Not authorized (user)!')
+
+    end
+
+    it 'does monitoring with admin user and invalid permission' do
+
+      permission = Permission.find_by(name: 'admin.monitoring')
+      permission.active = false
+      permission.save!
+
+      # health_check
+      authenticated_as(admin_user)
+      get '/api/v1/monitoring/health_check', params: {}, as: :json
+      expect(response).to have_http_status(401)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['healthy']).to be_falsey
+      expect(json_response['error']).to eq('Not authorized (user)!')
+
+      # status
+      get '/api/v1/monitoring/status', params: {}, as: :json
+      expect(response).to have_http_status(401)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['agents']).to be_falsey
+      expect(json_response['last_login']).to be_falsey
+      expect(json_response['counts']).to be_falsey
+      expect(json_response['last_created_at']).to be_falsey
+      expect(json_response['error']).to eq('Not authorized (user)!')
+
+      # token
+      post '/api/v1/monitoring/token', params: { token: token }, as: :json
+      expect(response).to have_http_status(401)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['token']).to be_falsey
+      expect(json_response['error']).to eq('Not authorized (user)!')
+
+      permission.active = true
+      permission.save!
+    end
+
+    it 'does monitoring with correct token and invalid permission' do
+
+      permission = Permission.find_by(name: 'admin.monitoring')
+      permission.active = false
+      permission.save!
+
+      # health_check
+      get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['error']).to be_falsey
+      expect(json_response['healthy']).to eq(true)
+      expect(json_response['message']).to eq('success')
+
+      # status
+      get "/api/v1/monitoring/status?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['error']).to be_falsey
+      expect(json_response.key?('agents')).to be_truthy
+      expect(json_response.key?('last_login')).to be_truthy
+      expect(json_response.key?('counts')).to be_truthy
+      expect(json_response.key?('last_created_at')).to be_truthy
+
+      # token
+      post '/api/v1/monitoring/token', params: { token: token }, as: :json
+      expect(response).to have_http_status(401)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['token']).to be_falsey
+      expect(json_response['error']).to eq('authentication failed')
+
+      permission.active = true
+      permission.save!
+
+    end
+
+    it 'does check health false' do
+
+      channel = Channel.find_by(active: true)
+      channel.status_in  = 'ok'
+      channel.status_out = 'error'
+      channel.last_log_in = nil
+      channel.last_log_out = nil
+      channel.save!
+
+      # health_check - channel
+      get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['message']).to be_truthy
+      expect(json_response['issues']).to be_truthy
+      expect(json_response['healthy']).to eq(false)
+      expect(json_response['message']).to eq('Channel: Email::Notification out  ')
+
+      # health_check - scheduler may not run
+      scheduler = Scheduler.where(active: true).last
+      scheduler.last_run = Time.zone.now - 20.minutes
+      scheduler.period = 600
+      scheduler.save!
+
+      get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['message']).to be_truthy
+      expect(json_response['issues']).to be_truthy
+      expect(json_response['healthy']).to eq(false)
+      expect(json_response['message']).to eq("Channel: Email::Notification out  ;scheduler may not run (last execution of #{scheduler.method} 10 minutes over) - please contact your system administrator")
+
+      # health_check - scheduler may not run
+      scheduler = Scheduler.where(active: true).last
+      scheduler.last_run = Time.zone.now - 1.day
+      scheduler.period = 600
+      scheduler.save!
+
+      get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['message']).to be_truthy
+      expect(json_response['issues']).to be_truthy
+      expect(json_response['healthy']).to eq(false)
+      expect(json_response['message']).to eq("Channel: Email::Notification out  ;scheduler may not run (last execution of #{scheduler.method} about 24 hours over) - please contact your system administrator")
+
+      # health_check - scheduler job count
+      travel 2.seconds
+      8001.times do
+        Delayed::Job.enqueue( BackgroundJobSearchIndex.new('Ticket', 1))
+      end
+      Scheduler.where(active: true).each do |local_scheduler|
+        local_scheduler.last_run = Time.zone.now
+        local_scheduler.save!
+      end
+      total_jobs = Delayed::Job.count
+
+      get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['message']).to be_truthy
+      expect(json_response['issues']).to be_truthy
+      expect(json_response['healthy']).to eq(false)
+      expect(json_response['message']).to eq('Channel: Email::Notification out  ')
+
+      travel 20.minutes
+      Scheduler.where(active: true).each do |local_scheduler|
+        local_scheduler.last_run = Time.zone.now
+        local_scheduler.save!
+      end
+
+      get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['message']).to be_truthy
+      expect(json_response['issues']).to be_truthy
+      expect(json_response['healthy']).to eq(false)
+      expect(json_response['message']).to eq("Channel: Email::Notification out  ;#{total_jobs} background jobs in queue")
+
+      Delayed::Job.delete_all
+      travel_back
+
+      # health_check - unprocessable mail
+      dir = Rails.root.join('tmp', 'unprocessable_mail')
+      FileUtils.mkdir_p(dir)
+      FileUtils.touch("#{dir}/test.eml")
+
+      get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['message']).to be_truthy
+      expect(json_response['issues']).to be_truthy
+      expect(json_response['healthy']).to eq(false)
+      expect(json_response['message']).to eq('Channel: Email::Notification out  ;unprocessable mails: 1')
+
+      # health_check - ldap
+      Setting.set('ldap_integration', true)
+      ImportJob.create(
+        name:        'Import::Ldap',
+        started_at:  Time.zone.now,
+        finished_at: Time.zone.now,
+        result:      {
+          error: 'Some bad error'
+        }
+      )
+
+      get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['message']).to be_truthy
+      expect(json_response['issues']).to be_truthy
+      expect(json_response['healthy']).to eq(false)
+      expect(json_response['message']).to eq("Channel: Email::Notification out  ;unprocessable mails: 1;Failed to run import backend 'Import::Ldap'. Cause: Some bad error")
+
+      stuck_updated_at_timestamp = 15.minutes.ago
+      ImportJob.create(
+        name:        'Import::Ldap',
+        started_at:  Time.zone.now,
+        finished_at: nil,
+        updated_at:  stuck_updated_at_timestamp,
+      )
+
+      # health_check
+      get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['message']).to be_truthy
+      expect(json_response['issues']).to be_truthy
+      expect(json_response['healthy']).to eq(false)
+      expect(json_response['message']).to eq("Channel: Email::Notification out  ;unprocessable mails: 1;Failed to run import backend 'Import::Ldap'. Cause: Some bad error;Stuck import backend 'Import::Ldap' detected. Last update: #{stuck_updated_at_timestamp}")
+
+      Setting.set('ldap_integration', false)
+    end
+
+    it 'does check restart_failed_jobs' do
+      authenticated_as(admin_user)
+      post '/api/v1/monitoring/restart_failed_jobs', params: {}, as: :json
+      expect(response).to have_http_status(200)
+    end
+
+    it 'does check failed delayed job', db_strategy: :reset do
+
+      # disable elasticsearch
+      prev_es_config = Setting.get('es_url')
+      Setting.set('es_url', 'http://127.0.0.1:92001')
+
+      # add a new object
+      object = create(:object_manager_attribute_text)
+
+      migration = ObjectManager::Attribute.migration_execute
+      expect(true).to eq(migration)
+
+      authenticated_as(admin_user)
+      post "/api/v1/object_manager_attributes/#{object.id}", params: {}, as: :json
+      token = @response.headers['CSRF-TOKEN']
+
+      # parameters for updating
+      params = {
+        'name': 'test4',
+        'object': 'Ticket',
+        'display': 'Test 4',
+        'active': true,
+        'data_type': 'input',
+        'data_option': {
+          'default': 'test',
+          'type': 'text',
+          'maxlength': 120
+        },
+        'screens': {
+          'create_middle': {
+            'ticket.customer': {
+              'shown': true,
+              'item_class': 'column'
+            },
+            'ticket.agent': {
+              'shown': true,
+              'item_class': 'column'
+            }
+          },
+          'edit': {
+            'ticket.customer': {
+              'shown': true
+            },
+            'ticket.agent': {
+              'shown': true
+            }
+          }
+        },
+        'id': 'c-196'
+      }
+
+      # update the object
+      put "/api/v1/object_manager_attributes/#{object.id}", params: params, as: :json
+
+      migration = ObjectManager::Attribute.migration_execute
+      expect(true).to eq(migration)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_truthy
+      expect(json_response['data_option']['null']).to be_truthy
+      expect('test4').to eq(json_response['name'])
+      expect('Test 4').to eq(json_response['display'])
+
+      jobs = Delayed::Job.all
+
+      4.times do
+        jobs.each do |job|
+          Delayed::Worker.new.run(job)
+        end
+      end
+
+      # health_check
+      get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['message']).to be_truthy
+      expect(json_response['issues']).to be_truthy
+      expect(json_response['healthy']).to eq(false)
+      expect( json_response['message']).to eq("Failed to run background job #1 'BackgroundJobSearchIndex' 1 time(s) with 4 attempt(s).")
+
+      # add another job
+      manual_added = Delayed::Job.enqueue( BackgroundJobSearchIndex.new('Ticket', 1))
+      manual_added.update!(attempts: 10)
+
+      # health_check
+      get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['message']).to be_truthy
+      expect(json_response['issues']).to be_truthy
+      expect(json_response['healthy']).to eq(false)
+      expect( json_response['message']).to eq("Failed to run background job #1 'BackgroundJobSearchIndex' 2 time(s) with 14 attempt(s).")
+
+      # add another job
+      dummy_class = Class.new do
+
+        def perform
+          puts 'work work'
+        end
+      end
+
+      manual_added = Delayed::Job.enqueue( dummy_class.new )
+      manual_added.update!(attempts: 5)
+
+      # health_check
+      get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['message']).to be_truthy
+      expect(json_response['issues']).to be_truthy
+      expect(json_response['healthy']).to eq(false)
+      expect( json_response['message']).to eq("Failed to run background job #1 'BackgroundJobSearchIndex' 2 time(s) with 14 attempt(s).;Failed to run background job #2 'Object' 1 time(s) with 5 attempt(s).")
+
+      # reset settings
+      Setting.set('es_url', prev_es_config)
+
+      # add some more failing job
+      10.times do
+        manual_added = Delayed::Job.enqueue( dummy_class.new )
+        manual_added.update!(attempts: 5)
+      end
+
+      # health_check
+      get "/api/v1/monitoring/health_check?token=#{token}", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['message']).to be_truthy
+      expect(json_response['issues']).to be_truthy
+      expect(json_response['healthy']).to eq(false)
+      expect( json_response['message']).to eq("13 failing background jobs;Failed to run background job #1 'Object' 8 time(s) with 40 attempt(s).;Failed to run background job #2 'BackgroundJobSearchIndex' 2 time(s) with 14 attempt(s).")
+
+      # cleanup
+      Delayed::Job.delete_all
+    end
+
+    it 'does check amount' do
+      Ticket.destroy_all
+
+      # amount_check - ok
+      get "/api/v1/monitoring/amount_check?token=#{token}&periode=1h", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['state']).to eq('ok')
+      expect(json_response['message']).to eq('')
+      expect(json_response['count']).to eq(0)
+
+      Ticket.destroy_all
+      (1..6).each do |i|
+        create(:ticket, title: "Ticket-#{i}")
+        travel 10.seconds
+      end
+
+      get "/api/v1/monitoring/amount_check?token=#{token}&periode=1h&min_warning=10&min_critical=8", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['state']).to eq('critical')
+      expect(json_response['message']).to eq('The minimum of 8 was undercut by 6 in the last 1h')
+      expect(json_response['count']).to eq(6)
+
+      get "/api/v1/monitoring/amount_check?token=#{token}&periode=1h&min_warning=7&min_critical=2", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['state']).to eq('warning')
+      expect(json_response['message']).to eq('The minimum of 7 was undercut by 6 in the last 1h')
+      expect(json_response['count']).to eq(6)
+
+      get "/api/v1/monitoring/amount_check?token=#{token}&periode=1h&max_warning=10&max_critical=20", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['state']).to eq('ok')
+      expect(json_response['message']).to eq('')
+      expect(json_response['count']).to eq(6)
+
+      (1..6).each do |i|
+        create(:ticket, title: "Ticket-#{i}")
+        travel 1.second
+      end
+
+      get "/api/v1/monitoring/amount_check?token=#{token}&periode=1h&max_warning=10&max_critical=20", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['state']).to eq('warning')
+      expect(json_response['message']).to eq('The limit of 10 was exceeded with 12 in the last 1h')
+      expect(json_response['count']).to eq(12)
+
+      (1..10).each do |i|
+        create(:ticket, title: "Ticket-#{i}")
+        travel 1.second
+      end
+
+      get "/api/v1/monitoring/amount_check?token=#{token}&periode=1h&max_warning=10&max_critical=20", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['state']).to eq('critical')
+      expect(json_response['message']).to eq('The limit of 20 was exceeded with 22 in the last 1h')
+      expect(json_response['count']).to eq(22)
+
+      get "/api/v1/monitoring/amount_check?token=#{token}&periode=1h", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['state']).to eq('ok')
+      expect(json_response['message']).to eq('')
+      expect(json_response['count']).to eq(22)
+
+      travel 2.hours
+
+      get "/api/v1/monitoring/amount_check?token=#{token}&periode=1h", params: {}, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['state']).to eq('ok')
+      expect(json_response['message']).to eq('')
+      expect(json_response['count']).to eq(0)
+    end
+  end
+end

+ 1029 - 0
spec/requests/integration/object_manager_attributes_spec.rb

@@ -0,0 +1,1029 @@
+require 'rails_helper'
+
+RSpec.describe 'ObjectManager Attributes', type: :request do
+
+  let(:admin_user) do
+    create(:admin_user)
+  end
+
+  describe 'request handling' do
+
+    it 'does add new ticket text object' do
+      authenticated_as(admin_user)
+      post '/api/v1/object_manager_attributes', params: {}, as: :json
+      token = response.headers['CSRF-TOKEN']
+
+      # token based on headers
+      params = {
+        'name': 'test1',
+        'object': 'Ticket',
+        'display': 'Test 1',
+        'active': true,
+        'data_type': 'input',
+        'data_option': {
+          'default': 'test',
+          'type': 'text',
+          'maxlength': 120
+        },
+        'screens': {
+          'create_middle': {
+            'ticket.customer': {
+              'shown': true,
+              'item_class': 'column'
+            },
+            'ticket.agent': {
+              'shown': true,
+              'item_class': 'column'
+            }
+          },
+          'edit': {
+            'ticket.customer': {
+              'shown': true
+            },
+            'ticket.agent': {
+              'shown': true
+            }
+          }
+        },
+        'id': 'c-196'
+      }
+
+      post '/api/v1/object_manager_attributes', params: params, as: :json
+      expect(response).to have_http_status(201)
+      expect(json_response).to be_truthy
+      expect(json_response['data_option']['null']).to be_truthy
+      expect(json_response['data_option']['null']).to eq(true)
+      expect(json_response['name']).to eq('test1')
+    end
+
+    it 'does add new ticket text object - no default' do
+      authenticated_as(admin_user)
+      post '/api/v1/object_manager_attributes', params: {}, as: :json
+      token = response.headers['CSRF-TOKEN']
+
+      # token based on headers
+      params = {
+        'name': 'test2',
+        'object': 'Ticket',
+        'display': 'Test 2',
+        'active': true,
+        'data_type': 'input',
+        'data_option': {
+          'type': 'text',
+          'maxlength': 120
+        },
+        'screens': {
+          'create_middle': {
+            'ticket.customer': {
+              'shown': true,
+              'item_class': 'column'
+            },
+            'ticket.agent': {
+              'shown': true,
+              'item_class': 'column'
+            }
+          },
+          'edit': {
+            'ticket.customer': {
+              'shown': true
+            },
+            'ticket.agent': {
+              'shown': true
+            }
+          }
+        },
+        'id': 'c-196'
+      }
+
+      post '/api/v1/object_manager_attributes', params: params, as: :json
+      expect(response).to have_http_status(201)
+      expect(json_response).to be_truthy
+      expect(json_response['data_option']['null']).to be_truthy
+      expect(json_response['data_option']['null']).to eq(true)
+      expect(json_response['name']).to eq('test2')
+    end
+
+    it 'does update ticket text object', db_strategy: :reset do
+
+      # add a new object
+      object = create(:object_manager_attribute_text)
+
+      migration = ObjectManager::Attribute.migration_execute
+      expect(migration).to eq(true)
+
+      authenticated_as(admin_user)
+      post "/api/v1/object_manager_attributes/#{object.id}", params: {}, as: :json
+      token = response.headers['CSRF-TOKEN']
+
+      # parameters for updating
+      params = {
+        'name': 'test4',
+        'object': 'Ticket',
+        'display': 'Test 4',
+        'active': true,
+        'data_type': 'input',
+        'data_option': {
+          'default': 'test',
+          'type': 'text',
+          'maxlength': 120
+        },
+        'screens': {
+          'create_middle': {
+            'ticket.customer': {
+              'shown': true,
+              'item_class': 'column'
+            },
+            'ticket.agent': {
+              'shown': true,
+              'item_class': 'column'
+            }
+          },
+          'edit': {
+            'ticket.customer': {
+              'shown': true
+            },
+            'ticket.agent': {
+              'shown': true
+            }
+          }
+        },
+        'id': 'c-196'
+      }
+
+      # update the object
+      put "/api/v1/object_manager_attributes/#{object.id}", params: params, as: :json
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_truthy
+      expect(json_response['data_option']['null']).to be_truthy
+      expect(json_response['name']).to eq('test4')
+      expect(json_response['display']).to eq('Test 4')
+    end
+
+    it 'does add new ticket boolean object' do
+      authenticated_as(admin_user)
+      post '/api/v1/object_manager_attributes', params: {}, as: :json
+      token = response.headers['CSRF-TOKEN']
+
+      # token based on headers
+      params = {
+        'active': true,
+        'data_option': {
+          'options': {
+            'false': 'no',
+            'true': 'yes'
+          }
+        },
+        'data_type': 'boolean',
+        'display': 'Boolean 2',
+        'id': 'c-200',
+        'name': 'bool2',
+        'object': 'Ticket',
+        'screens': {
+          'create_middle': {
+            'ticket.agent' => {
+              'item_class': 'column',
+              'shown': true
+            },
+            'ticket.customer' => {
+              'item_class': 'column',
+              'shown': true
+            }
+          },
+          'edit': {
+            'ticket.agent' => {
+              'shown': true
+            },
+            'ticket.customer' => {
+              'shown': true
+            }
+          }
+        }
+      }
+
+      post '/api/v1/object_manager_attributes', params: params, as: :json
+      expect(response).to have_http_status(201)
+      expect(json_response).to be_truthy
+      expect(json_response['data_option']['null']).to be_truthy
+      expect(json_response['data_option']['null']).to eq(true)
+      expect(json_response['name']).to eq('bool2')
+    end
+
+    it 'does add new user select object' do
+      authenticated_as(admin_user)
+      post '/api/v1/object_manager_attributes', params: {}, as: :json
+      token = response.headers['CSRF-TOKEN']
+
+      # token based on headers
+      params = {
+        'active': true,
+        'data_option': {
+          'options': {
+            'key1': 'foo'
+          }
+        },
+        'data_type': 'select',
+        'display': 'Test 5',
+        'id': 'c-204',
+        'name': 'test5',
+        'object': 'User',
+        'screens': {
+          'create': {
+            'admin.user' => {
+              'shown': true
+            },
+            'ticket.agent' => {
+              'shown': true
+            },
+            'ticket.customer' => {
+              'shown': true
+            }
+          },
+          'edit': {
+            'admin.user' => {
+              'shown': true
+            },
+            'ticket.agent' => {
+              'shown': true
+            }
+          },
+          'view': {
+            'admin.user' => {
+              'shown': true
+            },
+            'ticket.agent' => {
+              'shown': true
+            },
+            'ticket.customer' => {
+              'shown': true
+            }
+          }
+        }
+      }
+
+      post '/api/v1/object_manager_attributes', params: params, as: :json
+      expect(response).to have_http_status(201)
+      expect(json_response).to be_truthy
+      expect(json_response['data_option']['null']).to be_truthy
+      expect(json_response['data_option']['null']).to eq(true)
+      expect(json_response['name']).to eq('test5')
+    end
+
+    it 'does update user select object', db_strategy: :reset do
+
+      # add a new object
+      object = create(:object_manager_attribute_text)
+
+      migration = ObjectManager::Attribute.migration_execute
+      expect(migration).to eq(true)
+
+      post "/api/v1/object_manager_attributes/#{object.id}", params: {}, as: :json
+      token = response.headers['CSRF-TOKEN']
+
+      # parameters for updating
+      params = {
+        active: true,
+        data_option: {
+          options: {
+            key1: 'foo',
+            key2: 'bar'
+          }
+        },
+        data_type: 'select',
+        display: 'Test 7',
+        id: 'c-204',
+        name: 'test7',
+        object: 'User',
+        screens: {
+          create: {
+            'admin.user' => {
+              shown: true
+            },
+            'ticket.agent' => {
+              shown: true
+            },
+            'ticket.customer' => {
+              shown: true
+            }
+          },
+          edit: {
+            'admin.user' => {
+              shown: true
+            },
+            'ticket.agent' => {
+              shown: true
+            }
+          },
+          view: {
+            'admin.user' => {
+              shown: true
+            },
+            'ticket.agent' => {
+              shown: true
+            },
+            'ticket.customer' => {
+              shown: true
+            }
+          }
+        }
+      }
+
+      # update the object
+      authenticated_as(admin_user)
+      put "/api/v1/object_manager_attributes/#{object.id}", params: params, as: :json
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_truthy
+      expect(json_response['data_option']['options']).to be_truthy
+      expect(json_response['name']).to eq('test7')
+      expect(json_response['display']).to eq('Test 7')
+    end
+
+    it 'does converts string to boolean for default value for boolean data type with true (01)', db_strategy: :reset do
+      params = {
+        'name': "customerdescription#{rand(999_999_999)}",
+        'object': 'Ticket',
+        'display': "custom description#{rand(999_999_999)}",
+        'active': true,
+        'data_type': 'boolean',
+        'data_option': {
+          'options': {
+            'true': '',
+            'false': '',
+          },
+          'default': 'true',
+          'screens': {
+            'create_middle': {
+              'ticket.customer': {
+                'shown': true,
+                'item_class': 'column'
+              },
+              'ticket.agent': {
+                'shown': true,
+                'item_class': 'column'
+              }
+            },
+            'edit': {
+              'ticket.customer': {
+                'shown': true
+              },
+              'ticket.agent': {
+                'shown': true
+              }
+            }
+          }
+        },
+        'id': 'c-201'
+      }
+
+      authenticated_as(admin_user)
+      post '/api/v1/object_manager_attributes', params: params, as: :json
+
+      migration = ObjectManager::Attribute.migration_execute
+      expect(migration).to eq(true)
+
+      expect(response).to have_http_status(201) # created
+
+      expect(json_response).to be_truthy
+      expect(json_response['data_option']['default']).to be_truthy
+      expect(json_response['data_option']['default']).to eq(true)
+      expect(json_response['data_type']).to eq('boolean')
+    end
+
+    it 'does converts string to boolean for default value for boolean data type with false (02)', db_strategy: :reset do
+      params = {
+        'name': "customerdescription_#{rand(999_999_999)}",
+        'object': 'Ticket',
+        'display': "custom description #{rand(999_999_999)}",
+        'active': true,
+        'data_type': 'boolean',
+        'data_option': {
+          'options': {
+            'true': '',
+            'false': '',
+          },
+          'default': 'false',
+          'screens': {
+            'create_middle': {
+              'ticket.customer': {
+                'shown': true,
+                'item_class': 'column'
+              },
+              'ticket.agent': {
+                'shown': true,
+                'item_class': 'column'
+              }
+            },
+            'edit': {
+              'ticket.customer': {
+                'shown': true
+              },
+              'ticket.agent': {
+                'shown': true
+              }
+            }
+          }
+        },
+      }
+
+      authenticated_as(admin_user)
+      post '/api/v1/object_manager_attributes', params: params, as: :json
+
+      migration = ObjectManager::Attribute.migration_execute
+      expect(migration).to eq(true)
+
+      expect(response).to have_http_status(201) # created
+
+      expect(json_response).to be_truthy
+      expect(json_response['data_option']['default']).to be_falsey
+      expect(json_response['data_option']['default']).to eq(false)
+      expect(json_response['data_type']).to eq('boolean')
+    end
+
+    it 'does ticket attributes cannot be removed when it is referenced by an overview (03)', db_strategy: :reset do
+
+      # 1. create a new ticket attribute and execute migration
+      migration = ObjectManager::Attribute.migration_execute
+
+      params = {
+        'name': 'test_attribute_referenced_by_an_overview',
+        'object': 'Ticket',
+        'display': 'Test Attribute',
+        'active': true,
+        'data_type': 'input',
+        'data_option': {
+          'default': '',
+          'type': 'text',
+          'maxlength': 120,
+          'null': true,
+          'options': {},
+          'relation': ''
+        },
+        'screens': {
+          'create_middle': {
+            'ticket.customer': {
+              'shown': true,
+              'item_class': 'column'
+            },
+            'ticket.agent': {
+              'shown': true,
+              'item_class': 'column'
+            }
+          },
+          'edit': {
+            'ticket.customer': {
+              'shown': true
+            },
+            'ticket.agent': {
+              'shown': true
+            }
+          }
+        },
+      }
+
+      authenticated_as(admin_user)
+      post '/api/v1/object_manager_attributes', params: params, as: :json
+
+      migration = ObjectManager::Attribute.migration_execute
+      expect(migration).to eq(true)
+
+      # 2. create an overview that uses the attribute
+      params = {
+        name: 'test_overview',
+        roles: Role.where(name: 'Agent').pluck(:name),
+        condition: {
+          'ticket.state_id': {
+            'operator': 'is',
+            'value': Ticket::State.all.pluck(:id),
+          },
+          'ticket.test_attribute_referenced_by_an_overview': {
+            'operator': 'contains',
+            'value': 'DUMMY'
+          },
+        },
+        order: {
+          by: 'created_at',
+          direction: 'DESC',
+        },
+        view: {
+          d: %w[title customer state created_at],
+          s: %w[number title customer state created_at],
+          m: %w[number title customer state created_at],
+          view_mode_default: 's',
+        },
+        user_ids: [ '1' ],
+      }
+
+      if Overview.where('name like ?', '%test%').empty?
+        post '/api/v1/overviews', params: params, as: :json
+        expect(response).to have_http_status(201)
+        expect(Hash).to eq(json_response.class)
+        expect('test_overview').to eq(json_response['name'])
+      end
+
+      # 3. attempt to delete the ticket attribute
+      get '/api/v1/object_manager_attributes', as: :json
+      expect(response).to have_http_status(200)
+      target_attribute = json_response.select { |x| x['name'] == 'test_attribute_referenced_by_an_overview' && x['object'] == 'Ticket' }
+      expect(target_attribute.size).to eq(1)
+      target_id = target_attribute[0]['id']
+
+      delete "/api/v1/object_manager_attributes/#{target_id}", as: :json
+      expect(response).to have_http_status(422)
+      expect(response.body).to include('Overview')
+      expect(response.body).to include('test_overview')
+      expect(response.body).to include('cannot be deleted!')
+    end
+
+    it 'does ticket attributes cannot be removed when it is referenced by a trigger (04)', db_strategy: :reset do
+
+      # 1. create a new ticket attribute and execute migration
+      migration = ObjectManager::Attribute.migration_execute
+
+      params = {
+        'name': 'test_attribute_referenced_by_a_trigger',
+        'object': 'Ticket',
+        'display': 'Test Attribute',
+        'active': true,
+        'data_type': 'input',
+        'data_option': {
+          'default': '',
+          'type': 'text',
+          'maxlength': 120,
+          'null': true,
+          'options': {},
+          'relation': ''
+        },
+        'screens': {
+          'create_middle': {
+            'ticket.customer': {
+              'shown': true,
+              'item_class': 'column'
+            },
+            'ticket.agent': {
+              'shown': true,
+              'item_class': 'column'
+            }
+          },
+          'edit': {
+            'ticket.customer': {
+              'shown': true
+            },
+            'ticket.agent': {
+              'shown': true
+            }
+          }
+        },
+      }
+
+      authenticated_as(admin_user)
+      post '/api/v1/object_manager_attributes', params: params, as: :json
+
+      migration = ObjectManager::Attribute.migration_execute
+      expect(migration).to eq(true)
+
+      # 2. create an trigger that uses the attribute
+      params = {
+        name: 'test_trigger',
+        condition: {
+          'ticket.test_attribute_referenced_by_a_trigger': {
+            'operator': 'contains',
+            'value': 'DUMMY'
+          }
+        },
+        'perform': {
+          'ticket.state_id': {
+            'value': '2'
+          }
+        },
+        'active': true,
+        'id': 'c-3'
+      }
+
+      if Trigger.where('name like ?', '%test%').empty?
+        post '/api/v1/triggers', params: params, as: :json
+        expect(response).to have_http_status(201)
+        expect(Hash).to eq(json_response.class)
+        expect('test_trigger').to eq(json_response['name'])
+      end
+
+      # 3. attempt to delete the ticket attribute
+      get '/api/v1/object_manager_attributes', as: :json
+      expect(response).to have_http_status(200)
+      target_attribute = json_response.select { |x| x['name'] == 'test_attribute_referenced_by_a_trigger' && x['object'] == 'Ticket' }
+      expect(target_attribute.size).to eq(1)
+      target_id = target_attribute[0]['id']
+
+      delete "/api/v1/object_manager_attributes/#{target_id}", as: :json
+      expect(response).to have_http_status(422)
+      expect(response.body).to include('Trigger')
+      expect(response.body).to include('test_trigger')
+      expect(response.body).to include('cannot be deleted!')
+    end
+
+    it 'does ticket attributes cannot be removed when it is referenced by a scheduler (05)', db_strategy: :reset do
+
+      # 1. create a new ticket attribute and execute migration
+      migration = ObjectManager::Attribute.migration_execute
+
+      params = {
+        'name': 'test_attribute_referenced_by_a_scheduler',
+        'object': 'Ticket',
+        'display': 'Test Attribute',
+        'active': true,
+        'data_type': 'input',
+        'data_option': {
+          'default': '',
+          'type': 'text',
+          'maxlength': 120,
+          'null': true,
+          'options': {},
+          'relation': ''
+        },
+        'screens': {
+          'create_middle': {
+            'ticket.customer': {
+              'shown': true,
+              'item_class': 'column'
+            },
+            'ticket.agent': {
+              'shown': true,
+              'item_class': 'column'
+            }
+          },
+          'edit': {
+            'ticket.customer': {
+              'shown': true
+            },
+            'ticket.agent': {
+              'shown': true
+            }
+          }
+        },
+      }
+
+      authenticated_as(admin_user)
+      post '/api/v1/object_manager_attributes', params: params, as: :json
+
+      migration = ObjectManager::Attribute.migration_execute
+      expect(migration).to eq(true)
+
+      # 2. create a scheduler that uses the attribute
+      params = {
+        name: 'test_scheduler',
+        'timeplan': {
+          'days': {
+            'Mon': true,
+            'Tue': false,
+            'Wed': false,
+            'Thu': false,
+            'Fri': false,
+            'Sat': false,
+            'Sun': false
+          },
+          'hours': {
+            '0': true,
+            '1': false,
+            '2': false,
+            '3': false,
+            '4': false,
+            '5': false,
+            '6': false,
+            '7': false,
+            '8': false,
+            '9': false,
+            '10': false,
+            '11': false,
+            '12': false,
+            '13': false,
+            '14': false,
+            '15': false,
+            '16': false,
+            '17': false,
+            '18': false,
+            '19': false,
+            '20': false,
+            '21': false,
+            '22': false,
+            '23': false
+          },
+          'minutes': {
+            '0': true,
+            '10': false,
+            '20': false,
+            '30': false,
+            '40': false,
+            '50': false
+          }
+        },
+        'condition': {
+          'ticket.test_attribute_referenced_by_a_scheduler': {
+            'operator': 'contains',
+            'value': 'DUMMY'
+          }
+        },
+        'perform': {
+          'ticket.state_id': {
+            'value': '2'
+          }
+        },
+        'disable_notification': true,
+        'note': '',
+        'active': true,
+        'id': 'c-0'
+      }
+
+      if Job.where('name like ?', '%test%').empty?
+        post '/api/v1/jobs', params: params, as: :json
+        expect(response).to have_http_status(201)
+        expect(Hash).to eq(json_response.class)
+        expect('test_scheduler').to eq(json_response['name'])
+      end
+
+      # 3. attempt to delete the ticket attribute
+      get '/api/v1/object_manager_attributes', as: :json
+      expect(response).to have_http_status(200)
+      target_attribute = json_response.select { |x| x['name'] == 'test_attribute_referenced_by_a_scheduler' && x['object'] == 'Ticket' }
+      expect(target_attribute.size).to eq(1)
+      target_id = target_attribute[0]['id']
+
+      delete "/api/v1/object_manager_attributes/#{target_id}", as: :json
+      expect(response).to have_http_status(422)
+      expect(response.body).to include('Job')
+      expect(response.body).to include('test_scheduler')
+      expect(response.body).to include('cannot be deleted!')
+    end
+
+    it 'does ticket attributes can be removed when it is referenced by an overview but by user object (06)', db_strategy: :reset do
+
+      # 1. create a new ticket attribute and execute migration
+      migration = ObjectManager::Attribute.migration_execute
+
+      params = {
+        'name': 'test_attribute_referenced_by_an_overview',
+        'object': 'Ticket',
+        'display': 'Test Attribute',
+        'active': true,
+        'data_type': 'input',
+        'data_option': {
+          'default': '',
+          'type': 'text',
+          'maxlength': 120,
+          'null': true,
+          'options': {},
+          'relation': ''
+        },
+        'screens': {
+          'create_middle': {
+            'ticket.customer': {
+              'shown': true,
+              'item_class': 'column'
+            },
+            'ticket.agent': {
+              'shown': true,
+              'item_class': 'column'
+            }
+          },
+          'edit': {
+            'ticket.customer': {
+              'shown': true
+            },
+            'ticket.agent': {
+              'shown': true
+            }
+          }
+        },
+      }
+
+      authenticated_as(admin_user)
+      post '/api/v1/object_manager_attributes', params: params, as: :json
+
+      params = {
+        'name': 'test_attribute_referenced_by_an_overview',
+        'object': 'User',
+        'display': 'Test Attribute',
+        'active': true,
+        'data_type': 'input',
+        'data_option': {
+          'default': '',
+          'type': 'text',
+          'maxlength': 120,
+          'null': true,
+          'options': {},
+          'relation': ''
+        },
+        'screens': {
+          'create_middle': {
+            'ticket.customer': {
+              'shown': true,
+              'item_class': 'column'
+            },
+            'ticket.agent': {
+              'shown': true,
+              'item_class': 'column'
+            }
+          },
+          'edit': {
+            'ticket.customer': {
+              'shown': true
+            },
+            'ticket.agent': {
+              'shown': true
+            }
+          }
+        },
+      }
+
+      post '/api/v1/object_manager_attributes', params: params, as: :json
+
+      migration = ObjectManager::Attribute.migration_execute
+      expect(migration).to eq(true)
+
+      # 2. create an overview that uses the attribute
+      params = {
+        name: 'test_overview',
+        roles: Role.where(name: 'Agent').pluck(:name),
+        condition: {
+          'ticket.state_id': {
+            'operator': 'is',
+            'value': Ticket::State.all.pluck(:id),
+          },
+          'ticket.test_attribute_referenced_by_an_overview': {
+            'operator': 'contains',
+            'value': 'DUMMY'
+          },
+        },
+        order: {
+          by: 'created_at',
+          direction: 'DESC',
+        },
+        view: {
+          d: %w[title customer state created_at],
+          s: %w[number title customer state created_at],
+          m: %w[number title customer state created_at],
+          view_mode_default: 's',
+        },
+        user_ids: [ '1' ],
+      }
+
+      if Overview.where('name like ?', '%test%').empty?
+        post '/api/v1/overviews', params: params, as: :json
+        expect(response).to have_http_status(201)
+        expect(Hash).to eq(json_response.class)
+        expect('test_overview').to eq(json_response['name'])
+      end
+
+      # 3. attempt to delete the ticket attribute
+      get '/api/v1/object_manager_attributes', as: :json
+      expect(response).to have_http_status(200)
+      all_json_response = json_response
+
+      target_attribute = all_json_response.select { |x| x['name'] == 'test_attribute_referenced_by_an_overview' && x['object'] == 'User' }
+      expect(target_attribute.size).to eq(1)
+      target_id = target_attribute[0]['id']
+
+      delete "/api/v1/object_manager_attributes/#{target_id}", as: :json
+      expect(response).to have_http_status(200)
+
+      target_attribute = all_json_response.select { |x| x['name'] == 'test_attribute_referenced_by_an_overview' && x['object'] == 'Ticket' }
+      expect(target_attribute.size).to eq(1)
+      target_id = target_attribute[0]['id']
+
+      delete "/api/v1/object_manager_attributes/#{target_id}", as: :json
+      expect(response).to have_http_status(422)
+      expect(response.body).to include('Overview')
+      expect(response.body).to include('test_overview')
+      expect(response.body).to include('cannot be deleted!')
+
+      migration = ObjectManager::Attribute.migration_execute
+      expect(migration).to eq(true)
+    end
+
+    it 'does verify if attribute type can not be changed (07)', db_strategy: :reset do
+
+      params = {
+        'name': "customerdescription_#{rand(999_999_999)}",
+        'object': 'Ticket',
+        'display': "custom description #{rand(999_999_999)}",
+        'active': true,
+        'data_type': 'boolean',
+        'data_option': {
+          'options': {
+            'true': '',
+            'false': '',
+          },
+          'default': 'false',
+          'screens': {
+            'create_middle': {
+              'ticket.customer': {
+                'shown': true,
+                'item_class': 'column'
+              },
+              'ticket.agent': {
+                'shown': true,
+                'item_class': 'column'
+              }
+            },
+            'edit': {
+              'ticket.customer': {
+                'shown': true
+              },
+              'ticket.agent': {
+                'shown': true
+              }
+            }
+          }
+        },
+      }
+
+      authenticated_as(admin_user)
+      post '/api/v1/object_manager_attributes', params: params, as: :json
+
+      expect(response).to have_http_status(201) # created
+
+      expect(json_response).to be_truthy
+      expect(json_response['data_option']['default']).to be_falsey
+      expect(json_response['data_option']['default']).to eq(false)
+      expect(json_response['data_type']).to eq('boolean')
+
+      migration = ObjectManager::Attribute.migration_execute
+      expect(migration).to eq(true)
+
+      params['data_type'] = 'input'
+      params['data_option'] = {
+        'default': 'test',
+        'type': 'text',
+        'maxlength': 120
+      }
+
+      put "/api/v1/object_manager_attributes/#{json_response['id']}", params: params, as: :json
+      expect(response).to have_http_status(422)
+      expect(json_response).to be_truthy
+      expect(json_response['error']).to be_truthy
+
+    end
+
+    it 'does verify if attribute type can be changed (08)', db_strategy: :reset do
+
+      params = {
+        'name': "customerdescription_#{rand(999_999_999)}",
+        'object': 'Ticket',
+        'display': "custom description #{rand(999_999_999)}",
+        'active': true,
+        'data_type': 'input',
+        'data_option': {
+          'default': 'test',
+          'type': 'text',
+          'maxlength': 120,
+        },
+        'screens': {
+          'create_middle': {
+            'ticket.customer': {
+              'shown': true,
+              'item_class': 'column'
+            },
+            'ticket.agent': {
+              'shown': true,
+              'item_class': 'column'
+            }
+          },
+          'edit': {
+            'ticket.customer': {
+              'shown': true
+            },
+            'ticket.agent': {
+              'shown': true
+            }
+          },
+        },
+      }
+
+      authenticated_as(admin_user)
+      post '/api/v1/object_manager_attributes', params: params, as: :json
+
+      expect(response).to have_http_status(201) # created
+
+      expect(json_response).to be_truthy
+      expect(json_response['data_option']['default']).to eq('test')
+      expect(json_response['data_type']).to eq('input')
+
+      migration = ObjectManager::Attribute.migration_execute
+      expect(migration).to eq(true)
+
+      params['data_type'] = 'select'
+      params['data_option'] = {
+        'default': 'fuu',
+        'options': {
+          'key1': 'foo',
+          'key2': 'fuu',
+        }
+      }
+
+      put "/api/v1/object_manager_attributes/#{json_response['id']}", params: params, as: :json
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_truthy
+      expect(json_response['data_option']['default']).to eq('test')
+      expect(json_response['data_option_new']['default']).to eq('fuu')
+      expect(json_response['data_type']).to eq('select')
+    end
+  end
+end

+ 367 - 0
spec/requests/integration/telegram_spec.rb

@@ -0,0 +1,367 @@
+require 'rails_helper'
+
+RSpec.describe 'Telegram', type: :request do
+
+  describe 'request handling' do
+
+    it 'does basic call' do
+      Ticket.destroy_all
+
+      # configure telegram channel
+      token = 'valid_token'
+      bot_id = 123_456_789
+      group_id = Group.find_by(name: 'Users').id
+
+      UserInfo.current_user_id = 1
+      Channel.where(area: 'Telegram::Bot').destroy_all
+
+      # try with invalid token
+      stub_request(:post, 'https://api.telegram.org/botnot_existing/getMe')
+        .to_return(status: 404, body: '{"ok":false,"error_code":404,"description":"Not Found"}', headers: {})
+
+      expect do
+        Telegram.check_token('not_existing')
+      end.to raise_error(RuntimeError)
+
+      # try valid token
+      stub_request(:post, "https://api.telegram.org/bot#{token}/getMe")
+        .to_return(status: 200, body: "{\"ok\":true,\"result\":{\"id\":#{bot_id},\"first_name\":\"Chrispresso Customer Service\",\"username\":\"ChrispressoBot\"}}", headers: {})
+
+      bot = Telegram.check_token(token)
+      expect(bot['id']).to eq(bot_id)
+
+      stub_request(:post, "https://api.telegram.org/bot#{token}/getMe")
+        .to_return(status: 200, body: "{\"ok\":true,\"result\":{\"id\":#{bot_id},\"first_name\":\"Chrispresso Customer Service\",\"username\":\"ChrispressoBot\"}}", headers: {})
+
+      Setting.set('http_type', 'http')
+      expect do
+        Telegram.create_or_update_channel(token, { group_id: group_id, welcome: 'hi!' })
+      end.to raise_error(RuntimeError)
+
+      # try invalid port
+      stub_request(:post, "https://api.telegram.org:443/bot#{token}/getMe")
+        .to_return(status: 200, body: "{\"ok\":true,\"result\":{\"id\":#{bot_id},\"first_name\":\"Chrispresso Customer Service\",\"username\":\"ChrispressoBot\"}}", headers: {})
+      stub_request(:post, "https://api.telegram.org:443/bot#{token}/setWebhook")
+        .with(body: { 'url' => "https://somehost.example.com:12345/api/v1/channels_telegram_webhook/callback_token?bid=#{bot_id}" })
+        .to_return(status: 400, body: '{"ok":false,"error_code":400,"description":"Bad Request: bad webhook: Webhook can be set up only on ports 80, 88, 443 or 8443"}', headers: {})
+
+      Setting.set('http_type', 'https')
+      Setting.set('fqdn', 'somehost.example.com:12345')
+      expect do
+        Telegram.create_or_update_channel(token, { group_id: group_id, welcome: 'hi!' })
+      end.to raise_error(RuntimeError)
+
+      # try invalid host
+      stub_request(:post, "https://api.telegram.org:443/bot#{token}/setWebhook")
+        .with(body: { 'url' => "https://somehost.example.com/api/v1/channels_telegram_webhook/callback_token?bid=#{bot_id}" })
+        .to_return(status: 400, body: '{"ok":false,"error_code":400,"description":"Bad Request: bad webhook: getaddrinfo: Name or service not known"}', headers: {})
+
+      Setting.set('fqdn', 'somehost.example.com')
+      expect do
+        Telegram.create_or_update_channel(token, { group_id: group_id, welcome: 'hi!' })
+      end.to raise_error(RuntimeError)
+
+      # valid token, host and port
+      stub_request(:post, "https://api.telegram.org:443/bot#{token}/setWebhook")
+        .with(body: { 'url' => "https://example.com/api/v1/channels_telegram_webhook/callback_token?bid=#{bot_id}" })
+        .to_return(status: 200, body: '{"ok":true,"result":true,"description":"Webhook was set"}', headers: {})
+
+      Setting.set('fqdn', 'example.com')
+      channel = Telegram.create_or_update_channel(token, { group_id: group_id, welcome: 'hi!' })
+      UserInfo.current_user_id = nil
+
+      # start communication #1
+      post '/api/v1/channels/telegram_webhook', params: read_message('personal1_message_start'), as: :json
+      expect(response).to have_http_status(404)
+
+      post '/api/v1/channels_telegram_webhook/not_existing', params: read_message('personal1_message_start'), as: :json
+      expect(response).to have_http_status(422)
+      expect(json_response['error']).to eq('bot param missing')
+
+      callback_url = "/api/v1/channels_telegram_webhook/not_existing?bid=#{channel.options[:bot][:id]}"
+      post callback_url, params: read_message('personal1_message_start'), as: :json
+      expect(response).to have_http_status(422)
+      expect(json_response['error']).to eq('invalid callback token')
+
+      callback_url = "/api/v1/channels_telegram_webhook/#{channel.options[:callback_token]}?bid=#{channel.options[:bot][:id]}"
+      post callback_url, params: read_message('personal1_message_start'), as: :json
+      expect(response).to have_http_status(200)
+
+      # send message1
+      post callback_url, params: read_message('personal1_message_content1'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(1)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Hello, I need your Help')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(1)
+      expect(ticket.articles.first.body).to eq('Hello, I need your Help')
+      expect(ticket.articles.first.content_type).to eq('text/plain')
+
+      # send channel message1
+      post callback_url, params: read_message('channel1_message_content1'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(1)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Hello, I need your Help')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(1)
+      expect(ticket.articles.first.body).to eq('Hello, I need your Help')
+      expect(ticket.articles.first.content_type).to eq('text/plain')
+
+      # edit channel message1
+      post callback_url, params: read_message('channel2_message_content1'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(1)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Hello, I need your Help')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(1)
+      expect(ticket.articles.first.body).to eq('Hello, I need your Help')
+      expect(ticket.articles.first.content_type).to eq('text/plain')
+
+      # send same message again, ignore it
+      post callback_url, params: read_message('personal1_message_content1'), as: :json
+      expect(response).to have_http_status(200)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Hello, I need your Help')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(1)
+      expect(ticket.articles.first.body).to eq('Hello, I need your Help')
+      expect(ticket.articles.first.content_type).to eq('text/plain')
+
+      # send message2
+      post callback_url, params: read_message('personal1_message_content2'), as: :json
+      expect(response).to have_http_status(200)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Hello, I need your Help')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(2)
+      expect(ticket.articles.last.body).to eq('Hello, I need your Help 2')
+      expect(ticket.articles.last.content_type).to eq('text/plain')
+
+      # send end message
+      post callback_url, params: read_message('personal1_message_end'), as: :json
+      expect(response).to have_http_status(200)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Hello, I need your Help')
+      expect(ticket.state.name).to eq('closed')
+      expect(ticket.articles.count).to eq(2)
+      expect(ticket.articles.last.body).to eq('Hello, I need your Help 2')
+      expect(ticket.articles.last.content_type).to eq('text/plain')
+
+      # start communication #2
+      post callback_url, params: read_message('personal2_message_start'), as: :json
+      expect(response).to have_http_status(200)
+
+      # send message1
+      post callback_url, params: read_message('personal2_message_content1'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(2)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Can you help me with my feature?')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(1)
+      expect(ticket.articles.first.body).to eq('Can you help me with my feature?')
+      expect(ticket.articles.first.content_type).to eq('text/plain')
+
+      # send message2
+      post callback_url, params: read_message('personal2_message_content2'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(2)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Can you help me with my feature?')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(2)
+      expect(ticket.articles.last.body).to eq('Yes of course! <b>lalal</b>')
+      expect(ticket.articles.last.content_type).to eq('text/plain')
+
+      # start communication #3
+      post callback_url, params: read_message('personal3_message_start'), as: :json
+      expect(response).to have_http_status(200)
+
+      # send message1
+      post callback_url, params: read_message('personal3_message_content1'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(3)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Can you help me with my feature?')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(1)
+      expect(ticket.articles.last.body).to eq('Can you help me with my feature?')
+      expect(ticket.articles.last.content_type).to eq('text/plain')
+
+      # send message2
+      stub_request(:post, "https://api.telegram.org/bot#{token}/getFile")
+        .with(body: { 'file_id' => 'ABC-123VabcOcv123w0ABHywrcPqfrbAYIABC' })
+        .to_return(status: 200, body: '{"result":{"file_size":123,"file_id":"ABC-123VabcOcv123w0ABHywrcPqfrbAYIABC","file_path":"abc123"}}', headers: {})
+      stub_request(:get, "https://api.telegram.org/file/bot#{token}/abc123")
+        .to_return(status: 200, body: 'ABC1', headers: {})
+
+      post callback_url, params: read_message('personal3_message_content2'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(3)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Can you help me with my feature?')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(2)
+      expect(ticket.articles.last.body).to match(/<img style="width:360px;height:327px;"/i)
+      expect(ticket.articles.last.content_type).to eq('text/html')
+
+      # send channel message 3
+      post callback_url, params: read_message('channel1_message_content3'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(3)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Can you help me with my feature?')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(2)
+      expect(ticket.articles.last.body).to match(/<img style="width:360px;height:327px;"/i)
+      expect(ticket.articles.last.content_type).to eq('text/html')
+
+      # send message3
+      stub_request(:post, "https://api.telegram.org/bot#{token}/getFile")
+        .with(body: { 'file_id' => 'AAQCABO0I4INAATATQAB5HWPq4XgxQACAg' })
+        .to_return(status: 200, body: '{"result":{"file_size":123,"file_id":"ABC-123AAQCABO0I4INAATATQAB5HWPq4XgxQACAg","file_path":"abc123"}}', headers: {})
+      stub_request(:get, "https://api.telegram.org/file/bot#{token}/abc123")
+        .to_return(status: 200, body: 'ABC2', headers: {})
+      stub_request(:post, "https://api.telegram.org/bot#{token}/getFile")
+        .with(body: { 'file_id' => 'BQADAgADDgAD7x6ZSC_-1LMkOEmoAg' })
+        .to_return(status: 200, body: '{"result":{"file_size":123,"file_id":"ABC-123BQADAgADDgAD7x6ZSC_-1LMkOEmoAg","file_path":"abc123"}}', headers: {})
+
+      post callback_url, params: read_message('personal3_message_content3'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(3)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Can you help me with my feature?')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(3)
+      expect(ticket.articles.last.body).to match(/<img style="width:200px;height:200px;"/i)
+      expect(ticket.articles.last.content_type).to eq('text/html')
+      expect(ticket.articles.last.attachments.count).to eq(1)
+
+      # isend channel message 2
+      post callback_url, params: read_message('channel1_message_content2'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(3)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Can you help me with my feature?')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(3)
+      expect(ticket.articles.last.body).to match(/<img style="width:200px;height:200px;"/i)
+      expect(ticket.articles.last.content_type).to eq('text/html')
+      expect(ticket.articles.last.attachments.count).to eq(1)
+
+      # update message1
+      post callback_url, params: read_message('personal3_message_content4'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(3)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Can you help me with my feature?')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(3)
+      expect(ticket.articles.last.body).to match(/<img style="width:200px;height:200px;"/i)
+      expect(ticket.articles.last.content_type).to eq('text/html')
+      expect(ticket.articles.last.attachments.count).to eq(1)
+
+      expect(ticket.articles.first.body).to eq('UPDATE: 1231444')
+      expect(ticket.articles.first.content_type).to eq('text/plain')
+
+      # send voice5
+      stub_request(:post, "https://api.telegram.org/bot#{token}/getFile")
+        .with(body: { 'file_id' => 'AwADAgADVQADCEIYSZwyOmSZK9iZAg' })
+        .to_return(status: 200, body: '{"result":{"file_size":123,"file_id":"ABC-123AwADAgADVQADCEIYSZwyOmSZK9iZAg","file_path":"abc123"}}', headers: {})
+
+      post callback_url, params: read_message('personal3_message_content5'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(3)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Can you help me with my feature?')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(4)
+      expect(ticket.articles.last.content_type).to eq('text/html')
+      expect(ticket.articles.last.attachments.count).to eq(1)
+
+      # send channel message 4 with voice
+      post callback_url, params: read_message('channel1_message_content4'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(3)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Can you help me with my feature?')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(4)
+      expect(ticket.articles.last.content_type).to eq('text/html')
+      expect(ticket.articles.last.attachments.count).to eq(1)
+
+      # start communication #4 - with sticker
+      stub_request(:post, "https://api.telegram.org/bot#{token}/getFile")
+        .with(body: { 'file_id' => 'AAQDABO3-e4qAASs6ZOjJUT7tQ4lAAIC' })
+        .to_return(status: 200, body: '{"result":{"file_size":123,"file_id":"ABC-123AAQDABO3-e4qAASs6ZOjJUT7tQ4lAAIC","file_path":"abc123"}}', headers: {})
+      stub_request(:post, "https://api.telegram.org/bot#{token}/getFile")
+        .with(body: { 'file_id' => 'BQADAwAD0QIAAqbJWAAB8OkQqgtDQe0C' })
+        .to_return(status: 200, body: '{"result":{"file_size":123,"file_id":"ABC-123BQADAwAD0QIAAqbJWAAB8OkQqgtDQe0C","file_path":"abc123"}}', headers: {})
+
+      post callback_url, params: read_message('personal4_message_content1'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(4)
+      ticket = Ticket.last
+      if Rails.application.config.db_4bytes_utf8
+        expect(ticket.title).to eq('💻')
+      else
+        expect(ticket.title).to eq('')
+      end
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(1)
+      expect(ticket.articles.last.body).to match(/<img style="/i)
+      expect(ticket.articles.last.content_type).to eq('text/html')
+      expect(ticket.articles.last.attachments.count).to eq(1)
+
+      # send channel message #5 with sticker
+      post callback_url, params: read_message('channel1_message_content5'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(4)
+      ticket = Ticket.last
+      if Rails.application.config.db_4bytes_utf8
+        expect(ticket.title).to eq('💻')
+      else
+        expect(ticket.title).to eq('')
+      end
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(1)
+      expect(ticket.articles.last.body).to match(/<img style="/i)
+      expect(ticket.articles.last.content_type).to eq('text/html')
+      expect(ticket.articles.last.attachments.count).to eq(1)
+
+      # start communication #5 - with photo
+      stub_request(:post, "https://api.telegram.org/bot#{token}/getFile")
+        .with(body: { 'file_id' => 'AgADAgADwacxGxk5MUmim45lijOwsKk1Sw0ABNQoaI8BwR_z_2MFAAEC' })
+        .to_return(status: 200, body: '{"result":{"file_size":123,"file_id":"ABC-123AgADAgADwacxGxk5MUmim45lijOwsKk1Sw0ABNQoaI8BwR_z_2MFAAEC","file_path":"abc123"}}', headers: {})
+
+      post callback_url, params: read_message('personal5_message_content1'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(5)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('-')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(1)
+      expect(ticket.articles.last.body).to match(/<img style="/i)
+      expect(ticket.articles.last.content_type).to eq('text/html')
+      expect(ticket.articles.last.attachments.count).to eq(0)
+
+      post callback_url, params: read_message('personal5_message_content2'), as: :json
+      expect(response).to have_http_status(200)
+      expect(Ticket.count).to eq(5)
+      ticket = Ticket.last
+      expect(ticket.title).to eq('Hello, I need your Help')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.articles.count).to eq(2)
+      expect(ticket.articles.last.body).to match(/Hello, I need your Help/i)
+      expect(ticket.articles.last.content_type).to eq('text/plain')
+      expect(ticket.articles.last.attachments.count).to eq(0)
+    end
+
+    def read_message(file)
+      JSON.parse(File.read(Rails.root.join('test', 'data', 'telegram', "#{file}.json")))
+    end
+  end
+end

+ 210 - 0
spec/requests/integration/twilio_sms_spec.rb

@@ -0,0 +1,210 @@
+require 'rails_helper'
+
+RSpec.describe 'Twilio SMS', type: :request do
+
+  describe 'request handling' do
+
+    let(:agent_user) do
+      create(:agent_user, groups: Group.all)
+    end
+
+    it 'does basic call' do
+
+      # configure twilio channel
+      bot_id = 123_456_789
+      group_id = Group.find_by(name: 'Users').id
+
+      UserInfo.current_user_id = 1
+      channel = create(
+        :channel,
+        area: 'Sms::Account',
+        options: {
+          adapter: 'sms/twilio',
+          webhook_token: 'f409460e50f76d331fdac8ba7b7963b6',
+          account_id: '111',
+          token: '223',
+          sender: '333',
+        },
+        group_id: nil,
+      )
+
+      # process inbound sms
+      post '/api/v1/sms_webhook', params: read_message('inbound_sms1'), as: :json
+      expect(response).to have_http_status(404)
+
+      post '/api/v1/sms_webhook/not_existing', params: read_message('inbound_sms1'), as: :json
+      expect(response).to have_http_status(404)
+
+      post '/api/v1/sms_webhook/f409460e50f76d331fdac8ba7b7963b6', params: read_message('inbound_sms1'), as: :json
+      expect(response).to have_http_status(422)
+      expect('Can\'t use Channel::Driver::Sms::Twilio: #<Exceptions::UnprocessableEntity: Group needed in channel definition!>').to eq(json_response['error'])
+
+      channel.group_id = Group.first.id
+      channel.save!
+
+      post '/api/v1/sms_webhook/f409460e50f76d331fdac8ba7b7963b6', params: read_message('inbound_sms1'), as: :json
+      expect(response).to have_http_status(200)
+      xml_response = REXML::Document.new(response.body)
+      expect(1).to eq(xml_response.elements.count)
+
+      ticket = Ticket.last
+      article = Ticket::Article.last
+      customer = User.last
+      expect(ticket.articles.count).to eq(1)
+      expect(ticket.title).to eq('Ldfhxhcuffufuf. Fifififig.  Fifififiif F...')
+      expect(ticket.state.name).to eq('new')
+      expect(ticket.group_id).to eq(group_id)
+      expect(ticket.customer_id).to eq(customer.id)
+      expect(ticket.created_by_id).to eq(customer.id)
+      expect(article.from).to eq('+491710000000')
+      expect(article.to).to eq('+4915700000000')
+      expect(article.cc).to be_nil
+      expect(article.subject).to be_nil
+      expect(article.body).to eq('Ldfhxhcuffufuf. Fifififig.  Fifififiif Fifififiif Fifififiif Fifififiif Fifififiif')
+      expect(article.created_by_id).to eq(customer.id)
+      expect(article.sender.name).to eq('Customer')
+      expect(article.type.name).to eq('sms')
+
+      post '/api/v1/sms_webhook/f409460e50f76d331fdac8ba7b7963b6', params: read_message('inbound_sms2'), as: :json
+      expect(response).to have_http_status(200)
+      xml_response = REXML::Document.new(response.body)
+      expect(1).to eq(xml_response.elements.count)
+
+      ticket.reload
+      expect(ticket.articles.count).to eq(2)
+      expect(ticket.state.name).to eq('new')
+
+      article = Ticket::Article.last
+      expect(article.from).to eq('+491710000000')
+      expect(article.to).to eq('+4915700000000')
+      expect(article.cc).to be_nil
+      expect(article.subject).to be_nil
+      expect(article.body).to eq('Follow up')
+      expect(article.sender.name).to eq('Customer')
+      expect(article.type.name).to eq('sms')
+      expect(article.created_by_id).to eq(customer.id)
+
+      # check duplicate callbacks
+      post '/api/v1/sms_webhook/f409460e50f76d331fdac8ba7b7963b6', params: read_message('inbound_sms2'), as: :json
+      expect(response).to have_http_status(200)
+      xml_response = REXML::Document.new(response.body)
+      expect(1).to eq(xml_response.elements.count)
+
+      ticket.reload
+      expect(ticket.articles.count).to eq(2)
+      expect(ticket.state.name).to eq('new')
+      expect(article.id).to eq(Ticket::Article.last.id)
+
+      # new ticket need to be create
+      ticket.state = Ticket::State.find_by(name: 'closed')
+      ticket.save!
+
+      post '/api/v1/sms_webhook/f409460e50f76d331fdac8ba7b7963b6', params: read_message('inbound_sms3'), as: :json
+      expect(response).to have_http_status(200)
+      xml_response = REXML::Document.new(response.body)
+      expect(1).to eq(xml_response.elements.count)
+
+      ticket.reload
+      expect(ticket.articles.count).to eq(2)
+      expect(ticket.id).to_not eq(Ticket.last.id)
+      expect(ticket.state.name).to eq('closed')
+
+      ticket = Ticket.last
+      article = Ticket::Article.last
+      customer = User.last
+      expect(ticket.articles.count).to eq(1)
+      expect(ticket.title).to eq('new 2')
+      expect(ticket.group_id).to eq(group_id)
+      expect(ticket.customer_id).to eq(customer.id)
+      expect(ticket.created_by_id).to eq(customer.id)
+      expect(article.from).to eq('+491710000000')
+      expect(article.to).to eq('+4915700000000')
+      expect(article.cc).to be_nil
+      expect(article.subject).to be_nil
+      expect(article.body).to eq('new 2')
+      expect(article.created_by_id).to eq(customer.id)
+      expect(article.sender.name).to eq('Customer')
+      expect(article.type.name).to eq('sms')
+
+      # reply by agent
+      params = {
+        ticket_id: ticket.id,
+        body: 'some test',
+        type: 'sms',
+      }
+      authenticated_as(agent_user)
+      post '/api/v1/ticket_articles', params: params, as: :json
+      expect(response).to have_http_status(201)
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['subject']).to be_nil
+      expect(json_response['body']).to eq('some test')
+      expect(json_response['content_type']).to eq('text/plain')
+      expect(json_response['updated_by_id']).to eq(agent_user.id)
+      expect(json_response['created_by_id']).to eq(agent_user.id)
+
+      stub_request(:post, 'https://api.twilio.com/2010-04-01/Accounts/111/Messages.json')
+        .with(
+          body: {
+            'Body' => 'some test',
+            'From' => '333',
+            'To' => nil,
+          },
+          headers: {
+            'Accept' => 'application/json',
+            'Accept-Charset' => 'utf-8',
+            'Authorization' => 'Basic MTExOjIyMw==',
+            'Content-Type' => 'application/x-www-form-urlencoded',
+          }
+        ).to_return(status: 200, body: '', headers: {})
+
+      expect(article.preferences[:delivery_retry]).to be_nil
+      expect(article.preferences[:delivery_status]).to be_nil
+
+      Observer::Transaction.commit
+      Scheduler.worker(true)
+
+      article = Ticket::Article.find(json_response['id'])
+      expect(article.preferences[:delivery_retry]).to eq(1)
+      expect(article.preferences[:delivery_status]).to eq('success')
+
+    end
+
+    it 'does customer based on already existing mobile attibute' do
+
+      customer = create(
+        :customer_user,
+        email: 'me@example.com',
+        mobile: '01710000000',
+      )
+      Observer::Transaction.commit
+      Scheduler.worker(true)
+
+      # configure twilio channel
+      bot_id = 123_456_789
+
+      UserInfo.current_user_id = 1
+      channel = create(
+        :channel,
+        area: 'Sms::Account',
+        options: {
+          adapter: 'sms/twilio',
+          webhook_token: 'f409460e50f76d331fdac8ba7b7963b6',
+          account_id: '111',
+          token: '223',
+          sender: '333',
+        },
+      )
+
+      post '/api/v1/sms_webhook/f409460e50f76d331fdac8ba7b7963b6', params: read_message('inbound_sms1'), as: :json
+      expect(response).to have_http_status(200)
+      xml_response = REXML::Document.new(response.body)
+      expect(1).to eq(xml_response.elements.count)
+
+      expect(customer.id).to eq(User.last.id)
+    end
+
+    def read_message(file)
+      JSON.parse(File.read(Rails.root.join('test', 'data', 'twilio', "#{file}.json")))
+    end
+  end
+end

+ 700 - 0
spec/requests/integration/user_device_spec.rb

@@ -0,0 +1,700 @@
+require 'rails_helper'
+
+RSpec.describe 'User Device', type: :request, sends_notification_emails: true do
+
+  let!(:admin_user) do
+    create(:admin_user, login: 'user-device-admin', password: 'adminpw', groups: Group.all)
+  end
+  let!(:agent_user) do
+    create(:agent_user, login: 'user-device-agent', password: 'agentpw', groups: Group.all)
+  end
+
+  before(:each) do
+    ENV['TEST_REMOTE_IP'] = '5.9.62.170' # de
+    ENV['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:46.0) Gecko/20100101 Firefox/46.0'
+    ENV['SWITCHED_FROM_USER_ID'] = nil
+
+    UserDevice.destroy_all
+  end
+
+  describe 'request handling' do
+
+    it 'does index with nobody (01)' do
+
+      get '/api/v1/signshow'
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_a_kind_of(Hash)
+      expect('no valid session').to eq(json_response['error'])
+      expect(json_response['config']).to be_truthy
+      expect(controller.session[:user_device_fingerprint]).to be_falsey
+
+      Scheduler.worker(true)
+    end
+
+    it 'does login index with admin without fingerprint (02)' do
+
+      params = { without_fingerprint: 'none', username: 'user-device-admin', password: 'adminpw' }
+      post '/api/v1/signin', params: params, as: :json
+      expect(response).to have_http_status(422)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['error']).to eq('Need fingerprint param!')
+      expect(json_response['config']).to be_falsey
+      expect(controller.session[:user_device_fingerprint]).to be_falsey
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(0)
+    end
+
+    it 'does login index with admin with fingerprint - I (03)' do
+      params = { fingerprint: 'my_finger_print', username: 'user-device-admin', password: 'adminpw' }
+      post '/api/v1/signin', params: params, as: :json
+      expect(response).to have_http_status(201)
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['error']).to be_falsey
+      expect(json_response['config']).to be_truthy
+      expect(controller.session[:user_device_fingerprint]).to eq('my_finger_print')
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(1)
+      user_device_first = UserDevice.last
+      sleep 2
+
+      params = {}
+      get '/api/v1/users', params: params, as: :json
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_a_kind_of(Array)
+      expect(controller.session[:user_device_fingerprint]).to eq('my_finger_print')
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(1)
+      user_device_last = UserDevice.last
+      expect(user_device_first.updated_at.to_s).to eq(user_device_last.updated_at.to_s)
+
+      params = { fingerprint: 'my_finger_print' }
+      get '/api/v1/signshow', params: params, as: :json
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['session']).to be_truthy
+      expect('user-device-admin').to eq(json_response['session']['login'])
+      expect(json_response['config']).to be_truthy
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(1)
+      user_device_last = UserDevice.last
+      expect(user_device_first.updated_at.to_s).to eq(user_device_last.updated_at.to_s)
+
+      ENV['USER_DEVICE_UPDATED_AT'] = (Time.zone.now - 4.hours).to_s
+      params = {}
+      get '/api/v1/users', params: params, as: :json
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_a_kind_of(Array)
+      expect(controller.session[:user_device_fingerprint]).to eq('my_finger_print')
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(1)
+      user_device_last = UserDevice.last
+      expect(user_device_last.updated_at.to_s).to_not eq(user_device_first.updated_at.to_s)
+      ENV['USER_DEVICE_UPDATED_AT'] = nil
+
+      ENV['TEST_REMOTE_IP'] = '195.65.29.254' # ch
+
+      #reset_notification_checks
+
+      params = {}
+      get '/api/v1/users', params: params, as: :json
+      expect(response).to have_http_status(200)
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(2)
+
+      # ip reset
+      ENV['TEST_REMOTE_IP'] = '5.9.62.170' # de
+
+    end
+
+    it 'does login index with admin with fingerprint - II (04)' do
+
+      create(
+        :user_device,
+        user_id: admin_user.id,
+        fingerprint: 'fingerprintI',
+      )
+
+      params = { fingerprint: 'my_finger_print_II', username: 'user-device-admin', password: 'adminpw' }
+      post '/api/v1/signin', params: params, as: :json
+      expect(response).to have_http_status(201)
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(2)
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['error']).to be_falsey
+      expect(json_response['config']).to be_truthy
+      expect(controller.session[:user_device_fingerprint]).to be_truthy
+
+      get '/api/v1/users', params: params, as: :json
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_a_kind_of(Array)
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(2)
+
+      params = { fingerprint: 'my_finger_print_II' }
+      get '/api/v1/signshow', params: params, as: :json
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['session']).to be_truthy
+      expect('user-device-admin').to eq(json_response['session']['login'])
+      expect(json_response['config']).to be_truthy
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(2)
+
+      ENV['TEST_REMOTE_IP'] = '195.65.29.254' # ch
+
+      params = {}
+      get '/api/v1/users', params: params, as: :json
+      expect(response).to have_http_status(200)
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(3)
+
+      # ip reset
+      ENV['TEST_REMOTE_IP'] = '5.9.62.170' # de
+    end
+
+    it 'does login index with admin with fingerprint - II (05)' do
+
+      UserDevice.add(
+        ENV['HTTP_USER_AGENT'],
+        ENV['TEST_REMOTE_IP'],
+        admin_user.id,
+        'my_finger_print_II',
+        'session', # session|basic_auth|token_auth|sso
+      )
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(1)
+
+      params = { fingerprint: 'my_finger_print_II', username: 'user-device-admin', password: 'adminpw' }
+      post '/api/v1/signin', params: params, as: :json
+      expect(response).to have_http_status(201)
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(1)
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['error']).to be_falsey
+      expect(json_response['config']).to be_truthy
+      expect(controller.session[:user_device_fingerprint]).to be_truthy
+    end
+
+    it 'does login index with admin with basic auth (06)' do
+
+      ENV['HTTP_USER_AGENT'] = 'curl 1.0.0'
+      UserDevice.add(
+        ENV['HTTP_USER_AGENT'],
+        '127.0.0.1',
+        admin_user.id,
+        '',
+        'basic_auth', # session|basic_auth|token_auth|sso
+      )
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(1)
+
+      ENV['HTTP_USER_AGENT'] = 'curl 1.2.3'
+      params = {}
+      authenticated_as(admin_user, password: 'adminpw')
+      get '/api/v1/users', params: params, as: :json
+      expect(response).to have_http_status(200)
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(2)
+      expect(json_response).to be_a_kind_of(Array)
+      user_device_first = UserDevice.last
+      sleep 2
+
+      params = {}
+      get '/api/v1/users', params: params, as: :json
+      expect(response).to have_http_status(200)
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(2)
+      expect(json_response).to be_a_kind_of(Array)
+      user_device_last = UserDevice.last
+      expect(user_device_first.id).to eq(user_device_last.id)
+      expect(user_device_first.updated_at.to_s).to eq(user_device_last.updated_at.to_s)
+
+      user_device_last.updated_at = Time.zone.now - 4.hours
+      user_device_last.save!
+
+      params = {}
+      get '/api/v1/users', params: params, as: :json
+      expect(response).to have_http_status(200)
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(2)
+      expect(json_response).to be_a_kind_of(Array)
+      user_device_last = UserDevice.last
+      expect(user_device_first.id).to eq(user_device_last.id)
+      expect(user_device_last.updated_at > user_device_first.updated_at).to be_truthy
+    end
+
+    it 'does login index with admin with basic auth (07)' do
+
+      ENV['HTTP_USER_AGENT'] = 'curl 1.2.3'
+
+      UserDevice.add(
+        ENV['HTTP_USER_AGENT'],
+        ENV['TEST_REMOTE_IP'],
+        admin_user.id,
+        '',
+        'basic_auth', # session|basic_auth|token_auth|sso
+      )
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(1)
+
+      params = {}
+      authenticated_as(admin_user, password: 'adminpw')
+      get '/api/v1/users', params: params, as: :json
+      expect(response).to have_http_status(200)
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(1)
+      expect(json_response).to be_a_kind_of(Array)
+
+    end
+
+    it 'does login index with agent with basic auth (08)' do
+      ENV['HTTP_USER_AGENT'] = 'curl 1.2.3'
+
+      params = {}
+      authenticated_as(agent_user, password: 'agentpw')
+      get '/api/v1/users', params: params, as: :json
+      expect(response).to have_http_status(200)
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     agent_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     agent_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: agent_user.id).count).to eq(1)
+      expect(json_response).to be_a_kind_of(Array)
+    end
+
+    it 'does login index with agent with basic auth (09)' do
+
+      ENV['HTTP_USER_AGENT'] = 'curl 1.2.3'
+
+      UserDevice.add(
+        ENV['HTTP_USER_AGENT'],
+        ENV['TEST_REMOTE_IP'],
+        agent_user.id,
+        '',
+        'basic_auth', # session|basic_auth|token_auth|sso
+      )
+
+      expect(UserDevice.where(user_id: agent_user.id).count).to eq(1)
+
+      params = {}
+      authenticated_as(agent_user, password: 'agentpw')
+      get '/api/v1/users', params: params, as: :json
+      expect(response).to have_http_status(200)
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     agent_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     agent_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: agent_user.id).count).to eq(1)
+      expect(json_response).to be_a_kind_of(Array)
+
+    end
+
+    it 'does login with switched_from_user_id (10)' do
+      expect(UserDevice.where(user_id: agent_user.id).count).to eq(0)
+
+      ENV['SWITCHED_FROM_USER_ID'] = admin_user.id.to_s
+
+      params = { fingerprint: 'my_finger_print_II', username: 'user-device-agent', password: 'agentpw' }
+      post '/api/v1/signin', params: params, as: :json
+      expect(response).to have_http_status(201)
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     agent_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     agent_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: agent_user.id).count).to eq(0)
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['error']).to be_falsey
+      expect(json_response['config']).to be_truthy
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     agent_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     agent_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: agent_user.id).count).to eq(0)
+
+      ENV['USER_DEVICE_UPDATED_AT'] = (Time.zone.now - 4.hours).to_s
+      params = {}
+      get '/api/v1/users', params: params, as: :json
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_a_kind_of(Array)
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     agent_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     agent_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: agent_user.id).count).to eq(0)
+      ENV['USER_DEVICE_UPDATED_AT'] = nil
+
+      ENV['TEST_REMOTE_IP'] = '195.65.29.254' # ch
+      params = {}
+      get '/api/v1/users', params: params, as: :json
+      expect(response).to have_http_status(200)
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     agent_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     agent_user,
+        )
+      end
+
+      # ip reset
+      ENV['TEST_REMOTE_IP'] = '5.9.62.170' # de
+
+      expect(UserDevice.where(user_id: agent_user.id).count).to eq(0)
+    end
+
+    it 'does login with invalid fingerprint (11)' do
+      params = { fingerprint: 'to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890', username: 'user-device-admin', password: 'adminpw' }
+      post '/api/v1/signin', params: params, as: :json
+      expect(response).to have_http_status(422)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['error']).to eq('fingerprint is 198 chars but can only be 160 chars!')
+      expect(json_response['config']).to be_falsey
+      expect(controller.session[:user_device_fingerprint]).to be_falsey
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(0)
+    end
+
+    it 'does login with integer as fingerprint (12)' do
+      params = { fingerprint: 123_456_789, username: 'user-device-admin', password: 'adminpw' }
+      post '/api/v1/signin', params: params, as: :json
+      expect(response).to have_http_status(201)
+      expect(controller.session[:user_device_fingerprint]).to be_truthy
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(1)
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['error']).to be_nil
+    end
+
+    it 'does login form controller - check no user device logging (13)' do
+      Setting.set('form_ticket_create', true)
+
+      params = {
+        fingerprint: 'long_1234567890long_1234567890long_1234567890long_1234567890long_1234567890long_1234567890long_1234567890long_1234567890long_1234567890long_1234567890long_1234567890'
+      }
+      authenticated_as(admin_user, password: 'adminpw')
+      post '/api/v1/form_config', params: params, as: :json
+      expect(response).to have_http_status(200)
+
+      expect(json_response).to be_a_kind_of(Hash)
+      expect(json_response['error']).to be_falsey
+      expect(json_response['endpoint']).to be_truthy
+      expect(controller.session[:user_device_fingerprint]).to be_falsey
+
+      check_notification do
+
+        Scheduler.worker(true)
+
+        not_sent(
+          template: 'user_device_new',
+          user:     admin_user,
+        )
+        not_sent(
+          template: 'user_device_new_location',
+          user:     admin_user,
+        )
+      end
+
+      expect(UserDevice.where(user_id: admin_user.id).count).to eq(0)
+    end
+  end
+end

+ 3 - 1
spec/requests/long_polling_spec.rb

@@ -75,7 +75,9 @@ RSpec.describe 'LongPolling', type: :request do
 
     it 'send event spool and receive data' do
 
-      authenticated_as(agent_user)
+      # here we use a token for the authentication because the basic auth way with username and password
+      # will update the user by every request and return a different result for the test
+      authenticated_as(agent_user, token: create(:token, action: 'api', user_id: agent_user.id) )
       get '/api/v1/message_send', params: { data: { event: 'login' } }, as: :json
       expect(response).to have_http_status(200)
       expect(json_response['client_id'].to_i).to be_between(1, 9_999_999_999)

Some files were not shown because too many files changed in this diff