Merge branch 'develop' into private-te_rubocop_update
This commit is contained in:
commit
76dfdbaf83
30 changed files with 1214 additions and 244 deletions
|
@ -24,6 +24,7 @@ variables:
|
||||||
when: on_failure
|
when: on_failure
|
||||||
paths:
|
paths:
|
||||||
- tmp/screenshot*
|
- tmp/screenshot*
|
||||||
|
- tmp/screenshots/*
|
||||||
- log/*.log
|
- log/*.log
|
||||||
|
|
||||||
# Workaround to enable usage of mixed SSH and Docker GitLab CI runners
|
# Workaround to enable usage of mixed SSH and Docker GitLab CI runners
|
||||||
|
@ -67,6 +68,7 @@ stages:
|
||||||
pre:rubocop:
|
pre:rubocop:
|
||||||
<<: *pre_stage
|
<<: *pre_stage
|
||||||
script:
|
script:
|
||||||
|
- bundle install -j $(nproc)
|
||||||
- bundle exec rubocop
|
- bundle exec rubocop
|
||||||
|
|
||||||
pre:coffeelint:
|
pre:coffeelint:
|
||||||
|
@ -98,7 +100,7 @@ pre:github:
|
||||||
RAILS_ENV: "test"
|
RAILS_ENV: "test"
|
||||||
script:
|
script:
|
||||||
- rake zammad:db:init
|
- rake zammad:db:init
|
||||||
- bundle exec rspec
|
- bundle exec rspec -t ~type:system
|
||||||
|
|
||||||
test:rspec:mysql:
|
test:rspec:mysql:
|
||||||
stage: test
|
stage: test
|
||||||
|
@ -320,6 +322,67 @@ browser:build:
|
||||||
- public/assets/application-*
|
- public/assets/application-*
|
||||||
- public/assets/print-*
|
- public/assets/print-*
|
||||||
|
|
||||||
|
.services_browser_postgresql_template: &services_browser_postgresql_definition
|
||||||
|
services:
|
||||||
|
- name: registry.znuny.com/docker/zammad-postgresql:latest
|
||||||
|
alias: postgresql
|
||||||
|
- name: registry.znuny.com/docker/zammad-elasticsearch:latest
|
||||||
|
alias: elasticsearch
|
||||||
|
- name: docker.io/elgalu/selenium:latest
|
||||||
|
alias: selenium
|
||||||
|
- name: registry.znuny.com/docker/docker-imap-devel:latest
|
||||||
|
alias: mail
|
||||||
|
|
||||||
|
.services_browser_mysql_template: &services_browser_mysql_definition
|
||||||
|
services:
|
||||||
|
- name: registry.znuny.com/docker/zammad-mysql:latest
|
||||||
|
alias: mysql
|
||||||
|
- name: registry.znuny.com/docker/zammad-elasticsearch:latest
|
||||||
|
alias: elasticsearch
|
||||||
|
- name: docker.io/elgalu/selenium:latest
|
||||||
|
alias: selenium
|
||||||
|
- name: registry.znuny.com/docker/docker-imap-devel:latest
|
||||||
|
alias: mail
|
||||||
|
|
||||||
|
## Capybara
|
||||||
|
|
||||||
|
.test_capybara_template: &test_capybara_definition
|
||||||
|
<<: *base_env
|
||||||
|
stage: browser-core
|
||||||
|
script:
|
||||||
|
- rake zammad:ci:test:prepare[with_elasticsearch]
|
||||||
|
- bundle exec rspec --fail-fast -t type:system
|
||||||
|
|
||||||
|
.variables_capybara_chrome_template: &variables_capybara_chrome_definition
|
||||||
|
<<: *test_capybara_definition
|
||||||
|
variables:
|
||||||
|
RAILS_ENV: "test"
|
||||||
|
NO_RESET_BEFORE_SUITE: "true"
|
||||||
|
BROWSER: "chrome"
|
||||||
|
|
||||||
|
.variables_capybara_ff_template: &variables_capybara_ff_definition
|
||||||
|
<<: *test_capybara_definition
|
||||||
|
variables:
|
||||||
|
RAILS_ENV: "test"
|
||||||
|
NO_RESET_BEFORE_SUITE: "true"
|
||||||
|
BROWSER: "firefox"
|
||||||
|
|
||||||
|
test:browser:core:capybara_chrome_postgresql:
|
||||||
|
<<: *variables_capybara_chrome_definition
|
||||||
|
<<: *services_browser_postgresql_definition
|
||||||
|
|
||||||
|
test:browser:core:capybara_chrome_mysql:
|
||||||
|
<<: *variables_capybara_chrome_definition
|
||||||
|
<<: *services_browser_mysql_definition
|
||||||
|
|
||||||
|
test:browser:core:capybara_ff_postgresql:
|
||||||
|
<<: *variables_capybara_ff_definition
|
||||||
|
<<: *services_browser_postgresql_definition
|
||||||
|
|
||||||
|
test:browser:core:capybara_ff_mysql:
|
||||||
|
<<: *variables_capybara_ff_definition
|
||||||
|
<<: *services_browser_mysql_definition
|
||||||
|
|
||||||
## Browser core tests
|
## Browser core tests
|
||||||
|
|
||||||
.variables_browser_template: &variables_browser_definition
|
.variables_browser_template: &variables_browser_definition
|
||||||
|
@ -387,28 +450,12 @@ test:browser:integration:api_client_php:
|
||||||
.test_browser_core_postgresql_template: &test_browser_core_postgresql_definition
|
.test_browser_core_postgresql_template: &test_browser_core_postgresql_definition
|
||||||
<<: *test_browser_core_definition
|
<<: *test_browser_core_definition
|
||||||
<<: *script_browser_slice_definition
|
<<: *script_browser_slice_definition
|
||||||
services:
|
<<: *services_browser_postgresql_definition
|
||||||
- name: registry.znuny.com/docker/zammad-postgresql:latest
|
|
||||||
alias: postgresql
|
|
||||||
- name: registry.znuny.com/docker/zammad-elasticsearch:latest
|
|
||||||
alias: elasticsearch
|
|
||||||
- name: docker.io/elgalu/selenium:latest
|
|
||||||
alias: selenium
|
|
||||||
- name: registry.znuny.com/docker/docker-imap-devel:latest
|
|
||||||
alias: mail
|
|
||||||
|
|
||||||
.test_browser_core_mysql_template: &test_browser_core_mysql_definition
|
.test_browser_core_mysql_template: &test_browser_core_mysql_definition
|
||||||
<<: *test_browser_core_definition
|
<<: *test_browser_core_definition
|
||||||
<<: *script_browser_slice_definition
|
<<: *script_browser_slice_definition
|
||||||
services:
|
<<: *services_browser_mysql_definition
|
||||||
- name: registry.znuny.com/docker/zammad-mysql:latest
|
|
||||||
alias: mysql
|
|
||||||
- name: registry.znuny.com/docker/zammad-elasticsearch:latest
|
|
||||||
alias: elasticsearch
|
|
||||||
- name: docker.io/elgalu/selenium:latest
|
|
||||||
alias: selenium
|
|
||||||
- name: registry.znuny.com/docker/docker-imap-devel:latest
|
|
||||||
alias: mail
|
|
||||||
|
|
||||||
#### Firefox
|
#### Firefox
|
||||||
|
|
||||||
|
|
1
Gemfile
1
Gemfile
|
@ -145,6 +145,7 @@ group :development, :test do
|
||||||
gem 'simplecov-rcov'
|
gem 'simplecov-rcov'
|
||||||
|
|
||||||
# UI tests w/ Selenium
|
# UI tests w/ Selenium
|
||||||
|
gem 'capybara', '~> 2.13'
|
||||||
gem 'selenium-webdriver'
|
gem 'selenium-webdriver'
|
||||||
|
|
||||||
# livereload on template changes (html, js, css)
|
# livereload on template changes (html, js, css)
|
||||||
|
|
11
Gemfile.lock
11
Gemfile.lock
|
@ -104,6 +104,13 @@ GEM
|
||||||
buftok (0.2.0)
|
buftok (0.2.0)
|
||||||
builder (3.2.3)
|
builder (3.2.3)
|
||||||
byebug (10.0.2)
|
byebug (10.0.2)
|
||||||
|
capybara (2.18.0)
|
||||||
|
addressable
|
||||||
|
mini_mime (>= 0.1.3)
|
||||||
|
nokogiri (>= 1.3.3)
|
||||||
|
rack (>= 1.0.0)
|
||||||
|
rack-test (>= 0.5.4)
|
||||||
|
xpath (>= 2.0, < 4.0)
|
||||||
childprocess (0.9.0)
|
childprocess (0.9.0)
|
||||||
ffi (~> 1.0, >= 1.0.11)
|
ffi (~> 1.0, >= 1.0.11)
|
||||||
clavius (1.0.3)
|
clavius (1.0.3)
|
||||||
|
@ -255,6 +262,7 @@ GEM
|
||||||
mime-types (3.2.2)
|
mime-types (3.2.2)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2018.0812)
|
mime-types-data (3.2018.0812)
|
||||||
|
mini_mime (1.0.1)
|
||||||
mini_portile2 (2.3.0)
|
mini_portile2 (2.3.0)
|
||||||
minitest (5.11.3)
|
minitest (5.11.3)
|
||||||
multi_json (1.12.2)
|
multi_json (1.12.2)
|
||||||
|
@ -499,6 +507,8 @@ GEM
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.3)
|
websocket-extensions (0.1.3)
|
||||||
writeexcel (1.0.5)
|
writeexcel (1.0.5)
|
||||||
|
xpath (3.2.0)
|
||||||
|
nokogiri (~> 1.8)
|
||||||
zendesk_api (1.16.0)
|
zendesk_api (1.16.0)
|
||||||
faraday (~> 0.9)
|
faraday (~> 0.9)
|
||||||
hashie (>= 3.5.2, < 4.0.0)
|
hashie (>= 3.5.2, < 4.0.0)
|
||||||
|
@ -519,6 +529,7 @@ DEPENDENCIES
|
||||||
biz
|
biz
|
||||||
browser
|
browser
|
||||||
byebug
|
byebug
|
||||||
|
capybara (~> 2.13)
|
||||||
clearbit
|
clearbit
|
||||||
coffee-rails
|
coffee-rails
|
||||||
coffee-script-source
|
coffee-script-source
|
||||||
|
|
|
@ -257,13 +257,20 @@ class Scheduler < ApplicationModel
|
||||||
else
|
else
|
||||||
_start_job(job)
|
_start_job(job)
|
||||||
end
|
end
|
||||||
job.pid = ''
|
|
||||||
job.save
|
|
||||||
logger.info " ...stopped thread for '#{job.method}'"
|
|
||||||
ActiveRecord::Base.connection.close
|
|
||||||
|
|
||||||
# release thread lock and remove thread handle
|
if job.present?
|
||||||
@@jobs_started.delete(job.id)
|
job.pid = ''
|
||||||
|
job.save
|
||||||
|
|
||||||
|
logger.info " ...stopped thread for '#{job.method}'"
|
||||||
|
|
||||||
|
# release thread lock and remove thread handle
|
||||||
|
@@jobs_started.delete(job.id)
|
||||||
|
else
|
||||||
|
logger.warn ' ...Job got deleted while running'
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveRecord::Base.connection.close
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ threads_count_min = Integer(ENV['MIN_THREADS'] || 5)
|
||||||
threads_count_max = Integer(ENV['MAX_THREADS'] || 30)
|
threads_count_max = Integer(ENV['MAX_THREADS'] || 30)
|
||||||
threads threads_count_min, threads_count_max
|
threads threads_count_min, threads_count_max
|
||||||
|
|
||||||
|
environment ENV.fetch('RAILS_ENV', 'development')
|
||||||
|
|
||||||
preload_app!
|
preload_app!
|
||||||
|
|
||||||
on_worker_boot do
|
on_worker_boot do
|
||||||
|
|
|
@ -12,7 +12,12 @@ class Sessions::Event::Base
|
||||||
|
|
||||||
return if !self.class.instance_variable_get(:@database_connection)
|
return if !self.class.instance_variable_get(:@database_connection)
|
||||||
|
|
||||||
ActiveRecord::Base.establish_connection
|
if ActiveRecord::Base.connected?
|
||||||
|
@reused_connection = true
|
||||||
|
else
|
||||||
|
@reused_connection = false
|
||||||
|
ActiveRecord::Base.establish_connection
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.inherited(subclass)
|
def self.inherited(subclass)
|
||||||
|
@ -138,6 +143,7 @@ class Sessions::Event::Base
|
||||||
def destroy
|
def destroy
|
||||||
return if !@is_web_socket
|
return if !@is_web_socket
|
||||||
return if !self.class.instance_variable_get(:@database_connection)
|
return if !self.class.instance_variable_get(:@database_connection)
|
||||||
|
return if @reused_connection
|
||||||
|
|
||||||
ActiveRecord::Base.remove_connection
|
ActiveRecord::Base.remove_connection
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,8 @@ namespace :zammad do
|
||||||
namespace :test do
|
namespace :test do
|
||||||
|
|
||||||
desc 'Stops all of Zammads services and exists the rake task with exit code 1'
|
desc 'Stops all of Zammads services and exists the rake task with exit code 1'
|
||||||
task fail: %i[zammad:ci:test:stop] do
|
task :fail, [:no_app] do |_task, args|
|
||||||
|
Rake::Task['zammad:ci:test:stop'].invoke if args[:no_app].blank?
|
||||||
abort('Abort further test processing')
|
abort('Abort further test processing')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
23
lib/tasks/zammad/ci/test/prepare.rake
Normal file
23
lib/tasks/zammad/ci/test/prepare.rake
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
namespace :zammad do
|
||||||
|
|
||||||
|
namespace :ci do
|
||||||
|
|
||||||
|
namespace :test do
|
||||||
|
|
||||||
|
desc 'Prepares Zammad system for CI env'
|
||||||
|
task :prepare, [:elasticsearch] do |_task, args|
|
||||||
|
ENV['RAILS_ENV'] ||= 'production'
|
||||||
|
ENV['DISABLE_DATABASE_ENVIRONMENT_CHECK'] = 'true'
|
||||||
|
# we have to enforce the env
|
||||||
|
# otherwise it will fallback to default (develop)
|
||||||
|
Rails.env = ENV['RAILS_ENV']
|
||||||
|
|
||||||
|
Rake::Task['zammad:flush:cache'].invoke
|
||||||
|
|
||||||
|
Rake::Task['zammad:db:init'].invoke
|
||||||
|
|
||||||
|
Rake::Task['zammad:ci:settings'].invoke(args[:elasticsearch])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,17 +6,7 @@ namespace :zammad do
|
||||||
|
|
||||||
desc 'Starts all of Zammads services for CI test'
|
desc 'Starts all of Zammads services for CI test'
|
||||||
task :start, [:elasticsearch] do |_task, args|
|
task :start, [:elasticsearch] do |_task, args|
|
||||||
ENV['RAILS_ENV'] ||= 'production'
|
Rake::Task['zammad:ci:test:prepare'].invoke(args[:elasticsearch])
|
||||||
ENV['DISABLE_DATABASE_ENVIRONMENT_CHECK'] = 'true'
|
|
||||||
# we have to enforce the env
|
|
||||||
# otherwise it will fallback to default (develop)
|
|
||||||
Rails.env = ENV['RAILS_ENV']
|
|
||||||
|
|
||||||
Rake::Task['zammad:flush:cache'].invoke
|
|
||||||
|
|
||||||
Rake::Task['zammad:db:init'].invoke
|
|
||||||
|
|
||||||
Rake::Task['zammad:ci:settings'].invoke(args[:elasticsearch])
|
|
||||||
Rake::Task['zammad:ci:app:start'].invoke
|
Rake::Task['zammad:ci:app:start'].invoke
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@ namespace :zammad do
|
||||||
namespace :test do
|
namespace :test do
|
||||||
|
|
||||||
desc 'Stop of all Zammad services and cleans up the database(s)'
|
desc 'Stop of all Zammad services and cleans up the database(s)'
|
||||||
task :stop do
|
task :stop, [:no_app] do |_task, args|
|
||||||
ENV['RAILS_ENV'] ||= 'production'
|
ENV['RAILS_ENV'] ||= 'production'
|
||||||
ENV['DISABLE_DATABASE_ENVIRONMENT_CHECK'] = 'true'
|
ENV['DISABLE_DATABASE_ENVIRONMENT_CHECK'] = 'true'
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ namespace :zammad do
|
||||||
# otherwise it will fallback to default (develop)
|
# otherwise it will fallback to default (develop)
|
||||||
Rails.env = ENV['RAILS_ENV']
|
Rails.env = ENV['RAILS_ENV']
|
||||||
|
|
||||||
Rake::Task['zammad:ci:app:stop'].invoke
|
Rake::Task['zammad:ci:app:stop'].invoke if args[:no_app].blank?
|
||||||
Rake::Task['db:drop:all'].invoke
|
Rake::Task['db:drop:all'].invoke
|
||||||
|
|
||||||
next if !SearchIndexBackend.enabled?
|
next if !SearchIndexBackend.enabled?
|
||||||
|
|
227
lib/websocket_server.rb
Normal file
227
lib/websocket_server.rb
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
class WebsocketServer
|
||||||
|
|
||||||
|
cattr_reader :clients, :options
|
||||||
|
|
||||||
|
def self.run(options)
|
||||||
|
@options = options
|
||||||
|
@clients = {}
|
||||||
|
|
||||||
|
Rails.configuration.interface = 'websocket'
|
||||||
|
EventMachine.run do
|
||||||
|
EventMachine::WebSocket.start( host: @options[:b], port: @options[:p], secure: @options[:s], tls_options: @options[:tls_options] ) do |ws|
|
||||||
|
|
||||||
|
# register client connection
|
||||||
|
ws.onopen do |handshake|
|
||||||
|
WebsocketServer.onopen(ws, handshake)
|
||||||
|
end
|
||||||
|
|
||||||
|
# unregister client connection
|
||||||
|
ws.onclose do
|
||||||
|
WebsocketServer.onclose(ws)
|
||||||
|
end
|
||||||
|
|
||||||
|
# manage messages
|
||||||
|
ws.onmessage do |msg|
|
||||||
|
WebsocketServer.onmessage(ws, msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# check unused connections
|
||||||
|
EventMachine.add_timer(0.5) do
|
||||||
|
WebsocketServer.check_unused_connections
|
||||||
|
end
|
||||||
|
|
||||||
|
# check open unused connections, kick all connection without activitie in the last 2 minutes
|
||||||
|
EventMachine.add_periodic_timer(120) do
|
||||||
|
WebsocketServer.check_unused_connections
|
||||||
|
end
|
||||||
|
|
||||||
|
EventMachine.add_periodic_timer(20) do
|
||||||
|
WebsocketServer.log_status
|
||||||
|
end
|
||||||
|
|
||||||
|
EventMachine.add_periodic_timer(0.4) do
|
||||||
|
WebsocketServer.send_to_client
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.onopen(websocket, handshake)
|
||||||
|
headers = handshake.headers
|
||||||
|
remote_ip = get_remote_ip(headers)
|
||||||
|
client_id = websocket.object_id.to_s
|
||||||
|
log 'notice', 'Client connected.', client_id
|
||||||
|
Sessions.create( client_id, {}, { type: 'websocket' } )
|
||||||
|
|
||||||
|
return if @clients.include? client_id
|
||||||
|
|
||||||
|
@clients[client_id] = {
|
||||||
|
websocket: websocket,
|
||||||
|
last_ping: Time.now.utc.to_i,
|
||||||
|
error_count: 0,
|
||||||
|
headers: headers,
|
||||||
|
remote_ip: remote_ip,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.onclose(websocket)
|
||||||
|
client_id = websocket.object_id.to_s
|
||||||
|
log 'notice', 'Client disconnected.', client_id
|
||||||
|
|
||||||
|
# removed from current client list
|
||||||
|
if @clients.include? client_id
|
||||||
|
@clients.delete client_id
|
||||||
|
end
|
||||||
|
|
||||||
|
Sessions.destroy(client_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.onmessage(websocket, msg)
|
||||||
|
client_id = websocket.object_id.to_s
|
||||||
|
log 'debug', "received: #{msg} ", client_id
|
||||||
|
begin
|
||||||
|
data = JSON.parse(msg)
|
||||||
|
rescue => e
|
||||||
|
log 'error', "can't parse message: #{msg}, #{e.inspect}", client_id
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# check if connection not already exists
|
||||||
|
return if !@clients[client_id]
|
||||||
|
|
||||||
|
Sessions.touch(client_id) # rubocop:disable Rails/SkipsModelValidations
|
||||||
|
@clients[client_id][:last_ping] = Time.now.utc.to_i
|
||||||
|
|
||||||
|
# spool messages for new connects
|
||||||
|
if data['spool']
|
||||||
|
Sessions.spool_create(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
if data['event']
|
||||||
|
log 'debug', "execute event '#{data['event']}'", client_id
|
||||||
|
message = Sessions::Event.run(
|
||||||
|
event: data['event'],
|
||||||
|
payload: data,
|
||||||
|
session: @clients[client_id][:session],
|
||||||
|
remote_ip: @clients[client_id][:remote_ip],
|
||||||
|
client_id: client_id,
|
||||||
|
clients: @clients,
|
||||||
|
options: @options,
|
||||||
|
)
|
||||||
|
if message
|
||||||
|
websocket_send(client_id, message)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log 'error', "unknown message '#{data.inspect}'", client_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.get_remote_ip(headers)
|
||||||
|
return headers['X-Forwarded-For'] if headers && headers['X-Forwarded-For']
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.websocket_send(client_id, data)
|
||||||
|
msg = if data.class != Array
|
||||||
|
"[#{data.to_json}]"
|
||||||
|
else
|
||||||
|
data.to_json
|
||||||
|
end
|
||||||
|
log 'debug', "send #{msg}", client_id
|
||||||
|
if !@clients[client_id]
|
||||||
|
log 'error', "no such @clients for #{client_id}", client_id
|
||||||
|
return
|
||||||
|
end
|
||||||
|
@clients[client_id][:websocket].send(msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.check_unused_connections
|
||||||
|
log 'notice', 'check unused idle connections...'
|
||||||
|
|
||||||
|
idle_time_in_sec = 4 * 60
|
||||||
|
|
||||||
|
# close unused web socket sessions
|
||||||
|
@clients.each do |client_id, client|
|
||||||
|
|
||||||
|
next if ( client[:last_ping].to_i + idle_time_in_sec ) >= Time.now.utc.to_i
|
||||||
|
|
||||||
|
log 'notice', 'closing idle websocket connection', client_id
|
||||||
|
|
||||||
|
# remember to not use this connection anymore
|
||||||
|
client[:disconnect] = true
|
||||||
|
|
||||||
|
# try to close regular
|
||||||
|
client[:websocket].close_websocket
|
||||||
|
|
||||||
|
# delete session from client list
|
||||||
|
sleep 0.3
|
||||||
|
@clients.delete(client_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# close unused ajax long polling sessions
|
||||||
|
clients = Sessions.destroy_idle_sessions(idle_time_in_sec)
|
||||||
|
clients.each do |client_id|
|
||||||
|
log 'notice', 'closing idle long polling connection', client_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.send_to_client
|
||||||
|
return if @clients.size.zero?
|
||||||
|
|
||||||
|
#log 'debug', 'checking for data to send...'
|
||||||
|
@clients.each do |client_id, client|
|
||||||
|
next if client[:disconnect]
|
||||||
|
|
||||||
|
log 'debug', 'checking for data...', client_id
|
||||||
|
begin
|
||||||
|
queue = Sessions.queue(client_id)
|
||||||
|
next if queue.blank?
|
||||||
|
|
||||||
|
log 'notice', 'send data to client', client_id
|
||||||
|
websocket_send(client_id, queue)
|
||||||
|
rescue => e
|
||||||
|
log 'error', 'problem:' + e.inspect, client_id
|
||||||
|
|
||||||
|
# disconnect client
|
||||||
|
client[:error_count] += 1
|
||||||
|
if client[:error_count] > 20
|
||||||
|
if @clients.include? client_id
|
||||||
|
@clients.delete client_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.log_status
|
||||||
|
# websocket
|
||||||
|
log 'notice', "Status: websocket clients: #{@clients.size}"
|
||||||
|
@clients.each_key do |client_id|
|
||||||
|
log 'notice', 'working...', client_id
|
||||||
|
end
|
||||||
|
|
||||||
|
# ajax
|
||||||
|
client_list = Sessions.list
|
||||||
|
clients = 0
|
||||||
|
client_list.each_value do |client|
|
||||||
|
next if client[:meta][:type] == 'websocket'
|
||||||
|
|
||||||
|
clients = clients + 1
|
||||||
|
end
|
||||||
|
log 'notice', "Status: ajax clients: #{clients}"
|
||||||
|
client_list.each do |client_id, client|
|
||||||
|
next if client[:meta][:type] == 'websocket'
|
||||||
|
|
||||||
|
log 'notice', 'working...', client_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.log(level, data, client_id = '-')
|
||||||
|
if !@options[:v]
|
||||||
|
return if level == 'debug'
|
||||||
|
end
|
||||||
|
puts "#{Time.now.utc.iso8601}:client(#{client_id}) #{data}" # rubocop:disable Rails/Output
|
||||||
|
#puts "#{Time.now.utc.iso8601}:#{ level }:client(#{ client_id }) #{ data }"
|
||||||
|
end
|
||||||
|
end
|
|
@ -87,10 +87,12 @@ OptionParser.new do |opts|
|
||||||
@options[:i] = i
|
@options[:i] = i
|
||||||
end
|
end
|
||||||
opts.on('-k', '--private-key [OPT]', '/path/to/server.key for secure connections') do |k|
|
opts.on('-k', '--private-key [OPT]', '/path/to/server.key for secure connections') do |k|
|
||||||
tls_options[:private_key_file] = k
|
options[:tls_options] ||= {}
|
||||||
|
options[:tls_options][:private_key_file] = k
|
||||||
end
|
end
|
||||||
opts.on('-c', '--certificate [OPT]', '/path/to/server.crt for secure connections') do |c|
|
opts.on('-c', '--certificate [OPT]', '/path/to/server.crt for secure connections') do |c|
|
||||||
tls_options[:cert_chain_file] = c
|
options[:tls_options] ||= {}
|
||||||
|
options[:tls_options][:cert_chain_file] = c
|
||||||
end
|
end
|
||||||
end.parse!
|
end.parse!
|
||||||
|
|
||||||
|
@ -125,205 +127,4 @@ if ARGV[0] == 'start' && @options[:d]
|
||||||
after_fork(dir)
|
after_fork(dir)
|
||||||
end
|
end
|
||||||
|
|
||||||
@clients = {}
|
WebsocketServer.run(@options)
|
||||||
Rails.configuration.interface = 'websocket'
|
|
||||||
EventMachine.run do
|
|
||||||
EventMachine::WebSocket.start( host: @options[:b], port: @options[:p], secure: @options[:s], tls_options: tls_options ) do |ws|
|
|
||||||
|
|
||||||
# register client connection
|
|
||||||
ws.onopen do |handshake|
|
|
||||||
headers = handshake.headers
|
|
||||||
remote_ip = get_remote_ip(headers)
|
|
||||||
client_id = ws.object_id.to_s
|
|
||||||
log 'notice', 'Client connected.', client_id
|
|
||||||
Sessions.create( client_id, {}, { type: 'websocket' } )
|
|
||||||
|
|
||||||
if !@clients.include? client_id
|
|
||||||
@clients[client_id] = {
|
|
||||||
websocket: ws,
|
|
||||||
last_ping: Time.now.utc.to_i,
|
|
||||||
error_count: 0,
|
|
||||||
headers: headers,
|
|
||||||
remote_ip: remote_ip,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# unregister client connection
|
|
||||||
ws.onclose do
|
|
||||||
client_id = ws.object_id.to_s
|
|
||||||
log 'notice', 'Client disconnected.', client_id
|
|
||||||
|
|
||||||
# removed from current client list
|
|
||||||
if @clients.include? client_id
|
|
||||||
@clients.delete client_id
|
|
||||||
end
|
|
||||||
|
|
||||||
Sessions.destroy(client_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
# manage messages
|
|
||||||
ws.onmessage do |msg|
|
|
||||||
|
|
||||||
client_id = ws.object_id.to_s
|
|
||||||
log 'debug', "received: #{msg} ", client_id
|
|
||||||
begin
|
|
||||||
data = JSON.parse(msg)
|
|
||||||
rescue => e
|
|
||||||
log 'error', "can't parse message: #{msg}, #{e.inspect}", client_id
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
# check if connection not already exists
|
|
||||||
next if !@clients[client_id]
|
|
||||||
|
|
||||||
Sessions.touch(client_id) # rubocop:disable Rails/SkipsModelValidations
|
|
||||||
@clients[client_id][:last_ping] = Time.now.utc.to_i
|
|
||||||
|
|
||||||
# spool messages for new connects
|
|
||||||
if data['spool']
|
|
||||||
Sessions.spool_create(data)
|
|
||||||
end
|
|
||||||
|
|
||||||
if data['event']
|
|
||||||
log 'debug', "execute event '#{data['event']}'", client_id
|
|
||||||
message = Sessions::Event.run(
|
|
||||||
event: data['event'],
|
|
||||||
payload: data,
|
|
||||||
session: @clients[client_id][:session],
|
|
||||||
remote_ip: @clients[client_id][:remote_ip],
|
|
||||||
client_id: client_id,
|
|
||||||
clients: @clients,
|
|
||||||
options: @options,
|
|
||||||
)
|
|
||||||
if message
|
|
||||||
websocket_send(client_id, message)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
log 'error', "unknown message '#{data.inspect}'", client_id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# check unused connections
|
|
||||||
EventMachine.add_timer(0.5) do
|
|
||||||
check_unused_connections
|
|
||||||
end
|
|
||||||
|
|
||||||
# check open unused connections, kick all connection without activitie in the last 2 minutes
|
|
||||||
EventMachine.add_periodic_timer(120) do
|
|
||||||
check_unused_connections
|
|
||||||
end
|
|
||||||
|
|
||||||
EventMachine.add_periodic_timer(20) do
|
|
||||||
|
|
||||||
# websocket
|
|
||||||
log 'notice', "Status: websocket clients: #{@clients.size}"
|
|
||||||
@clients.each_key do |client_id|
|
|
||||||
log 'notice', 'working...', client_id
|
|
||||||
end
|
|
||||||
|
|
||||||
# ajax
|
|
||||||
client_list = Sessions.list
|
|
||||||
clients = 0
|
|
||||||
client_list.each_value do |client|
|
|
||||||
next if client[:meta][:type] == 'websocket'
|
|
||||||
|
|
||||||
clients = clients + 1
|
|
||||||
end
|
|
||||||
log 'notice', "Status: ajax clients: #{clients}"
|
|
||||||
client_list.each do |client_id, client|
|
|
||||||
next if client[:meta][:type] == 'websocket'
|
|
||||||
|
|
||||||
log 'notice', 'working...', client_id
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
EventMachine.add_periodic_timer(0.4) do
|
|
||||||
next if @clients.size.zero?
|
|
||||||
|
|
||||||
#log 'debug', 'checking for data to send...'
|
|
||||||
@clients.each do |client_id, client|
|
|
||||||
next if client[:disconnect]
|
|
||||||
|
|
||||||
log 'debug', 'checking for data...', client_id
|
|
||||||
begin
|
|
||||||
queue = Sessions.queue(client_id)
|
|
||||||
next if queue.blank?
|
|
||||||
|
|
||||||
log 'notice', 'send data to client', client_id
|
|
||||||
websocket_send(client_id, queue)
|
|
||||||
rescue => e
|
|
||||||
log 'error', 'problem:' + e.inspect, client_id
|
|
||||||
|
|
||||||
# disconnect client
|
|
||||||
client[:error_count] += 1
|
|
||||||
if client[:error_count] > 20
|
|
||||||
if @clients.include? client_id
|
|
||||||
@clients.delete client_id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_remote_ip(headers)
|
|
||||||
return headers['X-Forwarded-For'] if headers && headers['X-Forwarded-For']
|
|
||||||
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def websocket_send(client_id, data)
|
|
||||||
msg = if data.class != Array
|
|
||||||
"[#{data.to_json}]"
|
|
||||||
else
|
|
||||||
data.to_json
|
|
||||||
end
|
|
||||||
log 'debug', "send #{msg}", client_id
|
|
||||||
if !@clients[client_id]
|
|
||||||
log 'error', "no such @clients for #{client_id}", client_id
|
|
||||||
return
|
|
||||||
end
|
|
||||||
@clients[client_id][:websocket].send(msg)
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_unused_connections
|
|
||||||
log 'notice', 'check unused idle connections...'
|
|
||||||
|
|
||||||
idle_time_in_sec = 4 * 60
|
|
||||||
|
|
||||||
# close unused web socket sessions
|
|
||||||
@clients.each do |client_id, client|
|
|
||||||
|
|
||||||
next if ( client[:last_ping].to_i + idle_time_in_sec ) >= Time.now.utc.to_i
|
|
||||||
|
|
||||||
log 'notice', 'closing idle websocket connection', client_id
|
|
||||||
|
|
||||||
# remember to not use this connection anymore
|
|
||||||
client[:disconnect] = true
|
|
||||||
|
|
||||||
# try to close regular
|
|
||||||
client[:websocket].close_websocket
|
|
||||||
|
|
||||||
# delete session from client list
|
|
||||||
sleep 0.3
|
|
||||||
@clients.delete(client_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
# close unused ajax long polling sessions
|
|
||||||
clients = Sessions.destroy_idle_sessions(idle_time_in_sec)
|
|
||||||
clients.each do |client_id|
|
|
||||||
log 'notice', 'closing idle long polling connection', client_id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def log(level, data, client_id = '-')
|
|
||||||
if !@options[:v]
|
|
||||||
return if level == 'debug'
|
|
||||||
end
|
|
||||||
puts "#{Time.now.utc.iso8601}:client(#{client_id}) #{data}"
|
|
||||||
#puts "#{Time.now.utc.iso8601}:#{ level }:client(#{ client_id }) #{ data }"
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
25
spec/support/capybara/authenticated.rb
Normal file
25
spec/support/capybara/authenticated.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# This file registers a hook before each system test
|
||||||
|
# which logs in with/authenticates the master@example.com account.
|
||||||
|
|
||||||
|
# we need to make sure that Capybara is configured/started before
|
||||||
|
# this hook. Otherwise a login try is performed while the app/puma
|
||||||
|
# hasn't started yet.
|
||||||
|
require_relative './driven_by'
|
||||||
|
|
||||||
|
RSpec.configure do |config|
|
||||||
|
|
||||||
|
config.before(:each, type: :system) do |example|
|
||||||
|
|
||||||
|
# there is no way to authenticated in a not set up system
|
||||||
|
next if !example.metadata.fetch(:set_up, true)
|
||||||
|
|
||||||
|
# check if authentication should be performed
|
||||||
|
next if example.metadata.fetch(:authenticated, true).blank?
|
||||||
|
|
||||||
|
# authenticate
|
||||||
|
login(
|
||||||
|
username: 'master@example.com',
|
||||||
|
password: 'test',
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
72
spec/support/capybara/browser_test_helper.rb
Normal file
72
spec/support/capybara/browser_test_helper.rb
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
module BrowserTestHelper
|
||||||
|
|
||||||
|
# Finds an element and clicks it - wrapped in one method.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# click('.js-channel .btn.email')
|
||||||
|
#
|
||||||
|
# click(:href, '#settings/branding')
|
||||||
|
#
|
||||||
|
def click(*args)
|
||||||
|
find(*args).click
|
||||||
|
end
|
||||||
|
|
||||||
|
# This is a wrapper around the Selenium::WebDriver::Wait class
|
||||||
|
# with additional methods.
|
||||||
|
# @see BrowserTestHelper::Waiter
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# wait(5).until { ... }
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# wait(5, interval: 0.5).until { ... }
|
||||||
|
#
|
||||||
|
def wait(seconds = Capybara.default_max_wait_time, **kargs)
|
||||||
|
wait_args = Hash(kargs).merge(timeout: seconds)
|
||||||
|
wait_handle = Selenium::WebDriver::Wait.new(wait_args)
|
||||||
|
Waiter.new(wait_handle)
|
||||||
|
end
|
||||||
|
|
||||||
|
class Waiter < SimpleDelegator
|
||||||
|
|
||||||
|
# This method is a derivation of Selenium::WebDriver::Wait#until
|
||||||
|
# which ignores Capybara::ElementNotFound exceptions raised
|
||||||
|
# in the given block.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# wait(5).until_exists { find('[data-title="example"]') }
|
||||||
|
#
|
||||||
|
def until_exists
|
||||||
|
self.until do
|
||||||
|
begin
|
||||||
|
yield
|
||||||
|
rescue Capybara::ElementNotFound # rubocop:disable Lint/HandleExceptions
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue Selenium::WebDriver::Error::TimeOutError => e
|
||||||
|
# cleanup backtrace
|
||||||
|
e.set_backtrace(e.backtrace.drop(3))
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method loops a given block until the result of it is constant.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# wait(5).until_constant { find('.total').text }
|
||||||
|
#
|
||||||
|
def until_constant
|
||||||
|
previous = nil
|
||||||
|
loop do
|
||||||
|
sleep __getobj__.instance_variable_get(:@interval)
|
||||||
|
latest = yield
|
||||||
|
break if latest == previous
|
||||||
|
|
||||||
|
previous = latest
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.configure do |config|
|
||||||
|
config.include BrowserTestHelper, type: :system
|
||||||
|
end
|
131
spec/support/capybara/common_actions.rb
Normal file
131
spec/support/capybara/common_actions.rb
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
module CommonActions
|
||||||
|
|
||||||
|
delegate :app_host, to: Capybara
|
||||||
|
|
||||||
|
# Performs a login with the given credentials and closes the clues (if present).
|
||||||
|
# The 'remember me' can optionally be checked.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# login(
|
||||||
|
# username: 'master@example.com',
|
||||||
|
# password: 'test',
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# login(
|
||||||
|
# username: 'master@example.com',
|
||||||
|
# password: 'test',
|
||||||
|
# remember_me: true,
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# return [nil]
|
||||||
|
def login(username:, password:, remember_me: false)
|
||||||
|
visit '/'
|
||||||
|
|
||||||
|
within('#login') do
|
||||||
|
fill_in 'username', with: username
|
||||||
|
fill_in 'password', with: password
|
||||||
|
|
||||||
|
# check via label because checkbox is hidden
|
||||||
|
click('.checkbox-replacement') if remember_me
|
||||||
|
|
||||||
|
# submit
|
||||||
|
click_button
|
||||||
|
end
|
||||||
|
|
||||||
|
wait(4).until_exists do
|
||||||
|
current_login
|
||||||
|
end
|
||||||
|
|
||||||
|
return if User.find_by(login: current_login).preferences[:intro]
|
||||||
|
|
||||||
|
find(:clues_close, wait: 3).in_fixed_postion.click
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if the current session is logged in.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# logged_in?
|
||||||
|
# => true
|
||||||
|
#
|
||||||
|
# @return [true, false]
|
||||||
|
def logged_in?
|
||||||
|
current_login.present?
|
||||||
|
rescue Capybara::ElementNotFound
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the login of the currently logged in user.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# current_login
|
||||||
|
# => 'master@example.com'
|
||||||
|
#
|
||||||
|
# @return [String] the login of the currently logged in user.
|
||||||
|
def current_login
|
||||||
|
find('.user-menu .user a')[:title]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Logs out the currently logged in user.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# logout
|
||||||
|
#
|
||||||
|
def logout
|
||||||
|
visit('logout')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Overwrites the Capybara::Session#visit method to allow SPA navigation.
|
||||||
|
# All routes not starting with `/` will be handled as SPA routes.
|
||||||
|
#
|
||||||
|
# @see Capybara::Session#visit
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# visit('logout')
|
||||||
|
# => visited SPA route '/#logout'
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# visit('/test/ui')
|
||||||
|
# => visited regular route '/test/ui'
|
||||||
|
#
|
||||||
|
def visit(route)
|
||||||
|
if !route.start_with?('/')
|
||||||
|
route = "/##{route}"
|
||||||
|
end
|
||||||
|
super(route)
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method is equivalent to Capybara::RSpecMatchers#have_current_path
|
||||||
|
# but checks the SPA route instead of the actual path.
|
||||||
|
#
|
||||||
|
# @see Capybara::RSpecMatchers#have_current_path
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# expect(page).to have_current_route('login')
|
||||||
|
# => checks for SPA route '/#login'
|
||||||
|
#
|
||||||
|
def have_current_route(route, **options)
|
||||||
|
if route.is_a?(String)
|
||||||
|
route = Regexp.new(Regexp.quote("/##{route}"))
|
||||||
|
end
|
||||||
|
|
||||||
|
options.reverse_merge!(wait: 0, url: true)
|
||||||
|
|
||||||
|
have_current_path("/##{route}", **options)
|
||||||
|
end
|
||||||
|
|
||||||
|
# This is a convenient wrapper method around #have_current_route
|
||||||
|
# which requires no previous `expect(page).to ` call.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# expect_current_routes('login')
|
||||||
|
# => checks for SPA route '/#login'
|
||||||
|
#
|
||||||
|
def expect_current_route(route, **options)
|
||||||
|
expect(page).to have_current_route(route, **options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.configure do |config|
|
||||||
|
config.include CommonActions, type: :system
|
||||||
|
end
|
4
spec/support/capybara/config.rb
Normal file
4
spec/support/capybara/config.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
Capybara.configure do |config|
|
||||||
|
config.always_include_port = true
|
||||||
|
config.default_max_wait_time = 16
|
||||||
|
end
|
34
spec/support/capybara/custom_extensions.rb
Normal file
34
spec/support/capybara/custom_extensions.rb
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
class Capybara::Node::Element
|
||||||
|
|
||||||
|
# This is an extension to each node to check if the element
|
||||||
|
# is moving or in a fixed position. This is especially helpful
|
||||||
|
# for animated elements that cause flanky tests.
|
||||||
|
# NOTE: In CI env a special sleep is performed between checks
|
||||||
|
# because animations can be slow.
|
||||||
|
#
|
||||||
|
# @param [Integer] checks the number of performed movement checks
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# find('.clues-close').in_fixed_postion.click
|
||||||
|
# => waits till clues moved to final position and performs click afterwards
|
||||||
|
#
|
||||||
|
# @raise [RuntimeError] raised in case the element is
|
||||||
|
# still moving after max number of checks was passed
|
||||||
|
#
|
||||||
|
# @return [Capybara::Node::Element] the element/node
|
||||||
|
def in_fixed_postion(checks: 100)
|
||||||
|
|
||||||
|
previous = native.location
|
||||||
|
(checks + 1).times do |check|
|
||||||
|
raise "Element still moving after #{checks} checks" if check == checks
|
||||||
|
|
||||||
|
current = native.location
|
||||||
|
sleep 0.2 if ENV['CI']
|
||||||
|
break if previous == current
|
||||||
|
|
||||||
|
previous = current
|
||||||
|
end
|
||||||
|
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
19
spec/support/capybara/driven_by.rb
Normal file
19
spec/support/capybara/driven_by.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
require_relative './set_up'
|
||||||
|
|
||||||
|
RSpec.configure do |config|
|
||||||
|
config.before(:each, type: :system) do
|
||||||
|
|
||||||
|
# start a silenced Puma as application server
|
||||||
|
Capybara.server = :puma, { Silent: true, Host: '0.0.0.0' }
|
||||||
|
|
||||||
|
# set the Host from gather container IP for CI runs
|
||||||
|
if ENV['CI'].present?
|
||||||
|
ip_address = Socket.ip_address_list.detect(&:ipv4_private?).ip_address
|
||||||
|
host!("http://#{ip_address}")
|
||||||
|
end
|
||||||
|
|
||||||
|
# set custom Zammad driver (e.g. zammad_chrome) for special
|
||||||
|
# functionalities and CI requirements
|
||||||
|
driven_by("zammad_#{ENV.fetch('BROWSER', 'firefox')}".to_sym)
|
||||||
|
end
|
||||||
|
end
|
21
spec/support/capybara/selectors.rb
Normal file
21
spec/support/capybara/selectors.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# This file defines custom Capybara selectors for DRYed specs.
|
||||||
|
|
||||||
|
Capybara.add_selector(:href) do
|
||||||
|
css { |href| %(a[href="#{href}"]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
Capybara.add_selector(:active_content) do
|
||||||
|
css { |content_class| ['.content.active', content_class].compact.join(' ') }
|
||||||
|
end
|
||||||
|
|
||||||
|
Capybara.add_selector(:manage) do
|
||||||
|
css { 'a[href="#manage"]' }
|
||||||
|
end
|
||||||
|
|
||||||
|
Capybara.add_selector(:clues_close) do
|
||||||
|
css { '.js-modal--clue .js-close' }
|
||||||
|
end
|
||||||
|
|
||||||
|
Capybara.add_selector(:richtext) do
|
||||||
|
css { |name| "div[data-name=#{name}]" }
|
||||||
|
end
|
49
spec/support/capybara/selenium_driver.rb
Normal file
49
spec/support/capybara/selenium_driver.rb
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# This file registers the custom Zammad chrome and firefox drivers.
|
||||||
|
# The options check if a REMOTE_URL ENV is given and change the
|
||||||
|
# configurations accordingly.
|
||||||
|
|
||||||
|
Capybara.register_driver(:zammad_chrome) do |app|
|
||||||
|
|
||||||
|
# Turn on browser logs
|
||||||
|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
|
||||||
|
loggingPrefs: {
|
||||||
|
browser: 'ALL'
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
options = {
|
||||||
|
browser: :chrome,
|
||||||
|
desired_capabilities: capabilities,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ENV['REMOTE_URL'].present?
|
||||||
|
options[:browser] = :remote
|
||||||
|
options[:url] = ENV['REMOTE_URL']
|
||||||
|
end
|
||||||
|
|
||||||
|
Capybara::Selenium::Driver.new(app, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
Capybara.register_driver(:zammad_firefox) do |app|
|
||||||
|
|
||||||
|
profile = Selenium::WebDriver::Firefox::Profile.new
|
||||||
|
profile['intl.locale.matchOS'] = false
|
||||||
|
profile['intl.accept_languages'] = 'en-US'
|
||||||
|
profile['general.useragent.locale'] = 'en-US'
|
||||||
|
|
||||||
|
capabilities = Selenium::WebDriver::Remote::Capabilities.firefox(
|
||||||
|
firefox_profile: profile,
|
||||||
|
)
|
||||||
|
|
||||||
|
options = {
|
||||||
|
browser: :firefox,
|
||||||
|
desired_capabilities: capabilities,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ENV['REMOTE_URL'].present?
|
||||||
|
options[:browser] = :remote
|
||||||
|
options[:url] = ENV['REMOTE_URL']
|
||||||
|
end
|
||||||
|
|
||||||
|
Capybara::Selenium::Driver.new(app, options)
|
||||||
|
end
|
24
spec/support/capybara/set_up.rb
Normal file
24
spec/support/capybara/set_up.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
RSpec.configure do |config|
|
||||||
|
config.before(:each, type: :system) do |example|
|
||||||
|
|
||||||
|
# make sure system is in a fresh state
|
||||||
|
Cache.clear
|
||||||
|
Setting.reload
|
||||||
|
|
||||||
|
# check if system is already set up
|
||||||
|
next if Setting.get('system_init_done')
|
||||||
|
|
||||||
|
# check if system should get set up
|
||||||
|
next if !example.metadata.fetch(:set_up, true)
|
||||||
|
|
||||||
|
# perform setup via auto_wizard
|
||||||
|
Rake::Task['zammad:setup:auto_wizard'].execute
|
||||||
|
|
||||||
|
# skip intro/clues for created agents/admins
|
||||||
|
%w[master@example.com agent1@example.com].each do |login|
|
||||||
|
user = User.find_by(login: login)
|
||||||
|
user.preferences[:intro] = true
|
||||||
|
user.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
24
spec/support/capybara/websocket_server.rb
Normal file
24
spec/support/capybara/websocket_server.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
RSpec.configure do |config|
|
||||||
|
config.around(:each, type: :system) do |example|
|
||||||
|
|
||||||
|
server_required = example.metadata.fetch(:websocket, true)
|
||||||
|
|
||||||
|
if server_required
|
||||||
|
websocket_server = Thread.new do
|
||||||
|
WebsocketServer.run(
|
||||||
|
p: ENV['WS_PORT'] || 6042,
|
||||||
|
b: '0.0.0.0',
|
||||||
|
s: false,
|
||||||
|
v: false,
|
||||||
|
d: false,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
example.run
|
||||||
|
|
||||||
|
next if !server_required
|
||||||
|
|
||||||
|
Thread.kill(websocket_server)
|
||||||
|
end
|
||||||
|
end
|
18
spec/system/basic/authentication_spec.rb
Normal file
18
spec/system/basic/authentication_spec.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Authentication', type: :system do
|
||||||
|
|
||||||
|
scenario 'Login', authenticated: false do
|
||||||
|
login(
|
||||||
|
username: 'master@example.com',
|
||||||
|
password: 'test',
|
||||||
|
)
|
||||||
|
|
||||||
|
have_current_route 'dashboard'
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Logout' do
|
||||||
|
logout
|
||||||
|
have_current_route 'login', wait: 2
|
||||||
|
end
|
||||||
|
end
|
24
spec/system/basic/redirects_spec.rb
Normal file
24
spec/system/basic/redirects_spec.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Unauthenticated redirect', type: :system, authenticated: false do
|
||||||
|
|
||||||
|
scenario 'Sessions' do
|
||||||
|
visit 'system/sessions'
|
||||||
|
have_current_route 'login'
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Profile' do
|
||||||
|
visit 'profile/linked'
|
||||||
|
have_current_route 'login'
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Ticket' do
|
||||||
|
visit 'ticket/zoom/1'
|
||||||
|
have_current_route 'login'
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Not existing route' do
|
||||||
|
visit 'not_existing'
|
||||||
|
have_current_route 'not_existing'
|
||||||
|
end
|
||||||
|
end
|
55
spec/system/basic/richtext_spec.rb
Normal file
55
spec/system/basic/richtext_spec.rb
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Richtext', type: :system do
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
click(:href, '#current_user')
|
||||||
|
click(:href, '#layout_ref')
|
||||||
|
click(:href, '#layout_ref/richtext')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'Richtext' do
|
||||||
|
|
||||||
|
scenario 'Single line mode' do
|
||||||
|
|
||||||
|
element = find('#content .text-1')
|
||||||
|
|
||||||
|
element.send_keys(
|
||||||
|
'some test for browser ',
|
||||||
|
:enter,
|
||||||
|
'and some other for browser'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(element).to have_content('some test for browser and some other for browser')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Multi line mode' do
|
||||||
|
|
||||||
|
element = find('#content .text-5')
|
||||||
|
|
||||||
|
element.send_keys(
|
||||||
|
'some test for browser ',
|
||||||
|
:enter,
|
||||||
|
'and some other for browser'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(element).to have_content("some test for browser \nand some other for browser")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'Regular text' do
|
||||||
|
|
||||||
|
scenario 'Multi line mode' do
|
||||||
|
|
||||||
|
element = find('#content .text-3')
|
||||||
|
|
||||||
|
element.send_keys(
|
||||||
|
'some test for browser ',
|
||||||
|
:enter,
|
||||||
|
'and some other for browser'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(element).to have_content("some test for browser \nand some other for browser")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
112
spec/system/js/q_unit_spec.rb
Normal file
112
spec/system/js/q_unit_spec.rb
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'QUnit', type: :system, authenticated: false, set_up: true, websocket: false do
|
||||||
|
|
||||||
|
def q_unit_tests(test_name)
|
||||||
|
|
||||||
|
visit "/tests_#{test_name}"
|
||||||
|
|
||||||
|
yield if block_given?
|
||||||
|
|
||||||
|
expect(page).to have_css('.result', text: 'Tests completed')
|
||||||
|
expect(page).to have_css('.result .failed', text: '0')
|
||||||
|
end
|
||||||
|
|
||||||
|
def async_q_unit_tests(*args)
|
||||||
|
q_unit_tests(*args) do
|
||||||
|
wait(10, interval: 4).until_constant do
|
||||||
|
find('.total').text
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Core' do
|
||||||
|
async_q_unit_tests('core')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'UI' do
|
||||||
|
|
||||||
|
scenario 'Base' do
|
||||||
|
q_unit_tests('ui')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Model' do
|
||||||
|
async_q_unit_tests('model')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Model binding' do
|
||||||
|
q_unit_tests('model_binding')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Model UI' do
|
||||||
|
|
||||||
|
if !ENV['CI']
|
||||||
|
skip("Can't run locally because of dependence of special Timezone")
|
||||||
|
end
|
||||||
|
|
||||||
|
q_unit_tests('model_ui')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Ticket selector' do
|
||||||
|
q_unit_tests('ticket_selector')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'Form' do
|
||||||
|
|
||||||
|
scenario 'Base' do
|
||||||
|
async_q_unit_tests('form')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Trim' do
|
||||||
|
q_unit_tests('form_trim')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Find' do
|
||||||
|
q_unit_tests('form_find')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Timer' do
|
||||||
|
q_unit_tests('form_timer')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Extended' do
|
||||||
|
q_unit_tests('form_extended')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Searchable select' do
|
||||||
|
q_unit_tests('form_searchable_select')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Tree select' do
|
||||||
|
q_unit_tests('form_tree_select')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Column select' do
|
||||||
|
q_unit_tests('form_column_select')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Validation' do
|
||||||
|
q_unit_tests('form_validation')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'Table' do
|
||||||
|
|
||||||
|
scenario 'Base' do
|
||||||
|
q_unit_tests('table')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Extended' do
|
||||||
|
q_unit_tests('table_extended')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'HTML utils' do
|
||||||
|
q_unit_tests('html_utils')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Taskbar' do
|
||||||
|
q_unit_tests('taskbar')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
17
spec/system/setup/auto_wizard_spec.rb
Normal file
17
spec/system/setup/auto_wizard_spec.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Auto wizard', type: :system, set_up: false do
|
||||||
|
|
||||||
|
scenario 'Automatic setup and login' do
|
||||||
|
|
||||||
|
FileUtils.ln(
|
||||||
|
Rails.root.join('contrib', 'auto_wizard_test.json'),
|
||||||
|
Rails.root.join('auto_wizard.json'),
|
||||||
|
force: true
|
||||||
|
)
|
||||||
|
|
||||||
|
visit 'getting_started/auto_wizard'
|
||||||
|
|
||||||
|
expect(current_login).to eq('master@example.com')
|
||||||
|
end
|
||||||
|
end
|
109
spec/system/setup/mail_accounts_spec.rb
Normal file
109
spec/system/setup/mail_accounts_spec.rb
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Mail accounts', type: :system do
|
||||||
|
|
||||||
|
def perform_check
|
||||||
|
# getting started - auto mail
|
||||||
|
visit 'getting_started/channel'
|
||||||
|
|
||||||
|
click('.js-channel .btn.email')
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
# wait for verification process to finish
|
||||||
|
expect(page).to have_css('.js-agent h2', text: 'Invite Colleagues', wait: 4.minutes)
|
||||||
|
|
||||||
|
have_current_route 'getting_started/agents'
|
||||||
|
end
|
||||||
|
|
||||||
|
def fill_in_credentials(account)
|
||||||
|
within('.js-intro') do
|
||||||
|
|
||||||
|
fill_in 'realname', with: account[:realname]
|
||||||
|
fill_in 'email', with: account[:email]
|
||||||
|
fill_in 'password', with: account[:password]
|
||||||
|
|
||||||
|
click_on('Connect')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Auto detectable configurations' do
|
||||||
|
|
||||||
|
skip('NOTICE: This test is currently disabled because of collisions with other non Capybara browser tests')
|
||||||
|
|
||||||
|
accounts = (1..10).each_with_object([]) do |count, result|
|
||||||
|
next if !ENV["MAILBOX_AUTO#{count}"]
|
||||||
|
|
||||||
|
email, password = ENV["MAILBOX_AUTO#{count}"].split(':')
|
||||||
|
result.push(
|
||||||
|
realname: 'auto account',
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if accounts.blank?
|
||||||
|
skip("NOTICE: Need min. MAILBOX_AUTO1 as ENV variable like export MAILBOX_AUTO1='nicole.braun2015@gmail.com:somepass'")
|
||||||
|
end
|
||||||
|
|
||||||
|
accounts.each do |account|
|
||||||
|
|
||||||
|
perform_check do
|
||||||
|
fill_in_credentials(account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Manual configurations' do
|
||||||
|
|
||||||
|
accounts = (1..10).each_with_object([]) do |count, result|
|
||||||
|
next if !ENV["MAILBOX_MANUAL#{count}"]
|
||||||
|
|
||||||
|
email, password, inbound, outbound = ENV["MAILBOX_MANUAL#{count}"].split(':')
|
||||||
|
|
||||||
|
result.push(
|
||||||
|
realname: 'manual account',
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
inbound: {
|
||||||
|
'options::host' => inbound,
|
||||||
|
},
|
||||||
|
outbound: {
|
||||||
|
'options::host' => outbound,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if accounts.blank?
|
||||||
|
skip("NOTICE: Need min. MAILBOX_MANUAL1 as ENV variable like export MAILBOX_MANUAL1='nicole.bauer2015@yahoo.de:somepass:imap.mail.yahoo.com:smtp.mail.yahoo.com'")
|
||||||
|
end
|
||||||
|
|
||||||
|
accounts.each do |account|
|
||||||
|
|
||||||
|
perform_check do
|
||||||
|
fill_in_credentials(account)
|
||||||
|
|
||||||
|
within('.js-inbound') do
|
||||||
|
|
||||||
|
expect(page).to have_css('h2', text: 'inbound', wait: 4.minutes)
|
||||||
|
expect(page).to have_css('body', text: 'manual')
|
||||||
|
|
||||||
|
fill_in 'options::host', with: account[:inbound]['options::host']
|
||||||
|
|
||||||
|
click_on('Connect')
|
||||||
|
end
|
||||||
|
|
||||||
|
within('.js-outbound') do
|
||||||
|
|
||||||
|
expect(page).to have_css('h2', text: 'outbound', wait: 4.minutes)
|
||||||
|
|
||||||
|
select('SMTP - configure your own outgoing SMTP settings', from: 'adapter')
|
||||||
|
|
||||||
|
fill_in 'options::host', with: account[:outbound]['options::host']
|
||||||
|
|
||||||
|
click_on('Connect')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
113
spec/system/setup/system_spec.rb
Normal file
113
spec/system/setup/system_spec.rb
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'System setup process', type: :system, set_up: false do
|
||||||
|
|
||||||
|
def fqdn
|
||||||
|
match_data = %r{://(.+?)(:.+?|/.+?|)$}.match(app_host)
|
||||||
|
return match_data.captures.first if match_data.present?
|
||||||
|
|
||||||
|
raise "Unable to get fqdn based on #{app_host}"
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Setting up a new system', authenticated: false do
|
||||||
|
|
||||||
|
if !ENV['MAILBOX_INIT']
|
||||||
|
skip("NOTICE: Need MAILBOX_INIT as ENV variable like export MAILBOX_INIT='unittest01@znuny.com:somepass'")
|
||||||
|
end
|
||||||
|
mailbox_user = ENV['MAILBOX_INIT'].split(':')[0]
|
||||||
|
mailbox_password = ENV['MAILBOX_INIT'].split(':')[1]
|
||||||
|
|
||||||
|
visit '/'
|
||||||
|
|
||||||
|
expect(page).to have_css('.setup.wizard', text: 'Setup new System')
|
||||||
|
|
||||||
|
# choose setup (over migration)
|
||||||
|
click_on('Setup new System')
|
||||||
|
|
||||||
|
# admin user form
|
||||||
|
expect(page).to have_css('.js-admin h2', text: 'Administrator Account')
|
||||||
|
|
||||||
|
within('.js-admin') do
|
||||||
|
fill_in 'firstname', with: 'Test Master'
|
||||||
|
fill_in 'lastname', with: 'Agent'
|
||||||
|
fill_in 'email', with: 'master@example.com'
|
||||||
|
fill_in 'password', with: 'test1234äöüß'
|
||||||
|
fill_in 'password_confirm', with: 'test1234äöüß'
|
||||||
|
|
||||||
|
click_on('Create')
|
||||||
|
end
|
||||||
|
|
||||||
|
# configure Organization
|
||||||
|
expect(page).to have_css('.js-base h2', text: 'Organization')
|
||||||
|
within('.js-base') do
|
||||||
|
fill_in 'organization', with: 'Some Organization'
|
||||||
|
|
||||||
|
# fill in wrong URL
|
||||||
|
fill_in 'url', with: 'some host'
|
||||||
|
click_on('Next')
|
||||||
|
expect(page).to have_css('.alert', text: 'A URL looks like')
|
||||||
|
|
||||||
|
# fill in valild/current URL
|
||||||
|
fill_in 'url', with: app_host
|
||||||
|
click_on('Next')
|
||||||
|
end
|
||||||
|
|
||||||
|
# configure Email Notification
|
||||||
|
expect(page).to have_css('.js-outbound h2', text: 'Email Notification')
|
||||||
|
have_current_route 'getting_started/email_notification'
|
||||||
|
click_on('Continue')
|
||||||
|
|
||||||
|
# create email account
|
||||||
|
expect(page).to have_css('.js-channel h2', text: 'Connect Channels')
|
||||||
|
have_current_route 'getting_started/channel'
|
||||||
|
click('.js-channel .btn.email')
|
||||||
|
|
||||||
|
within('.js-intro') do
|
||||||
|
fill_in 'realname', with: 'Some Realname'
|
||||||
|
fill_in 'email', with: mailbox_user
|
||||||
|
fill_in 'password', with: mailbox_password
|
||||||
|
|
||||||
|
click_on('Connect')
|
||||||
|
end
|
||||||
|
|
||||||
|
# wait for verification process to start
|
||||||
|
expect(page).to have_css('body', text: 'Verify sending and receiving', wait: 20)
|
||||||
|
|
||||||
|
# wait for verification process to finish
|
||||||
|
expect(page).to have_css('.js-agent h2', text: 'Invite Colleagues', wait: 2.minutes)
|
||||||
|
have_current_route 'getting_started/agents'
|
||||||
|
|
||||||
|
# invite agent1
|
||||||
|
within('.js-agent') do
|
||||||
|
fill_in 'firstname', with: 'Agent 1'
|
||||||
|
fill_in 'lastname', with: 'Test'
|
||||||
|
fill_in 'email', with: 'agent12@example.com'
|
||||||
|
|
||||||
|
click_on('Invite')
|
||||||
|
end
|
||||||
|
expect(page).to have_css('body', text: 'Invitation sent!')
|
||||||
|
|
||||||
|
# expect to still be on the same page
|
||||||
|
have_current_route 'getting_started/agents'
|
||||||
|
within('.js-agent') do
|
||||||
|
click_on('Continue')
|
||||||
|
end
|
||||||
|
|
||||||
|
# expect Dashboard of a fresh system
|
||||||
|
expect(page).to have_css('body', text: 'My Stats')
|
||||||
|
have_current_route 'clues'
|
||||||
|
find(:clues_close, wait: 4).in_fixed_postion.click
|
||||||
|
|
||||||
|
# verify organization and fqdn
|
||||||
|
click(:manage)
|
||||||
|
|
||||||
|
within(:active_content) do
|
||||||
|
|
||||||
|
click(:href, '#settings/branding')
|
||||||
|
expect(page).to have_field('organization', with: 'Some Organization')
|
||||||
|
|
||||||
|
click(:href, '#settings/system')
|
||||||
|
expect(page).to have_field('fqdn', with: fqdn)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -58,6 +58,9 @@ class ChatTest < ActiveSupport::TestCase
|
||||||
# with websockets
|
# with websockets
|
||||||
assert(User.first)
|
assert(User.first)
|
||||||
|
|
||||||
|
# make sure to emulate unconnected WS env
|
||||||
|
ActiveRecord::Base.remove_connection
|
||||||
|
|
||||||
message = Sessions::Event.run(
|
message = Sessions::Event.run(
|
||||||
event: 'login',
|
event: 'login',
|
||||||
payload: {},
|
payload: {},
|
||||||
|
|
Loading…
Reference in a new issue