From 5745fa46bd86ab9ad8966d9bd7555cce0f007fd1 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Thu, 4 Jul 2019 13:16:55 +0200 Subject: [PATCH] Bumps [rails](https://github.com/rails/rails) from 5.1.7 to 5.2.3. - [Release notes](https://github.com/rails/rails/releases) - [Changelog](https://github.com/rails/rails/blob/master/CHANGELOG.md) - [Commits](https://github.com/rails/rails/compare/v5.1.7...v5.2.3) --- .rubocop.yml | 8 ++ Gemfile | 3 +- Gemfile.lock | 100 ++++++++++-------- .../application_controller/prevents_csrf.rb | 3 + app/controllers/monitoring_controller.rb | 2 +- app/jobs/application_job.rb | 11 +- app/models/activity_stream.rb | 4 +- .../application_model/can_associations.rb | 2 +- app/models/authorization.rb | 2 +- app/models/avatar.rb | 2 +- app/models/channel.rb | 2 +- app/models/concerns/can_be_published.rb | 2 +- .../concerns/has_group_relation_definition.rb | 4 +- app/models/concerns/has_groups.rb | 6 +- app/models/concerns/has_roles.rb | 4 +- app/models/cti/caller_id.rb | 2 +- app/models/email_address.rb | 2 +- app/models/group.rb | 4 +- app/models/history.rb | 6 +- app/models/karma/activity_log.rb | 2 +- app/models/knowledge_base/category.rb | 3 +- app/models/link.rb | 4 +- app/models/object_manager/attribute.rb | 2 +- app/models/online_notification.rb | 6 +- app/models/organization/search.rb | 4 +- app/models/recent_view.rb | 6 +- app/models/sla.rb | 2 +- app/models/stats_store.rb | 4 +- app/models/store.rb | 4 +- app/models/tag.rb | 4 +- app/models/ticket.rb | 22 ++-- app/models/ticket/article.rb | 12 +-- app/models/ticket/overviews.rb | 4 +- app/models/ticket/search.rb | 4 +- app/models/ticket/state.rb | 4 +- app/models/ticket/time_accounting.rb | 4 +- app/models/token.rb | 2 +- app/models/user.rb | 4 +- app/models/user/search.rb | 10 +- bin/rails | 2 +- bin/setup | 31 +++--- bin/update | 31 ++++++ bin/yarn | 9 ++ config/application.rb | 5 +- config/boot.rb | 1 + config/environment.rb | 2 +- config/environments/development.rb | 64 +++++++---- config/environments/production.rb | 53 ++++++---- config/environments/test.rb | 12 ++- .../application_controller_renderer.rb | 8 ++ config/initializers/assets.rb | 8 +- .../initializers/content_security_policy.rb | 25 +++++ config/initializers/cookies_serializer.rb | 2 + config/initializers/db_preferences.rb | 18 ++-- config/initializers/db_preferences_mysql.rb | 78 -------------- .../initializers/db_preferences_postgresql.rb | 29 ----- config/initializers/db_preflight_check.rb | 4 + config/initializers/logo.rb | 11 +- config/initializers/models_searchable.rb | 16 ++- config/initializers/wrap_parameters.rb | 2 +- config/locales/en.yml | 32 +++++- config/spring.rb | 7 ++ .../initializer/db_preflight_check.rb | 19 ++++ .../initializer/db_preflight_check/base.rb | 43 ++++++++ .../initializer/db_preflight_check/mysql2.rb | 88 +++++++++++++++ .../db_preflight_check/postgresql.rb | 73 +++++++++++++ spec/lib/core_ext/string_spec.rb | 2 +- spec/models/cti/caller_id_spec.rb | 9 +- spec/models/knowledge_base/category_spec.rb | 2 +- test/unit/chat_test.rb | 2 + 70 files changed, 648 insertions(+), 316 deletions(-) create mode 100755 bin/update create mode 100755 bin/yarn create mode 100644 config/initializers/application_controller_renderer.rb create mode 100644 config/initializers/content_security_policy.rb delete mode 100644 config/initializers/db_preferences_mysql.rb delete mode 100644 config/initializers/db_preferences_postgresql.rb create mode 100644 config/initializers/db_preflight_check.rb create mode 100644 lib/zammad/application/initializer/db_preflight_check.rb create mode 100644 lib/zammad/application/initializer/db_preflight_check/base.rb create mode 100644 lib/zammad/application/initializer/db_preflight_check/mysql2.rb create mode 100644 lib/zammad/application/initializer/db_preflight_check/postgresql.rb diff --git a/.rubocop.yml b/.rubocop.yml index deb186e28..b5ae44841 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -268,3 +268,11 @@ Layout/MultilineMethodCallIndentation: Style/HashSyntax: Exclude: - "**/*.rake" + +Rails/Exit: + Exclude: + - "config/initializers/*.rb" + +Rails/Output: + Exclude: + - "config/initializers/*.rb" diff --git a/Gemfile b/Gemfile index 3a9c3313d..48132f759 100644 --- a/Gemfile +++ b/Gemfile @@ -2,11 +2,12 @@ source 'https://rubygems.org' # core - base ruby '2.5.5' -gem 'rails', '5.1.7' +gem 'rails', '5.2.3' # core - rails additions gem 'activerecord-import' gem 'activerecord-session_store' +gem 'bootsnap', require: false gem 'composite_primary_keys' gem 'json' gem 'rails-observers' diff --git a/Gemfile.lock b/Gemfile.lock index 54980f5bc..eb7dc751f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -49,39 +49,39 @@ GEM specs: aasm (5.0.0) concurrent-ruby (~> 1.0) - actioncable (5.1.7) - actionpack (= 5.1.7) + actioncable (5.2.3) + actionpack (= 5.2.3) nio4r (~> 2.0) - websocket-driver (~> 0.6.1) - actionmailer (5.1.7) - actionpack (= 5.1.7) - actionview (= 5.1.7) - activejob (= 5.1.7) + websocket-driver (>= 0.6.1) + actionmailer (5.2.3) + actionpack (= 5.2.3) + actionview (= 5.2.3) + activejob (= 5.2.3) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.1.7) - actionview (= 5.1.7) - activesupport (= 5.1.7) + actionpack (5.2.3) + actionview (= 5.2.3) + activesupport (= 5.2.3) rack (~> 2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.1.7) - activesupport (= 5.1.7) + actionview (5.2.3) + activesupport (= 5.2.3) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.1.7) - activesupport (= 5.1.7) + activejob (5.2.3) + activesupport (= 5.2.3) globalid (>= 0.3.6) - activemodel (5.1.7) - activesupport (= 5.1.7) - activerecord (5.1.7) - activemodel (= 5.1.7) - activesupport (= 5.1.7) - arel (~> 8.0) - activerecord-import (1.0.1) + activemodel (5.2.3) + activesupport (= 5.2.3) + activerecord (5.2.3) + activemodel (= 5.2.3) + activesupport (= 5.2.3) + arel (>= 9.0) + activerecord-import (1.0.2) activerecord (>= 3.2) activerecord-nulldb-adapter (0.3.9) activerecord (>= 2.0.0) @@ -91,16 +91,20 @@ GEM multi_json (~> 1.11, >= 1.11.2) rack (>= 1.5.2, < 3) railties (>= 4.0) - activesupport (5.1.7) + activestorage (5.2.3) + actionpack (= 5.2.3) + activerecord (= 5.2.3) + marcel (~> 0.3.1) + activesupport (5.2.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - acts_as_list (0.9.16) + acts_as_list (0.9.19) activerecord (>= 3.0) addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) - arel (8.0.0) + arel (9.0.0) argon2 (2.0.2) ffi (~> 1.9) ffi-compiler (>= 0.1) @@ -112,6 +116,8 @@ GEM biz (1.8.2) clavius (~> 1.0) tzinfo + bootsnap (1.3.2) + msgpack (~> 1.0) browser (2.5.3) buftok (0.2.0) builder (3.2.3) @@ -141,8 +147,8 @@ GEM coffee-script execjs json - composite_primary_keys (10.0.5) - activerecord (~> 5.1.0, >= 5.1.6) + composite_primary_keys (11.2.0) + activerecord (~> 5.2.1) concurrent-ruby (1.1.5) coveralls (0.8.23) json (>= 1.8, < 3) @@ -156,7 +162,7 @@ GEM daemons (1.3.1) dalli (2.7.10) debug_inspector (0.0.3) - delayed_job (4.1.5) + delayed_job (4.1.7) activesupport (>= 3.0, < 5.3) delayed_job_active_record (4.1.3) activerecord (>= 3.0, < 5.3) @@ -268,17 +274,21 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.5.9) lumberjack (1.0.13) + marcel (0.3.3) + mimemagic (~> 0.3.2) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) method_source (0.9.2) mime-types (3.2.2) mime-types-data (~> 3.2015) mime-types-data (3.2019.0331) + mimemagic (0.3.3) mini_mime (1.0.1) mini_portile2 (2.4.0) mini_racer (0.2.4) libv8 (>= 6.3) minitest (5.11.3) + msgpack (1.2.4) multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.1.1) @@ -366,17 +376,18 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (5.1.7) - actioncable (= 5.1.7) - actionmailer (= 5.1.7) - actionpack (= 5.1.7) - actionview (= 5.1.7) - activejob (= 5.1.7) - activemodel (= 5.1.7) - activerecord (= 5.1.7) - activesupport (= 5.1.7) + rails (5.2.3) + actioncable (= 5.2.3) + actionmailer (= 5.2.3) + actionpack (= 5.2.3) + actionview (= 5.2.3) + activejob (= 5.2.3) + activemodel (= 5.2.3) + activerecord (= 5.2.3) + activestorage (= 5.2.3) + activesupport (= 5.2.3) bundler (>= 1.3.0) - railties (= 5.1.7) + railties (= 5.2.3) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.4) actionpack (>= 5.0.1.x) @@ -389,12 +400,12 @@ GEM loofah (~> 2.2, >= 2.2.2) rails-observers (0.1.5) activemodel (>= 4.0) - railties (5.1.7) - actionpack (= 5.1.7) - activesupport (= 5.1.7) + railties (5.2.3) + actionpack (= 5.2.3) + activesupport (= 5.2.3) method_source rake (>= 0.8.7) - thor (>= 0.18.1, < 2.0) + thor (>= 0.19.0, < 2.0) rainbow (3.0.0) raindrops (0.19.0) rake (12.3.2) @@ -522,9 +533,9 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff - websocket-driver (0.6.5) + websocket-driver (0.7.1) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.3) + websocket-extensions (0.1.4) writeexcel (1.0.5) xpath (3.2.0) nokogiri (~> 1.8) @@ -548,6 +559,7 @@ DEPENDENCIES autodiscover! autoprefixer-rails biz + bootsnap browser byebug capybara @@ -602,7 +614,7 @@ DEPENDENCIES pry-stack_explorer puma rack-livereload - rails (= 5.1.7) + rails (= 5.2.3) rails-controller-testing rails-observers rb-fsevent diff --git a/app/controllers/application_controller/prevents_csrf.rb b/app/controllers/application_controller/prevents_csrf.rb index 2182ac07a..dbe3d123e 100644 --- a/app/controllers/application_controller/prevents_csrf.rb +++ b/app/controllers/application_controller/prevents_csrf.rb @@ -2,6 +2,9 @@ module ApplicationController::PreventsCsrf extend ActiveSupport::Concern included do + # disable Rails default (>= 5.2) CSRF checks + skip_before_action :verify_authenticity_token, raise: false + before_action :verify_csrf_token after_action :set_csrf_token_headers end diff --git a/app/controllers/monitoring_controller.rb b/app/controllers/monitoring_controller.rb index b05b6cadb..d7aa3df34 100644 --- a/app/controllers/monitoring_controller.rb +++ b/app/controllers/monitoring_controller.rb @@ -102,7 +102,7 @@ curl http://localhost/api/v1/monitoring/health_check?token=XXX handler_attempts_map = {} failed_jobs.order(:created_at).limit(10).each do |job| - job_name = if job.name == 'ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper'.freeze + job_name = if job.class.name == 'Delayed::Backend::ActiveRecord::Job'.freeze && job.payload_object.respond_to?(:job_data) job.payload_object.job_data['job_class'] else job.name diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index cab2f8af4..6c74b8203 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -9,12 +9,9 @@ class ApplicationJob < ActiveJob::Base # e.g. in the MonitoringController. # This is a workaround to sync ActiveJob#executions to Delayed::Job#attempts # until we resolve this dependency. - around_enqueue do |job, block| - block.call.tap do |delayed_job| - # skip test adapter - break if delayed_job.is_a?(Array) - - delayed_job.update!(attempts: job.executions) - end + after_enqueue do |job| + # update the column right away without loading Delayed::Job record + # see: https://stackoverflow.com/a/34264580 + Delayed::Job.where(id: job.provider_job_id).update_all(attempts: job.executions) # rubocop:disable Rails/SkipsModelValidations end end diff --git a/app/models/activity_stream.rb b/app/models/activity_stream.rb index 76763a115..a4bf6c7e7 100644 --- a/app/models/activity_stream.rb +++ b/app/models/activity_stream.rb @@ -5,8 +5,8 @@ class ActivityStream < ApplicationModel self.table_name = 'activity_streams' # rubocop:disable Rails/InverseOf - belongs_to :object, class_name: 'ObjectLookup', foreign_key: 'activity_stream_object_id' - belongs_to :type, class_name: 'TypeLookup', foreign_key: 'activity_stream_type_id' + belongs_to :object, class_name: 'ObjectLookup', foreign_key: 'activity_stream_object_id', optional: true + belongs_to :type, class_name: 'TypeLookup', foreign_key: 'activity_stream_type_id', optional: true # rubocop:enable Rails/InverseOf # the noop is needed since Layout/EmptyLines detects diff --git a/app/models/application_model/can_associations.rb b/app/models/application_model/can_associations.rb index 368f69789..91b261552 100644 --- a/app/models/application_model/can_associations.rb +++ b/app/models/application_model/can_associations.rb @@ -134,7 +134,7 @@ returns next if association_attributes_ignored.include?(assoc_name) eager_load.push(assoc_name) - pluck.push("#{ActiveRecord::Base.connection.quote_table_name(assoc.table_name)}.id AS #{ActiveRecord::Base.connection.quote_table_name(assoc_name)}") + pluck.push(Arel.sql("#{ActiveRecord::Base.connection.quote_table_name(assoc.table_name)}.id AS #{ActiveRecord::Base.connection.quote_table_name(assoc_name)}")) keys.push("#{assoc_name.to_s.singularize}_ids") end diff --git a/app/models/authorization.rb b/app/models/authorization.rb index c84deb4a9..9fdc3f339 100644 --- a/app/models/authorization.rb +++ b/app/models/authorization.rb @@ -1,7 +1,7 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Authorization < ApplicationModel - belongs_to :user + belongs_to :user, optional: true after_create :delete_user_cache after_update :delete_user_cache after_destroy :delete_user_cache diff --git a/app/models/avatar.rb b/app/models/avatar.rb index 0965d2ec7..8cea318cd 100644 --- a/app/models/avatar.rb +++ b/app/models/avatar.rb @@ -1,7 +1,7 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Avatar < ApplicationModel - belongs_to :object_lookup + belongs_to :object_lookup, optional: true =begin diff --git a/app/models/channel.rb b/app/models/channel.rb index b5ef8f7b5..e184405b4 100644 --- a/app/models/channel.rb +++ b/app/models/channel.rb @@ -3,7 +3,7 @@ class Channel < ApplicationModel include Channel::Assets - belongs_to :group + belongs_to :group, optional: true store :options store :preferences diff --git a/app/models/concerns/can_be_published.rb b/app/models/concerns/can_be_published.rb index a2ff5b6d7..18a11cc12 100644 --- a/app/models/concerns/can_be_published.rb +++ b/app/models/concerns/can_be_published.rb @@ -31,7 +31,7 @@ module CanBePublished local = "#{scope_name}_by".to_sym remote = inverse_relation_name(scope_name).to_sym - belongs_to local, class_name: 'User', inverse_of: remote + belongs_to local, class_name: 'User', inverse_of: remote, optional: true User.has_many remote, class_name: model_name, inverse_of: local, foreign_key: "#{local}_id" User.association_attributes_ignored remote diff --git a/app/models/concerns/has_group_relation_definition.rb b/app/models/concerns/has_group_relation_definition.rb index b8282993a..78a51d29f 100644 --- a/app/models/concerns/has_group_relation_definition.rb +++ b/app/models/concerns/has_group_relation_definition.rb @@ -7,8 +7,8 @@ module HasGroupRelationDefinition self.table_name = "groups_#{group_relation_model_identifier}s" self.primary_keys = ref_key, :group_id, :access - belongs_to group_relation_model_identifier - belongs_to :group + belongs_to group_relation_model_identifier, optional: true + belongs_to :group, optional: true validates :access, presence: true validate :validate_access diff --git a/app/models/concerns/has_groups.rb b/app/models/concerns/has_groups.rb index 7ffe118f0..4c78ff07a 100644 --- a/app/models/concerns/has_groups.rb +++ b/app/models/concerns/has_groups.rb @@ -72,7 +72,7 @@ module HasGroups access = self.class.ensure_group_access_list_parameter(access) # check direct access - return true if group_through.klass.includes(:group).exists?( + return true if group_through.klass.eager_load(:group).exists?( group_through.foreign_key => id, group_id: group_id, access: access, @@ -108,13 +108,13 @@ module HasGroups klass = group_through.klass # check direct access - ids = klass.includes(:group).where(foreign_key => id, access: access, groups: { active: true }).pluck(:group_id) + ids = klass.eager_load(:group).where(foreign_key => id, access: access, groups: { active: true }).pluck(:group_id) ids ||= [] # check indirect access through roles if possible return ids if !respond_to?(:role_ids) - role_group_ids = RoleGroup.includes(:group).where(role_id: role_ids, access: access, groups: { active: true }).pluck(:group_id) + role_group_ids = RoleGroup.eager_load(:group).where(role_id: role_ids, access: access, groups: { active: true }).pluck(:group_id) # combines and removes duplicates # and returns them in one statement diff --git a/app/models/concerns/has_roles.rb b/app/models/concerns/has_roles.rb index e8c114dba..16952d88a 100644 --- a/app/models/concerns/has_roles.rb +++ b/app/models/concerns/has_roles.rb @@ -23,7 +23,7 @@ module HasRoles group_id = self.class.ensure_group_id_parameter(group_id) access = self.class.ensure_group_access_list_parameter(access) - RoleGroup.includes(:group, :role).exists?( + RoleGroup.eager_load(:group, :role).exists?( role_id: roles.pluck(:id), group_id: group_id, access: access, @@ -58,7 +58,7 @@ module HasRoles group_id = ensure_group_id_parameter(group_id) access = ensure_group_access_list_parameter(access) - role_ids = RoleGroup.includes(:role).where(group_id: group_id, access: access, roles: { active: true }).pluck(:role_id) + role_ids = RoleGroup.eager_load(:role).where(group_id: group_id, access: access, roles: { active: true }).pluck(:role_id) join_table = reflect_on_association(:roles).join_table joins(:roles).where(active: true, join_table => { role_id: role_ids }).distinct.select(&:groups_access_permission?) end diff --git a/app/models/cti/caller_id.rb b/app/models/cti/caller_id.rb index e6be826d3..ffe7a66cb 100644 --- a/app/models/cti/caller_id.rb +++ b/app/models/cti/caller_id.rb @@ -53,7 +53,7 @@ returns Cti::CallerId.select('MAX(id) as caller_id') .where({ caller_id: caller_id, level: level }.compact) .group(:user_id) - .order('caller_id DESC') # not used as `caller_id: :desc` because is needed for `as caller_id` + .order(Arel.sql('caller_id DESC')) # not used as `caller_id: :desc` because is needed for `as caller_id` .limit(20) .map(&:caller_id) end.find(&:present?) diff --git a/app/models/email_address.rb b/app/models/email_address.rb index 5dcd2c9ed..12bdc7129 100644 --- a/app/models/email_address.rb +++ b/app/models/email_address.rb @@ -4,7 +4,7 @@ class EmailAddress < ApplicationModel include ChecksLatestChangeObserved has_many :groups, after_add: :cache_update, after_remove: :cache_update - belongs_to :channel + belongs_to :channel, optional: true validates :realname, presence: true validates :email, presence: true diff --git a/app/models/group.rb b/app/models/group.rb index e0c4adfc3..880a41151 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -8,8 +8,8 @@ class Group < ApplicationModel include HasHistory include HasObjectManagerAttributesValidation - belongs_to :email_address - belongs_to :signature + belongs_to :email_address, optional: true + belongs_to :signature, optional: true validates :name, presence: true diff --git a/app/models/history.rb b/app/models/history.rb index 2575ec568..8ee791922 100644 --- a/app/models/history.rb +++ b/app/models/history.rb @@ -6,9 +6,9 @@ class History < ApplicationModel self.table_name = 'histories' - belongs_to :history_type, class_name: 'History::Type' - belongs_to :history_object, class_name: 'History::Object' - belongs_to :history_attribute, class_name: 'History::Attribute' + belongs_to :history_type, class_name: 'History::Type', optional: true + belongs_to :history_object, class_name: 'History::Object', optional: true + belongs_to :history_attribute, class_name: 'History::Attribute', optional: true # the noop is needed since Layout/EmptyLines detects # the block commend below wrongly as the measurement of diff --git a/app/models/karma/activity_log.rb b/app/models/karma/activity_log.rb index b49478427..38263899b 100644 --- a/app/models/karma/activity_log.rb +++ b/app/models/karma/activity_log.rb @@ -1,7 +1,7 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Karma::ActivityLog < ApplicationModel - belongs_to :object_lookup + belongs_to :object_lookup, optional: true self.table_name = 'karma_activity_logs' diff --git a/app/models/knowledge_base/category.rb b/app/models/knowledge_base/category.rb index 80e0bf460..e8c290768 100644 --- a/app/models/knowledge_base/category.rb +++ b/app/models/knowledge_base/category.rb @@ -22,7 +22,8 @@ class KnowledgeBase::Category < ApplicationModel belongs_to :parent, class_name: 'KnowledgeBase::Category', foreign_key: :parent_id, inverse_of: :children, - touch: true + touch: true, + optional: true validates :category_icon, presence: true diff --git a/app/models/link.rb b/app/models/link.rb index b747c53b5..e7ddf3350 100644 --- a/app/models/link.rb +++ b/app/models/link.rb @@ -2,8 +2,8 @@ class Link < ApplicationModel - belongs_to :link_type, class_name: 'Link::Type' - belongs_to :link_object, class_name: 'Link::Object' + belongs_to :link_type, class_name: 'Link::Type', optional: true + belongs_to :link_object, class_name: 'Link::Object', optional: true after_destroy :touch_link_references diff --git a/app/models/object_manager/attribute.rb b/app/models/object_manager/attribute.rb index 5f645ce6f..1fdbb3572 100644 --- a/app/models/object_manager/attribute.rb +++ b/app/models/object_manager/attribute.rb @@ -22,7 +22,7 @@ class ObjectManager::Attribute < ApplicationModel self.table_name = 'object_manager_attributes' - belongs_to :object_lookup + belongs_to :object_lookup, optional: true validates :name, presence: true validates :data_type, inclusion: { in: DATA_TYPES, msg: '%{value} is not a valid data type' } diff --git a/app/models/online_notification.rb b/app/models/online_notification.rb index 9e66afdf6..bc3f79ff5 100644 --- a/app/models/online_notification.rb +++ b/app/models/online_notification.rb @@ -3,10 +3,10 @@ class OnlineNotification < ApplicationModel include OnlineNotification::Assets - belongs_to :user + belongs_to :user, optional: true # rubocop:disable Rails/InverseOf - belongs_to :object, class_name: 'ObjectLookup', foreign_key: 'object_lookup_id' - belongs_to :type, class_name: 'TypeLookup', foreign_key: 'type_lookup_id' + belongs_to :object, class_name: 'ObjectLookup', foreign_key: 'object_lookup_id', optional: true + belongs_to :type, class_name: 'TypeLookup', foreign_key: 'type_lookup_id', optional: true # rubocop:enable Rails/InverseOf after_create :notify_clients_after_change diff --git a/app/models/organization/search.rb b/app/models/organization/search.rb index 9156a29d2..ef952889d 100644 --- a/app/models/organization/search.rb +++ b/app/models/organization/search.rb @@ -104,7 +104,7 @@ returns # - stip out * we already search for *query* - query.delete! '*' organizations = Organization.where_or_cis(%i[name note], "%#{query}%") - .order(order_sql) + .order(Arel.sql(order_sql)) .offset(offset) .limit(limit) .to_a @@ -118,7 +118,7 @@ returns organizations_by_user = Organization.select("DISTINCT(organizations.id), #{order_select_sql}") .joins('LEFT OUTER JOIN users ON users.organization_id = organizations.id') .where(User.or_cis(%i[firstname lastname email], "%#{query}%")) - .order(order_sql) + .order(Arel.sql(order_sql)) .limit(limit) organizations_by_user.each do |organization_by_user| diff --git a/app/models/recent_view.rb b/app/models/recent_view.rb index 611cac70f..fbc18082d 100644 --- a/app/models/recent_view.rb +++ b/app/models/recent_view.rb @@ -4,8 +4,8 @@ class RecentView < ApplicationModel include RecentView::Assets # rubocop:disable Rails/InverseOf - belongs_to :ticket, foreign_key: 'o_id' - belongs_to :object, class_name: 'ObjectLookup', foreign_key: 'recent_view_object_id' + belongs_to :ticket, foreign_key: 'o_id', optional: true + belongs_to :object, class_name: 'ObjectLookup', foreign_key: 'recent_view_object_id', optional: true # rubocop:enable Rails/InverseOf after_create :notify_clients @@ -42,7 +42,7 @@ class RecentView < ApplicationModel 'MAX(id) as id') .group(:o_id, :recent_view_object_id, :created_by_id) .where(created_by_id: user.id) - .order('MAX(created_at) DESC, MAX(id) DESC') + .order(Arel.sql('MAX(created_at) DESC, MAX(id) DESC')) .limit(limit) if object_name.present? diff --git a/app/models/sla.rb b/app/models/sla.rb index 81a0ba6c0..2f9584ba0 100644 --- a/app/models/sla.rb +++ b/app/models/sla.rb @@ -9,5 +9,5 @@ class Sla < ApplicationModel store :condition store :data validates :name, presence: true - belongs_to :calendar + belongs_to :calendar, optional: true end diff --git a/app/models/stats_store.rb b/app/models/stats_store.rb index 657546d39..4c51f2de7 100644 --- a/app/models/stats_store.rb +++ b/app/models/stats_store.rb @@ -5,8 +5,8 @@ class StatsStore < ApplicationModel include StatsStore::SearchIndex # rubocop:disable Rails/InverseOf - belongs_to :stats_store_object, class_name: 'ObjectLookup', foreign_key: 'stats_store_object_id' - belongs_to :related_stats_store_object, class_name: 'ObjectLookup', foreign_key: 'related_stats_store_object_id' + belongs_to :stats_store_object, class_name: 'ObjectLookup', foreign_key: 'stats_store_object_id', optional: true + belongs_to :related_stats_store_object, class_name: 'ObjectLookup', foreign_key: 'related_stats_store_object_id', optional: true # rubocop:enable Rails/InverseOf store :data diff --git a/app/models/store.rb b/app/models/store.rb index 7931fcbc1..9be84a3e2 100644 --- a/app/models/store.rb +++ b/app/models/store.rb @@ -5,8 +5,8 @@ require_dependency 'store/file' class Store < ApplicationModel - belongs_to :store_object, class_name: 'Store::Object' - belongs_to :store_file, class_name: 'Store::File' + belongs_to :store_object, class_name: 'Store::Object', optional: true + belongs_to :store_file, class_name: 'Store::File', optional: true validates :filename, presence: true diff --git a/app/models/tag.rb b/app/models/tag.rb index 768efde0d..bf4af42f1 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -2,8 +2,8 @@ class Tag < ApplicationModel - belongs_to :tag_object, class_name: 'Tag::Object' - belongs_to :tag_item, class_name: 'Tag::Item' + belongs_to :tag_object, class_name: 'Tag::Object', optional: true + belongs_to :tag_item, class_name: 'Tag::Item', optional: true # the noop is needed since Layout/EmptyLines detects # the block commend below wrongly as the measurement of diff --git a/app/models/ticket.rb b/app/models/ticket.rb index 3019e9f0c..3c4ba0f82 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -60,18 +60,18 @@ class Ticket < ApplicationModel sanitized_html :note - belongs_to :group - belongs_to :organization + belongs_to :group, optional: true + belongs_to :organization, optional: true has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket - belongs_to :state, class_name: 'Ticket::State' - belongs_to :priority, class_name: 'Ticket::Priority' - belongs_to :owner, class_name: 'User' - belongs_to :customer, class_name: 'User' - belongs_to :created_by, class_name: 'User' - belongs_to :updated_by, class_name: 'User' - belongs_to :create_article_type, class_name: 'Ticket::Article::Type' - belongs_to :create_article_sender, class_name: 'Ticket::Article::Sender' + belongs_to :state, class_name: 'Ticket::State', optional: true + belongs_to :priority, class_name: 'Ticket::Priority', optional: true + belongs_to :owner, class_name: 'User', optional: true + belongs_to :customer, class_name: 'User', optional: true + belongs_to :created_by, class_name: 'User', optional: true + belongs_to :updated_by, class_name: 'User', optional: true + belongs_to :create_article_type, class_name: 'Ticket::Article::Type', optional: true + belongs_to :create_article_sender, class_name: 'Ticket::Article::Sender', optional: true self.inheritance_column = nil @@ -1001,7 +1001,7 @@ perform active triggers on ticket end triggers = if Rails.configuration.db_case_sensitive - ::Trigger.where(active: true).order('LOWER(name)') + ::Trigger.where(active: true).order(Arel.sql('LOWER(name)')) else ::Trigger.where(active: true).order(:name) end diff --git a/app/models/ticket/article.rb b/app/models/ticket/article.rb index 240f89696..035940573 100644 --- a/app/models/ticket/article.rb +++ b/app/models/ticket/article.rb @@ -11,13 +11,13 @@ class Ticket::Article < ApplicationModel include Ticket::Article::ChecksAccess include Ticket::Article::Assets - belongs_to :ticket + belongs_to :ticket, optional: true has_one :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', foreign_key: :ticket_article_id, dependent: :destroy, inverse_of: :ticket_article - belongs_to :type, class_name: 'Ticket::Article::Type' - belongs_to :sender, class_name: 'Ticket::Article::Sender' - belongs_to :created_by, class_name: 'User' - belongs_to :updated_by, class_name: 'User' - belongs_to :origin_by, class_name: 'User' + belongs_to :type, class_name: 'Ticket::Article::Type', optional: true + belongs_to :sender, class_name: 'Ticket::Article::Sender', optional: true + belongs_to :created_by, class_name: 'User', optional: true + belongs_to :updated_by, class_name: 'User', optional: true + belongs_to :origin_by, class_name: 'User', optional: true before_create :check_subject, :check_body, :check_message_id_md5 before_update :check_subject, :check_body, :check_message_id_md5 diff --git a/app/models/ticket/overviews.rb b/app/models/ticket/overviews.rb index 680e5ad6f..29a30b237 100644 --- a/app/models/ticket/overviews.rb +++ b/app/models/ticket/overviews.rb @@ -121,9 +121,9 @@ returns .where(access_condition) .where(query_condition, *bind_condition) .joins(tables) - .order("#{order_by} #{direction}") + .order(Arel.sql("#{order_by} #{direction}")) .limit(2000) - .pluck(:id, :updated_at, order_by) + .pluck(:id, :updated_at, Arel.sql(order_by)) tickets = ticket_result.map do |ticket| { diff --git a/app/models/ticket/search.rb b/app/models/ticket/search.rb index b6de60896..e472f7734 100644 --- a/app/models/ticket/search.rb +++ b/app/models/ticket/search.rb @@ -190,7 +190,7 @@ returns .where(access_condition) .where('(tickets.title LIKE ? OR tickets.number LIKE ? OR ticket_articles.body LIKE ? OR ticket_articles.from LIKE ? OR ticket_articles.to LIKE ? OR ticket_articles.subject LIKE ?)', "%#{query}%", "%#{query}%", "%#{query}%", "%#{query}%", "%#{query}%", "%#{query}%" ) .joins(:articles) - .order(order_sql) + .order(Arel.sql(order_sql)) .offset(offset) .limit(limit) else @@ -199,7 +199,7 @@ returns .joins(tables) .where(access_condition) .where(query_condition, *bind_condition) - .order(order_sql) + .order(Arel.sql(order_sql)) .offset(offset) .limit(limit) end diff --git a/app/models/ticket/state.rb b/app/models/ticket/state.rb index e136edef8..630c5b04e 100644 --- a/app/models/ticket/state.rb +++ b/app/models/ticket/state.rb @@ -3,8 +3,8 @@ class Ticket::State < ApplicationModel include CanBeImported include ChecksLatestChangeObserved - belongs_to :state_type, class_name: 'Ticket::StateType', inverse_of: :states - belongs_to :next_state, class_name: 'Ticket::State' + belongs_to :state_type, class_name: 'Ticket::StateType', inverse_of: :states, optional: true + belongs_to :next_state, class_name: 'Ticket::State', optional: true after_create :ensure_defaults after_update :ensure_defaults diff --git a/app/models/ticket/time_accounting.rb b/app/models/ticket/time_accounting.rb index 0db8bc6cf..0cbc30cb2 100644 --- a/app/models/ticket/time_accounting.rb +++ b/app/models/ticket/time_accounting.rb @@ -1,8 +1,8 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Ticket::TimeAccounting < ApplicationModel - belongs_to :ticket - belongs_to :ticket_article, class_name: 'Ticket::Article', inverse_of: :ticket_time_accounting + belongs_to :ticket, optional: true + belongs_to :ticket_article, class_name: 'Ticket::Article', inverse_of: :ticket_time_accounting, optional: true after_create :ticket_time_unit_update after_update :ticket_time_unit_update diff --git a/app/models/token.rb b/app/models/token.rb index 1ec3187f0..e4586c545 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -2,7 +2,7 @@ class Token < ActiveRecord::Base before_create :generate_token - belongs_to :user + belongs_to :user, optional: true store :preferences =begin diff --git a/app/models/user.rb b/app/models/user.rb index 408b479a8..fa3b8ef33 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,4 +1,6 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +require_dependency 'karma/user' + class User < ApplicationModel include CanBeImported include HasActivityStreamLog @@ -20,7 +22,7 @@ class User < ApplicationModel has_and_belongs_to_many :organizations, after_add: :cache_update, after_remove: :cache_update, class_name: 'Organization' has_many :tokens, after_add: :cache_update, after_remove: :cache_update has_many :authorizations, after_add: :cache_update, after_remove: :cache_update - belongs_to :organization, inverse_of: :members + belongs_to :organization, inverse_of: :members, optional: true before_validation :check_name, :check_email, :check_login, :ensure_uniq_email, :ensure_password, :ensure_roles, :ensure_identifier before_validation :check_mail_delivery_failed, on: :update diff --git a/app/models/user/search.rb b/app/models/user/search.rb index f6d21208f..216e2784e 100644 --- a/app/models/user/search.rb +++ b/app/models/user/search.rb @@ -137,11 +137,17 @@ returns users = if params[:role_ids] User.joins(:roles).where('roles.id' => params[:role_ids]).where( '(users.firstname LIKE ? OR users.lastname LIKE ? OR users.email LIKE ? OR users.login LIKE ?) AND users.id != 1', "%#{query}%", "%#{query}%", "%#{query}%", "%#{query}%" - ).order(order_sql).offset(offset).limit(limit) + ) + .order(Arel.sql(order_sql)) + .offset(offset) + .limit(limit) else User.where( '(firstname LIKE ? OR lastname LIKE ? OR email LIKE ? OR login LIKE ?) AND id != 1', "%#{query}%", "%#{query}%", "%#{query}%", "%#{query}%" - ).order(order_sql).offset(offset).limit(limit) + ) + .order(Arel.sql(order_sql)) + .offset(offset) + .limit(limit) end users end diff --git a/bin/rails b/bin/rails index 0138d79b7..5badb2fde 100755 --- a/bin/rails +++ b/bin/rails @@ -4,6 +4,6 @@ begin rescue LoadError => e raise unless e.message.include?('spring') end -APP_PATH = File.expand_path('../../config/application', __FILE__) +APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' diff --git a/bin/setup b/bin/setup index 319f93a92..65e08d029 100755 --- a/bin/setup +++ b/bin/setup @@ -1,29 +1,36 @@ #!/usr/bin/env ruby -require 'pathname' +require 'fileutils' +include FileUtils # rubocop:disable Style/MixinUsage # path to your application root. -APP_ROOT = Pathname.new File.expand_path('..', __dir__) +APP_ROOT = File.expand_path('..', __dir__) -Dir.chdir APP_ROOT do +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do # This script is a starting point to setup your application. - # Add necessary setup steps to this file: + # Add necessary setup steps to this file. puts '== Installing dependencies ==' - system 'gem install bundler --conservative' - system 'bundle check || bundle install --jobs 8' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies if using Yarn + # system('bin/yarn') # puts "\n== Copying sample files ==" - # unless File.exist?("config/database.yml") - # system "cp config/database.yml.sample config/database.yml" + # unless File.exist?('config/database.yml') + # cp 'config/database.yml.sample', 'config/database.yml' # end puts "\n== Preparing database ==" - system 'bin/rake db:setup' + system! 'bin/rails db:setup' puts "\n== Removing old logs and tempfiles ==" - system 'rm -f log/*' - system 'rm -rf tmp/cache' + system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" - system 'touch tmp/restart.txt' + system! 'bin/rails restart' end diff --git a/bin/update b/bin/update new file mode 100755 index 000000000..05737c272 --- /dev/null +++ b/bin/update @@ -0,0 +1,31 @@ +#!/usr/bin/env ruby +require 'fileutils' +include FileUtils # rubocop:disable Style/MixinUsage + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies if using Yarn + # system('bin/yarn') + + puts "\n== Updating database ==" + system! 'bin/rails db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/bin/yarn b/bin/yarn new file mode 100755 index 000000000..d3627c34c --- /dev/null +++ b/bin/yarn @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +APP_ROOT = File.expand_path('..', __dir__) +Dir.chdir(APP_ROOT) do + exec 'yarnpkg', *ARGV +rescue Errno::ENOENT + warn 'Yarn executable was not detected in the system.' + warn 'Download Yarn at https://yarnpkg.com/en/docs/install' + exit 1 +end diff --git a/config/application.rb b/config/application.rb index 3629b661a..f6e0f1187 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,4 +1,4 @@ -require File.expand_path('boot', __dir__) +require_relative 'boot' require 'rails/all' @@ -11,6 +11,9 @@ Bundler.require(*Rails.groups) module Zammad class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 5.2 + # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. diff --git a/config/boot.rb b/config/boot.rb index 30f5120df..b9e460cef 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,3 +1,4 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. +require 'bootsnap/setup' # Speed up boot time by caching expensive operations. diff --git a/config/environment.rb b/config/environment.rb index 0b8bdd828..426333bb4 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,5 +1,5 @@ # Load the Rails application. -require File.expand_path('application', __dir__) +require_relative 'application' # Initialize the Rails application. Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb index d31be1d28..0377d60ef 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -9,28 +9,68 @@ Rails.application.configure do # Do not eager load code on boot. config.eager_load = false - # Show full error reports and disable caching. - config.consider_all_requests_local = true - config.action_controller.perform_caching = false + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join('tmp', 'caching-dev.txt').exist? + config.action_controller.perform_caching = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options) + # config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false + config.action_mailer.perform_caching = false + # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load - # Do not compress assets - config.assets.compress = false + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. - #config.assets.debug = true config.assets.debug = false + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + # Asset digests allow you to set far-future HTTP expiration dates on all assets, + # yet still be able to expire them through the digest params. + config.assets.digest = false + + # Adds additional error checking when serving assets at runtime. + # Checks for improperly declared sprockets dependencies. + # Raises helpful error messages. + config.assets.raise_runtime_errors = true + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true + # Automatically inject JavaScript needed for LiveReload if ENV['RAKE_LIVE_RELOAD'].present? require 'rack-livereload' @@ -43,16 +83,4 @@ Rails.application.configure do live_reload_port: 35_738 ) end - - # Asset digests allow you to set far-future HTTP expiration dates on all assets, - # yet still be able to expire them through the digest params. - config.assets.digest = false - - # Adds additional error checking when serving assets at runtime. - # Checks for improperly declared sprockets dependencies. - # Raises helpful error messages. - config.assets.raise_runtime_errors = true - - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true end diff --git a/config/environments/production.rb b/config/environments/production.rb index fd2382dde..2210cfbc8 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -14,11 +14,9 @@ Rails.application.configure do config.consider_all_requests_local = false config.action_controller.perform_caching = true - # Enable Rack::Cache to put a simple HTTP cache in front of your application - # Add `rack-cache` to your Gemfile before enabling this. - # For large-scale production use, consider using a caching reverse proxy like - # NGINX, varnish or squid. - # config.action_dispatch.rack_cache = true + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. @@ -31,16 +29,23 @@ Rails.application.configure do # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # Asset digests allow you to set far-future HTTP expiration dates on all assets, - # yet still be able to expire them through the digest params. - config.assets.digest = true - # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + # Store uploaded files on the local file system (see config/storage.yml for options) + # config.active_storage.service = :local + + # Mount Action Cable outside main process or domain + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true @@ -49,19 +54,16 @@ Rails.application.configure do config.log_level = :info # Prepend all log lines with the following tags. - # config.log_tags = [ :subdomain, :uuid ] + config.log_tags = [ :request_id ] - # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + # Use a different cache store in production. + # config.cache_store = :mem_cache_store - if ENV['RAILS_LOG_TO_STDOUT'].present? - logger = ActiveSupport::Logger.new(STDOUT) - logger.formatter = config.log_formatter - config.logger = ActiveSupport::TaggedLogging.new(logger) - end + # Use a real queuing backend for Active Job (and separate queues per environment) + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "zammad_#{Rails.env}" - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = 'http://assets.example.com' + config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. @@ -77,6 +79,19 @@ Rails.application.configure do # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify + # Use default logging formatter so that PID and timestamp are not suppressed. + # config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV['RAILS_LOG_TO_STDOUT'].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false diff --git a/config/environments/test.rb b/config/environments/test.rb index cbee70943..d5c9258fb 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -12,9 +12,11 @@ Rails.application.configure do # preloads Rails for running tests, you may have to set it to true. config.eager_load = false - # Configure static file server for tests with Cache-Control for performance. + # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true - config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' } + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } # Show full error reports and disable caching. config.consider_all_requests_local = true @@ -26,6 +28,11 @@ Rails.application.configure do # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false + # Store uploaded files on the local file system in a temporary directory + # config.active_storage.service = :test + + config.action_mailer.perform_caching = false + # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. @@ -43,6 +50,7 @@ Rails.application.configure do # Enable autoload config.dependency_loading = true + # Reduce log level to speed up test runs. config.after_initialize do ActiveRecord::Base.logger = Rails.logger.clone ActiveRecord::Base.logger.level = Logger::INFO diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 000000000..89d2efab2 --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 0833b4da1..8e1c0c31b 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -3,11 +3,15 @@ # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' -# Add additional assets to the asset load path +# Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path +# Add Yarn node_modules folder to the asset load path. +# Rails.application.config.assets.paths << Rails.root.join('node_modules') # Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) Rails.application.config.assets.precompile += %w[application-print.css] Rails.application.config.assets.precompile += %w[print.css] Rails.application.config.assets.precompile += %w[knowledge_base.css knowledge_base_public.js knowledge_base_public_polyfills.js] diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 000000000..d3bcaa5ec --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,25 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +# Rails.application.config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end + +# If you are using UJS then enable automatic nonce generation +# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } + +# Report CSP violations to a specified URI +# For further information see the following documentation: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# Rails.application.config.content_security_policy_report_only = true diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb index ac5f8b663..1389e86a3 100644 --- a/config/initializers/cookies_serializer.rb +++ b/config/initializers/cookies_serializer.rb @@ -1,3 +1,5 @@ # Be sure to restart your server when you modify this file. +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :marshal diff --git a/config/initializers/db_preferences.rb b/config/initializers/db_preferences.rb index 66cf8856c..f146146ef 100644 --- a/config/initializers/db_preferences.rb +++ b/config/initializers/db_preferences.rb @@ -1,6 +1,12 @@ -# set database preferences - -# defaults -Rails.application.config.db_case_sensitive = false -Rails.application.config.db_like = 'LIKE' -Rails.application.config.db_4bytes_utf8 = true +case ActiveRecord::Base.connection_config[:adapter] +when 'mysql2' + Rails.application.config.db_4bytes_utf8 = false + Rails.application.config.db_case_sensitive = false + Rails.application.config.db_like = 'LIKE' + Rails.application.config.db_null_byte = true +when 'postgresql' + Rails.application.config.db_4bytes_utf8 = true + Rails.application.config.db_case_sensitive = true + Rails.application.config.db_like = 'ILIKE' + Rails.application.config.db_null_byte = false +end diff --git a/config/initializers/db_preferences_mysql.rb b/config/initializers/db_preferences_mysql.rb deleted file mode 100644 index e4de98227..000000000 --- a/config/initializers/db_preferences_mysql.rb +++ /dev/null @@ -1,78 +0,0 @@ -return if ActiveRecord::Base.connection_config[:adapter] != 'mysql2' - -Rails.application.config.db_4bytes_utf8 = false -Rails.application.config.db_null_byte = true -connection = ActiveRecord::Base.connection - -# rubocop:disable Rails/Output -# rubocop:disable Rails/Exit -# rubocop:disable Layout/IndentHeredoc - -# Version check -------------------------------------------------------------- -# mysql example: "5.7.3" -# mariadb example: "10.1.17-MariaDB" -server_version = connection.execute('SELECT @@version;').first.first -raise 'Unable to retrive database version' if server_version.blank? - -version_number = Gem::Version.new(server_version.split('-').first) -vendor = server_version.split('-').second || 'MySQL' - -case vendor -when 'MySQL' - if version_number < Gem::Version.new('5.6') - printf "\e[31m" # ANSI red - puts <<~MSG - Error: Incompatible database backend version - (MySQL 5.6+ required; #{version_number} found) - MSG - printf "\e[0m" # ANSI normal - exit 1 - end -when 'MariaDB' - if version_number < Gem::Version.new('10.0') - printf "\e[31m" # ANSI red - puts <<~MSG - Error: Incompatible database backend version - (MariaDB 10.0+ required; #{version_number} found) - MSG - printf "\e[0m" # ANSI normal - exit 1 - end -end - -# Configuration check -------------------------------------------------------- -# Before MySQL 8.0, the default value of max_allowed_packet was 1MB - 4MB, -# which is impractically small and can lead to failures processing emails. -# -# See https://github.com/zammad/zammad/issues/1759 -# https://github.com/zammad/zammad/issues/1970 -# https://github.com/zammad/zammad/issues/2034 -max_allowed_packet = connection.execute('SELECT @@max_allowed_packet;').first.first -max_allowed_packet_mb = max_allowed_packet / 1024 / 1024 - -if max_allowed_packet_mb <= 4 - printf "\e[31m" # ANSI red - puts <<~MSG - Error: Database config value 'max_allowed_packet' too small (#{max_allowed_packet_mb}MB) - Please increase this value in your #{vendor} configuration (64MB+ recommended). - MSG - printf "\e[0m" # ANSI normal - exit 1 -end - -if connection.execute("SHOW tables LIKE 'settings';").any? && - Setting.get('postmaster_max_size').present? && - Setting.get('postmaster_max_size').to_i > max_allowed_packet_mb - printf "\e[33m" # ANSI yellow - puts <<~MSG - Warning: Database config value 'max_allowed_packet' less than Zammad setting 'Maximum Email Size' - Zammad will fail to process emails (both incoming and outgoing) - larger than the value of 'max_allowed_packet' (#{max_allowed_packet_mb}MB). - Please increase this value in your #{vendor} configuration accordingly. - MSG - printf "\e[0m" # ANSI normal -end - -# rubocop:enable Rails/Exit -# rubocop:enable Rails/Output -# rubocop:enable Layout/IndentHeredoc diff --git a/config/initializers/db_preferences_postgresql.rb b/config/initializers/db_preferences_postgresql.rb deleted file mode 100644 index 72e6e91e7..000000000 --- a/config/initializers/db_preferences_postgresql.rb +++ /dev/null @@ -1,29 +0,0 @@ -return if ActiveRecord::Base.connection_config[:adapter] != 'postgresql' - -Rails.application.config.db_case_sensitive = true -Rails.application.config.db_like = 'ILIKE' -Rails.application.config.db_null_byte = false - -# rubocop:disable Rails/Output -# rubocop:disable Rails/Exit -# rubocop:disable Layout/IndentHeredoc - -# Version check -------------------------------------------------------------- -# example output: "9.5.0" -# "10.3 (Debian 10.3-2)" -server_version = ActiveRecord::Base.connection.execute('SHOW server_version;').first['server_version'] -version_number = Gem::Version.new(server_version.split.first) - -if version_number < Gem::Version.new('9.1') - printf "\e[31m" # ANSI red - puts <<~MSG - Error: Incompatible database backend version - (PostgreSQL 9.1+ required; #{version_number} found) - MSG - printf "\e[0m" # ANSI normal - exit 1 -end - -# rubocop:enable Rails/Exit -# rubocop:enable Rails/Output -# rubocop:enable Layout/IndentHeredoc diff --git a/config/initializers/db_preflight_check.rb b/config/initializers/db_preflight_check.rb new file mode 100644 index 000000000..8dc5749c6 --- /dev/null +++ b/config/initializers/db_preflight_check.rb @@ -0,0 +1,4 @@ +# Rails' constant auto-loading resolves to 'rails/initializable' instead +require_dependency 'zammad/application/initializer/db_preflight_check' + +Zammad::Application::Initializer::DBPreflightCheck.perform diff --git a/config/initializers/logo.rb b/config/initializers/logo.rb index 4980b69d5..45a8a00f8 100644 --- a/config/initializers/logo.rb +++ b/config/initializers/logo.rb @@ -1,6 +1,7 @@ +return if !ActiveRecord::Base.connected? + # sync logo to fs / only if settings already exists -if ActiveRecord::Base.connection.tables.include?('settings') - if Setting.column_names.include?('state_current') - StaticAssets.sync - end -end +return if ActiveRecord::Base.connection.tables.exclude?('settings') +return if Setting.column_names.exclude?('state_current') + +StaticAssets.sync diff --git a/config/initializers/models_searchable.rb b/config/initializers/models_searchable.rb index f91d6bae8..d17be60e2 100644 --- a/config/initializers/models_searchable.rb +++ b/config/initializers/models_searchable.rb @@ -1,11 +1,9 @@ # update settings for searchable models -if ActiveRecord::Base.connection.tables.include?('settings') - if Setting.columns_hash.key?('state_current') # TODO: remove me later - models_current = Models.searchable.map(&:to_s) - models_config = Setting.get('models_searchable') - setting = Setting.find_by(name: 'models_searchable') - if setting && models_current != models_config - Setting.set('models_searchable', models_current) - end - end + +begin + return if !Setting.exists?(name: 'models_searchable') + + Setting.set('models_searchable', Models.searchable.map(&:to_s)) +rescue ActiveRecord::StatementInvalid + nil end diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index ba7a7376e..c5383235d 100644 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -10,5 +10,5 @@ end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do -# self.include_root_in_json = true +# self.include_root_in_json = true # end diff --git a/config/locales/en.yml b/config/locales/en.yml index 179c14ca5..decc5a857 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,5 +1,33 @@ -# Sample localization file for English. Add more files in this directory for other locales. -# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# +# To learn more, please read the Rails Internationalization guide +# available at http://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" diff --git a/config/spring.rb b/config/spring.rb index d50143742..b9e4fd981 100644 --- a/config/spring.rb +++ b/config/spring.rb @@ -1,3 +1,10 @@ +%w[ + .ruby-version + .rbenv-vars + tmp/restart.txt + tmp/caching-dev.txt +].each { |path| Spring.watch(path) } + module Spring module Commands class SchedulerRb diff --git a/lib/zammad/application/initializer/db_preflight_check.rb b/lib/zammad/application/initializer/db_preflight_check.rb new file mode 100644 index 000000000..4e550623c --- /dev/null +++ b/lib/zammad/application/initializer/db_preflight_check.rb @@ -0,0 +1,19 @@ +require 'zammad/application/initializer/db_preflight_check/base' +require 'zammad/application/initializer/db_preflight_check/mysql2' +require 'zammad/application/initializer/db_preflight_check/postgresql' + +module Zammad + class Application + class Initializer + module DBPreflightCheck + def self.perform + adapter.perform + end + + def self.adapter + @adapter ||= const_get(ActiveRecord::Base.connection_config[:adapter].capitalize) + end + end + end + end +end diff --git a/lib/zammad/application/initializer/db_preflight_check/base.rb b/lib/zammad/application/initializer/db_preflight_check/base.rb new file mode 100644 index 000000000..4e47cf307 --- /dev/null +++ b/lib/zammad/application/initializer/db_preflight_check/base.rb @@ -0,0 +1,43 @@ +# NOTE: Why use Mysql2::Client / PG::Connection over ActiveRecord::Base.connection? +# +# As of Rails 5.2, db:create now runs initializers prior to creating the DB. +# That means if an initializer tries to establish an ActiveRecord::Base.connection, +# it will raise an ActiveRecord::NoDatabaseError +# (see https://github.com/rails/rails/issues/32870 for more details). +# +# The workaround is to use the bare RDBMS library +# and connect without specifying a database (MySQL), +# or connect to a standard system database instead (PostgreSQL). + +module Zammad + class Application + class Initializer + module DBPreflightCheck + module Base + def check_version_compatibility + return if Gem::Version.new(current_version) >= Gem::Version.new(min_version) + + err(<<~MSG) + Incompatible database backend version + (#{vendor} #{min_version}+ required; #{current_version} found) + MSG + end + + def warn(msg) + printf "\e[33m" # ANSI yellow + puts "Warning: #{msg}" # rubocop:disable Rails/Output + printf "\e[0m" # ANSI normal + end + + def err(msg) + printf "\e[31m" # ANSI red + puts "Error: #{msg}" # rubocop:disable Rails/Output + printf "\e[0m" # ANSI normal + + exit 1 # rubocop:disable Rails/Exit + end + end + end + end + end +end diff --git a/lib/zammad/application/initializer/db_preflight_check/mysql2.rb b/lib/zammad/application/initializer/db_preflight_check/mysql2.rb new file mode 100644 index 000000000..d005f5db1 --- /dev/null +++ b/lib/zammad/application/initializer/db_preflight_check/mysql2.rb @@ -0,0 +1,88 @@ +# NOTE: Why use Mysql2::Client over ActiveRecord::Base.connection? +# +# As of Rails 5.2, db:create now runs initializers prior to creating the DB. +# That means if an initializer tries to establish an ActiveRecord::Base.connection, +# it will raise an ActiveRecord::NoDatabaseError +# (see https://github.com/rails/rails/issues/32870 for more details). +# +# The workaround is to use the bare RDBMS library +# and connect without specifying a database. + +module Zammad + class Application + class Initializer + module DBPreflightCheck + module Mysql2 + extend Base + + def self.perform + check_version_compatibility + check_max_allowed_packet + end + + # Configuration check -------------------------------------------------- + # Before MySQL 8.0, the default value of max_allowed_packet was 1MB - 4MB, + # which is impractically small and can lead to email processing failures. + # + # See https://github.com/zammad/zammad/issues/1759 + # https://github.com/zammad/zammad/issues/1970 + # https://github.com/zammad/zammad/issues/2034 + def self.check_max_allowed_packet + if max_allowed_packet_mb <= 4 + err(<<~MSG) + Database config value 'max_allowed_packet' too small (#{max_allowed_packet_mb}MB) + Please increase this value in your #{vendor} configuration (64MB+ recommended). + MSG + elsif max_allowed_packet_mb < Setting.get('postmaster_max_size').to_i + warn(<<~MSG) + Database config value 'max_allowed_packet' less than Zammad setting 'Maximum Email Size' + Zammad will fail to process emails (both incoming and outgoing) + larger than the value of 'max_allowed_packet' (#{max_allowed_packet_mb}MB). + Please increase this value in your #{vendor} configuration accordingly. + MSG + end + # Setting.get will fail if 'settings' table does not exist + rescue ActiveRecord::StatementInvalid # rubocop:disable Lint/HandleExceptions + end + + def self.connection + @connection ||= ::Mysql2::Client.new(db_config) + end + + def self.db_config + @db_config ||= ActiveRecord::Base.connection_config + .symbolize_keys + .except(:database) + end + + # formats: "5.7.3" (MySQL) + # "10.1.17-MariaDB" (MariaDB) + def self.current_version + @current_version ||= mysql_variable('version').split('-').first + end + + def self.min_version + case vendor + when 'MySQL' + '5.6' + when 'MariaDB' + '10.0' + end + end + + def self.vendor + @vendor ||= mysql_variable('version').split('-').second || 'MySQL' + end + + def self.max_allowed_packet_mb + @max_allowed_packet_mb ||= mysql_variable('max_allowed_packet') / 1024 / 1024 + end + + def self.mysql_variable(name) + connection.query("SELECT @@#{name};").first["@@#{name}"] + end + end + end + end + end +end diff --git a/lib/zammad/application/initializer/db_preflight_check/postgresql.rb b/lib/zammad/application/initializer/db_preflight_check/postgresql.rb new file mode 100644 index 000000000..dd8d02d75 --- /dev/null +++ b/lib/zammad/application/initializer/db_preflight_check/postgresql.rb @@ -0,0 +1,73 @@ +# NOTE: Why use PG::Connection over ActiveRecord::Base.connection? +# +# As of Rails 5.2, db:create now runs initializers prior to creating the DB. +# That means if an initializer tries to establish an ActiveRecord::Base.connection, +# it will raise an ActiveRecord::NoDatabaseError +# (see https://github.com/rails/rails/issues/32870 for more details). +# +# The workaround is to use the bare RDBMS library +# and connect to a standard system database instead. + +module Zammad + class Application + class Initializer + module DBPreflightCheck + module Postgresql + extend Base + + def self.perform + check_version_compatibility + ensure + connection.try(:finish) + end + + def self.check_version_compatibility + return if connection.nil? # Edge case: if Postgres can't find a DB to connect to + + super + end + + def self.connection + alternate_dbs = %w[template0 template1 postgres] + + @connection ||= begin + PG.connect(db_config) + rescue PG::ConnectionBad + db_config[:dbname] = alternate_dbs.pop + retry if db_config[:dbname].present? + end + end + + # Adapted from ActiveRecord::ConnectionHandling#postgresql_connection + def self.db_config + @db_config ||= ActiveRecord::Base.connection_config.dup.tap do |config| + config.symbolize_keys! + config[:user] = config.delete(:username) + config[:dbname] = config.delete(:database) + config.slice!(*PG::Connection.conndefaults_hash.keys, :requiressl) + config.compact! + end + end + + # formats: "9.5.0" + # "10.3 (Debian 10.3-2)" + def self.current_version + @current_version ||= pg_variable('server_version').split.first + end + + def self.min_version + @min_version ||= '9.1' + end + + def self.vendor + @vendor ||= 'PostgreSQL' + end + + def self.pg_variable(name) + connection.exec("SHOW #{name};").first[name] + end + end + end + end + end +end diff --git a/spec/lib/core_ext/string_spec.rb b/spec/lib/core_ext/string_spec.rb index 08276ef79..d94fdc4d4 100644 --- a/spec/lib/core_ext/string_spec.rb +++ b/spec/lib/core_ext/string_spec.rb @@ -1886,7 +1886,7 @@ RSpec.describe String do .to raise_error(Encoding::InvalidByteSequenceError) expect { subject.utf8_encode(from: 'gb2312') } - .not_to raise_error(Encoding::InvalidByteSequenceError) + .not_to raise_error end it 'uses the detected input encoding instead' do diff --git a/spec/models/cti/caller_id_spec.rb b/spec/models/cti/caller_id_spec.rb index 85909bf66..64e26b877 100644 --- a/spec/models/cti/caller_id_spec.rb +++ b/spec/models/cti/caller_id_spec.rb @@ -113,8 +113,8 @@ RSpec.describe Cti::CallerId do context 'shared by multiple CallerIds' do context '(for different users)' do subject!(:caller_ids) do - [create(:caller_id, caller_id: number, user: User.last), - create(:caller_id, caller_id: number, user: User.offset(1).last)] + [create(:caller_id, caller_id: number, user: create(:user)), + create(:caller_id, caller_id: number, user: create(:user))] end it 'returns all corresponding CallerId records' do @@ -132,9 +132,8 @@ RSpec.describe Cti::CallerId do context '(some for the same user, some for another)' do subject!(:caller_ids) do - [create(:caller_id, caller_id: number, user: User.last), - create(:caller_id, caller_id: number, user: User.last), - create(:caller_id, caller_id: number, user: User.offset(1).last)] + [*create_list(:caller_id, 2, caller_id: number, user: create(:user)), + create(:caller_id, caller_id: number, user: create(:user))] end it 'returns one CallerId record per unique #user_id, by MAX(id)' do diff --git a/spec/models/knowledge_base/category_spec.rb b/spec/models/knowledge_base/category_spec.rb index 1a22bb98b..92b7895b9 100644 --- a/spec/models/knowledge_base/category_spec.rb +++ b/spec/models/knowledge_base/category_spec.rb @@ -14,7 +14,7 @@ RSpec.describe KnowledgeBase::Category, type: :model do it { is_expected.to have_many(:answers) } it { is_expected.to have_many(:children) } - it { is_expected.to belong_to(:parent) } + it { is_expected.to belong_to(:parent).optional } it { is_expected.to belong_to(:knowledge_base) } context 'in multilevel tree' do diff --git a/test/unit/chat_test.rb b/test/unit/chat_test.rb index 4467d008d..b9cfbfbd8 100644 --- a/test/unit/chat_test.rb +++ b/test/unit/chat_test.rb @@ -49,6 +49,8 @@ class ChatTest < ActiveSupport::TestCase # see: https://github.com/zammad/zammad/issues/2353 test 'chat event db connection test' do + skip "Can't properly disconnect while Spring is in use." if defined?(Spring) + class DummyWs def send(msg) Rails.logger.info "WS send: #{msg}"