From 21226b79613fe602d0d5a8322132725e3f648ec7 Mon Sep 17 00:00:00 2001 From: Anna Topalidi <60363870+antopalidi@users.noreply.github.com> Date: Mon, 21 Aug 2023 12:14:49 +0200 Subject: [PATCH] Triplicate conferences hacking the hashtag (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add custom menu items * controller override, add routes * change add item menu * Apply suggestions from code review Co-authored-by: Ivan Vergés * add options if to add_item * add rspec, refactoring * fix lint * promoted conferences override, add tests * remove global config from secret, refactoring controller's methods * change tests * add controller spec * fix robocop * move initializers and overrides to initializers folder * refactoring model and tests * Update config/locales/en.yml Co-authored-by: Ivan Vergés * fix initializers * fix initialize * gitignore * add actions * add config --------- Co-authored-by: Ivan Vergés --- .github/workflows/test.yml | 64 ++++++++++ .gitignore | 2 + .rubocop_ruby.yml | 5 - Gemfile | 4 +- Gemfile.lock | 1 + .../conferences_controller_override.rb | 60 +++++++++ app/models/concerns/conference_override.rb | 11 ++ bin/spring | 2 +- bin/webpack | 2 +- bin/webpack-dev-server | 2 +- config/application.rb | 53 -------- config/environments/production.rb | 10 +- config/initializers/conferences_hacks.rb | 76 ++++++++++++ config/locales/en.yml | 4 + config/routes.rb | 6 + config/secrets.yml | 9 ++ .../conferences_controller_spec.rb | 88 ++++++++++++++ spec/factories.rb | 4 + spec/lib/overrides_spec.rb | 41 +++++++ spec/models/conference_override_spec.rb | 41 +++++++ spec/rails_helper.rb | 76 ++++++++++++ spec/spec_helper.rb | 108 +++++++++++++++++ spec/system/conferences_spec.rb | 114 ++++++++++++++++++ 23 files changed, 714 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 app/controllers/concerns/conferences_controller_override.rb create mode 100644 app/models/concerns/conference_override.rb create mode 100644 config/initializers/conferences_hacks.rb create mode 100644 spec/controllers/conferences_controller_spec.rb create mode 100644 spec/factories.rb create mode 100644 spec/lib/overrides_spec.rb create mode 100644 spec/models/conference_override_spec.rb create mode 100644 spec/rails_helper.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/system/conferences_spec.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..1c97ccbd --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,64 @@ +name: Test + +on: + push: + branches: + - main + - staging + pull_request: + +env: + RUBY_VERSION: 3.0.6 + NODE_VERSION: 16.9.1 + +jobs: + test: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:11 + ports: + - 5432:5432 + env: + RAILS_ENV: test + DATABASE_USERNAME: postgres + DATABASE_PASSWORD: postgres + POSTGRES_HOST_AUTH_METHOD: trust + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 1 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ env.RUBY_VERSION }} + bundler-cache: true + + - run: bundle exec rubocop -P + name: Lint Ruby files + + - name: Setup & create Database + run: | + bundle exec rails db:create db:schema:load + env: + RAILS_ENV: test + DATABASE_USERNAME: postgres + DATABASE_PASSWORD: postgres + + - name: Precompile assets + run: | + npm ci + bundle exec rake assets:precompile + env: + RAILS_ENV: production + DB_ADAPTER: nulldb + SECRET_KEY_BASE: 1234567890 + + - name: Run RSpec + run: SIMPLECOV=1 CODECOV=1 bundle exec rspec + env: + RAILS_ENV: test + DATABASE_USERNAME: postgres + DATABASE_PASSWORD: postgres diff --git a/.gitignore b/.gitignore index 85f0d514..b2370b6c 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ yarn-debug.log* .rbenv-vars public/decidim-packs public/sw.js* +coverage/* +.rspec-* \ No newline at end of file diff --git a/.rubocop_ruby.yml b/.rubocop_ruby.yml index 59330388..c7186035 100644 --- a/.rubocop_ruby.yml +++ b/.rubocop_ruby.yml @@ -71,11 +71,6 @@ AllCops: # Otherwise we fallback to the oldest officially supported Ruby version (2.0). TargetRubyVersion: 2.7 - RSpec: - Patterns: - - "(?:^|/)spec/" - - "(?:^|/)test/" - # Indent private/protected/public as deep as method definitions Layout/AccessModifierIndentation: EnforcedStyle: indent diff --git a/Gemfile b/Gemfile index 062e7fc0..3e71840f 100644 --- a/Gemfile +++ b/Gemfile @@ -26,8 +26,8 @@ gem "wicked_pdf", "~> 2.1" gem "deface", ">= 1.9" group :development, :test do - gem "faker", "~> 2.14" gem "byebug", "~> 11.0", platform: :mri + gem "faker", "~> 2.14" gem "brakeman" gem "decidim-dev", DECIDIM_VERSION @@ -36,9 +36,9 @@ end group :development do gem "letter_opener_web" gem "listen" + gem "rubocop-faker" gem "spring" gem "spring-watcher-listen" - gem "rubocop-faker" gem "web-console" end diff --git a/Gemfile.lock b/Gemfile.lock index e8403ddb..91dd3fce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -872,6 +872,7 @@ GEM PLATFORMS x86_64-darwin-22 + x86_64-darwin-23 x86_64-linux DEPENDENCIES diff --git a/app/controllers/concerns/conferences_controller_override.rb b/app/controllers/concerns/conferences_controller_override.rb new file mode 100644 index 00000000..85e2fa83 --- /dev/null +++ b/app/controllers/concerns/conferences_controller_override.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module ConferencesControllerOverride + extend ActiveSupport::Concern + + included do + def hashtag_for_current_url + @hashtag_for_current_url ||= Rails.application.secrets.custom_conference_types.find do |item| + item[:url] == request.path + end&.dig(:hashtag) + end + + def published_conferences + @published_conferences ||= conferences_query(Decidim::Conferences::OrganizationPublishedConferences) + end + + def conferences + @conferences ||= conferences_query(Decidim::Conferences::OrganizationPrioritizedConferences) + end + + alias_method :collection, :conferences + + def promoted_conferences + @promoted_conferences ||= hashtag_for_current_url ? promoted_filtered_by_hashtag(conferences) : promoted_conferences_without_custom_hashtag(Decidim::Conference.promoted) + end + + private + + def custom_hashtags + @custom_hashtags ||= Rails.application.secrets.custom_conference_types.map { |item| item[:hashtag] } + end + + def conferences_query(conference_class) + conferences = conference_class.new(current_organization, current_user) + hashtag_for_current_url ? conferences_with_custom_hashtag_filter(conferences) : conferences_without_custom_hashtag(conferences) + end + + def conferences_with_custom_hashtag_filter(conferences) + conferences.query.merge(Decidim::Conference.filtered_by_hashtag(hashtag_for_current_url)) + end + + def conferences_without_custom_hashtag(conferences) + conferences.query.to_a.reject do |conference| + conference_hashtags = conference.hashtag.split + (conference_hashtags & custom_hashtags).any? + end + end + + def promoted_filtered_by_hashtag(conferences) + conferences.merge(Decidim::Conference.promoted.filtered_by_hashtag(hashtag_for_current_url)) + end + + def promoted_conferences_without_custom_hashtag(conferences) + conferences.select do |conference| + conference_hashtags = conference.hashtag.split + (conference_hashtags & custom_hashtags).empty? + end + end + end +end diff --git a/app/models/concerns/conference_override.rb b/app/models/concerns/conference_override.rb new file mode 100644 index 00000000..37ec4dd5 --- /dev/null +++ b/app/models/concerns/conference_override.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module ConferenceOverride + extend ActiveSupport::Concern + + included do + def self.filtered_by_hashtag(hashtag) + where("(CONCAT(' ', hashtag, ' ') ILIKE ?) OR (CONCAT(' ', hashtag, ' ') ILIKE ?)", "% #{hashtag} %", "% ##{hashtag} %") + end + end +end diff --git a/bin/spring b/bin/spring index ee9de35b..377edc8a 100755 --- a/bin/spring +++ b/bin/spring @@ -1,7 +1,7 @@ #!/usr/bin/env ruby # frozen_string_literal: true -if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"]) +if !defined?(Spring) && [nil, "development", "test"].include?(ENV.fetch("RAILS_ENV", nil)) gem "bundler" require "bundler" diff --git a/bin/webpack b/bin/webpack index 42c74e1f..b271fe76 100755 --- a/bin/webpack +++ b/bin/webpack @@ -1,7 +1,7 @@ #!/usr/bin/env ruby # frozen_string_literal: true -ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" +ENV["RAILS_ENV"] ||= ENV.fetch("RACK_ENV", nil) || "development" ENV["NODE_ENV"] ||= "development" require "pathname" diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server index 7f9e64a0..1bcc73d7 100755 --- a/bin/webpack-dev-server +++ b/bin/webpack-dev-server @@ -1,7 +1,7 @@ #!/usr/bin/env ruby # frozen_string_literal: true -ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" +ENV["RAILS_ENV"] ||= ENV.fetch("RACK_ENV", nil) || "development" ENV["NODE_ENV"] ||= "development" require "pathname" diff --git a/config/application.rb b/config/application.rb index 6cd6db94..86069292 100644 --- a/config/application.rb +++ b/config/application.rb @@ -22,58 +22,5 @@ class Application < Rails::Application # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") - - # generic overrides - config.to_prepare do - Decidim::StatisticCell.include(Decidim::StatisticCellOverride) - end - - initializer "decidim.core.homepage_content_blocks" do - Decidim.content_blocks.register(:homepage, :active_processes) do |content_block| - content_block.cell = "decidim/content_blocks/active_processes" - content_block.settings_form_cell = "decidim/content_blocks/active_processes_settings_form" - content_block.public_name_key = "decidim.content_blocks.active_processes.name" - - content_block.settings do |settings| - settings.attribute :button_text, type: :text, translated: true - settings.attribute :button_url, type: :text - - (1..3).each do |i| - settings.attribute "link_url_#{i}".to_sym, type: :text - settings.attribute "link_text_#{i}".to_sym, type: :text, translated: true - settings.attribute "text_color_#{i}".to_sym, type: :string - end - end - - (1..3).each do |i| - content_block.images << { - name: "image_#{i}".to_sym, - uploader: "Decidim::ActiveProcessesImageUploader" - } - end - - content_block.default! - end - - Decidim.content_blocks.register(:homepage, :extended_hero) do |content_block| - content_block.cell = "decidim/content_blocks/extended_hero" - content_block.settings_form_cell = "decidim/content_blocks/extended_hero_settings_form" - content_block.public_name_key = "decidim.content_blocks.extended_hero.name" - - content_block.settings do |settings| - settings.attribute :welcome_title, type: :text, translated: true - settings.attribute :subtitle, type: :text, translated: true - end - - content_block.images = [ - { - name: :background_image, - uploader: "Decidim::HomepageImageUploader" - } - ] - - content_block.default! - end - end end end diff --git a/config/environments/production.rb b/config/environments/production.rb index 1aadb62f..bb9cda18 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -27,7 +27,7 @@ config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? # Enable serving of images, stylesheets, and JavaScripts from an asset server. - config.asset_host = ENV["RAILS_ASSET_HOST"] if ENV["RAILS_ASSET_HOST"].present? + config.asset_host = ENV.fetch("RAILS_ASSET_HOST", nil) if ENV["RAILS_ASSET_HOST"].present? # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache @@ -46,7 +46,7 @@ # Include generic and useful information about system operation, but avoid logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). - config.log_level = %w(debug info warn error fatal).include?(ENV["RAILS_LOG_LEVEL"]) ? ENV["RAILS_LOG_LEVEL"] : :info + config.log_level = %w(debug info warn error fatal).include?(ENV.fetch("RAILS_LOG_LEVEL", nil)) ? ENV["RAILS_LOG_LEVEL"] : :info # Prepend all log lines with the following tags. config.log_tags = [:request_id] @@ -55,7 +55,7 @@ # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). - config.active_job.queue_adapter = ENV["QUEUE_ADAPTER"] if ENV["QUEUE_ADAPTER"].present? + config.active_job.queue_adapter = ENV.fetch("QUEUE_ADAPTER", nil) if ENV["QUEUE_ADAPTER"].present? # config.active_job.queue_name_prefix = "decidim_staging_production" config.action_mailer.perform_caching = false @@ -124,7 +124,5 @@ # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session - if config.respond_to?(:deface) - config.deface.enabled = ENV["DB_ADAPTER"].blank? || ENV["DB_ADAPTER"] == "postgresql" - end + config.deface.enabled = ENV["DB_ADAPTER"].blank? || ENV.fetch("DB_ADAPTER", nil) == "postgresql" if config.respond_to?(:deface) end diff --git a/config/initializers/conferences_hacks.rb b/config/initializers/conferences_hacks.rb new file mode 100644 index 00000000..392203f8 --- /dev/null +++ b/config/initializers/conferences_hacks.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# extra menus defined in secrets.yml +Decidim.menu :menu do |menu| + custom_conference_types = Rails.application.secrets.custom_conference_types || [] + custom_conference_types.each do |item| + hashtag = item[:hashtag] + options = {} + options[:position] = item[:position].to_i if item[:position] + options[:active] = item[:active].to_sym if item[:active] + options[:icon_name] = item[:icon_name].to_s if item[:icon_name] + options[:if] = Decidim::Conference.exists?(Decidim::Conference.filtered_by_hashtag(hashtag)) + menu.add_item item[:key], I18n.t(item[:key], scope: "decidim.conferences.custom_conference_types"), item[:url], options + end +end + +Rails.application.config.to_prepare do + Decidim::StatisticCell.include(Decidim::StatisticCellOverride) + Decidim::Conference.include(ConferenceOverride) +end + +Rails.application.config.after_initialize do + Decidim::Conferences::ConferencesController.include(ConferencesControllerOverride) +end + +Rails.application.config do + initializer "capitalitat.homepage_content_blocks" do + config.to_prepare do + Decidim.content_blocks.register(:homepage, :active_processes) do |content_block| + content_block.cell = "decidim/content_blocks/active_processes" + content_block.settings_form_cell = "decidim/content_blocks/active_processes_settings_form" + content_block.public_name_key = "decidim.content_blocks.active_processes.name" + + content_block.settings do |settings| + settings.attribute :button_text, type: :text, translated: true + settings.attribute :button_url, type: :text + + (1..3).each do |i| + settings.attribute "link_url_#{i}".to_sym, type: :text + settings.attribute "link_text_#{i}".to_sym, type: :text, translated: true + settings.attribute "text_color_#{i}".to_sym, type: :string + end + end + + (1..3).each do |i| + content_block.images << { + name: "image_#{i}".to_sym, + uploader: "Decidim::ActiveProcessesImageUploader" + } + end + + content_block.default! + end + + Decidim.content_blocks.register(:homepage, :extended_hero) do |content_block| + content_block.cell = "decidim/content_blocks/extended_hero" + content_block.settings_form_cell = "decidim/content_blocks/extended_hero_settings_form" + content_block.public_name_key = "decidim.content_blocks.extended_hero.name" + + content_block.settings do |settings| + settings.attribute :welcome_title, type: :text, translated: true + settings.attribute :subtitle, type: :text, translated: true + end + + content_block.images = [ + { + name: :background_image, + uploader: "Decidim::HomepageImageUploader" + } + ] + + content_block.default! + end + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 0b826187..b579e6f0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,6 +1,10 @@ --- en: decidim: + conferences: + custom_conference_types: + city: City conferences + parallel: Parallel conferences content_blocks: active_processes: name: Active processes diff --git a/config/routes.rb b/config/routes.rb index 8425519e..fb0d1afd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -12,5 +12,11 @@ resources :iframe, only: [:index] end + scope module: "decidim/conferences" do + Rails.application.secrets.custom_conference_types.each do |item| + get item[:url], to: "conferences#index" + end + end + mount Decidim::Core::Engine => "/" end diff --git a/config/secrets.yml b/config/secrets.yml index 37e20d18..e99fd31b 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -112,6 +112,15 @@ storage_default: &storage_default client_x509_cert_url: <%= Decidim::Env.new("GCS_CLIENT_X509_CERT_URL").to_s %> default: &default + custom_conference_types: + - key: parallel # used to find the I18n key and route path + url: /conferences/parallel + position: 6 + hashtag: actesparalels + - key: city # used to find the I18n key and route path + url: /conferences/city + position: 6 + hashtag: actesciutat decidim: <<: *decidim_default omniauth: diff --git a/spec/controllers/conferences_controller_spec.rb b/spec/controllers/conferences_controller_spec.rb new file mode 100644 index 00000000..c26d9063 --- /dev/null +++ b/spec/controllers/conferences_controller_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require "rails_helper" + +module Decidim + module Conferences + describe ConferencesController, type: :controller do + include ConferencesControllerOverride + routes { Decidim::Conferences::Engine.routes } + + let(:organization) { create(:organization) } + + let!(:published1) { create(:conference, :published, organization: organization, hashtag: "actesciutat") } + let!(:published2) { create(:conference, :published, organization: organization, hashtag: "actesparalels") } + let!(:published_no_hashtag) { create(:conference, :published, organization: organization, hashtag: "") } + let!(:promoted) { create(:conference, :published, :promoted, organization: organization, hashtag: "actesciutat") } + let!(:promoted_no_hashtag) { create(:conference, :published, :promoted, organization: organization, hashtag: "") } + + before do + request.env["decidim.current_organization"] = organization + allow(Rails.application.secrets).to receive(:custom_conference_types).and_return([{ url: "/conferences/city", hashtag: "actesciutat" }]) + end + + describe "published_conferences" do + context "when the custom hashtag exists" do + before do + request.env["PATH_INFO"] = "/conferences/city" + get :index + end + + it "includes only conferences with the matching hashtag" do + expect(controller.published_conferences).to match_array([published1, promoted]) + end + end + + context "when the custom hashtag does not exist" do + it "includes all published conferences except those with custom hashtag" do + get :index + + expect(controller.published_conferences).to match_array([published2, published_no_hashtag, promoted_no_hashtag]) + end + end + end + + describe "conferences" do + context "when the custom hashtag exists" do + before do + request.env["PATH_INFO"] = "/conferences/city" + get :index + end + + it "includes only conferences with the matching hashtag" do + expect(controller.helpers.conferences).to match_array([published1, promoted]) + end + end + + context "when the custom hashtag does not exist" do + it "includes all conferences except those with custom hashtags" do + get :index + + expect(controller.helpers.conferences).to match_array([published2, published_no_hashtag, promoted_no_hashtag]) + end + end + end + + describe "promoted_conferences" do + context "when the custom hashtag exists" do + before do + request.env["PATH_INFO"] = "/conferences/city" + get :index + end + + it "includes only promoted conferences with the matching hashtag" do + expect(controller.helpers.promoted_conferences).to match_array([promoted]) + end + end + + context "when the custom hashtag does not exist" do + it "includes all promoted conferences except those with custom hashtag" do + get :index + + expect(controller.helpers.promoted_conferences).to match_array([promoted_no_hashtag]) + end + end + end + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb new file mode 100644 index 00000000..2bd662e2 --- /dev/null +++ b/spec/factories.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require "decidim/core/test/factories" +require "decidim/conferences/test/factories" diff --git a/spec/lib/overrides_spec.rb b/spec/lib/overrides_spec.rb new file mode 100644 index 00000000..deefe5d1 --- /dev/null +++ b/spec/lib/overrides_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "rails_helper" + +# We make sure that the checksum of the file overriden is the same +# as the expected. If this test fails, it means that the overriden +# file should be updated to match any change/bug fix introduced in the core +checksums = [ + { + package: "decidim-conferences", + files: { + "/app/controllers/decidim/conferences/conferences_controller.rb" => "797899ada946b487a3c8c7312f0c14eb", + "/app/models/decidim/conference.rb" => "8d7b097f3dc4d626b6972f380332b713" + } + }, + { + package: "decidim-core", + files: { + "/app/cells/decidim/statistic_cell.rb" => "b6f5eb0ab09653dc5633656b84fc2f7b" + } + } +] + +describe "Overriden files", type: :view do + checksums.each do |item| + # rubocop:disable Rails/DynamicFindBy + spec = ::Gem::Specification.find_by_name(item[:package]) + # rubocop:enable Rails/DynamicFindBy + item[:files].each do |file, signature| + it "#{spec.gem_dir}#{file} matches checksum" do + expect(md5("#{spec.gem_dir}#{file}")).to eq(signature) + end + end + end + + private + + def md5(file) + Digest::MD5.hexdigest(File.read(file)) + end +end diff --git a/spec/models/conference_override_spec.rb b/spec/models/conference_override_spec.rb new file mode 100644 index 00000000..2d15fd3f --- /dev/null +++ b/spec/models/conference_override_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ConferenceOverride, type: :model do + describe ".filtered_by_hashtag" do + let(:organization) { create(:organization) } + + let!(:found) do + [ + create(:conference, organization: organization, hashtag: "#city"), + create(:conference, organization: organization, hashtag: "city"), + create(:conference, organization: organization, hashtag: " city "), + create(:conference, organization: organization, hashtag: "city "), + create(:conference, organization: organization, hashtag: " city"), + create(:conference, organization: organization, hashtag: "welcome to city "), + create(:conference, organization: organization, hashtag: "sunny city"), + create(:conference, organization: organization, hashtag: "the city in the middle"), + create(:conference, organization: organization, hashtag: "the #city in the middle") + ] + end + + let!(:not_found) do + [ + create(:conference, organization: organization, hashtag: "cityofdreams"), + create(:conference, organization: organization, hashtag: "#cityofdreams"), + create(:conference, organization: organization, hashtag: "my cityofdreams is missing"), + create(:conference, organization: organization, hashtag: "my #cityofdreams is missing") + ] + end + + it "returns all conferences" do + expect(Decidim::Conference.all).to match_array(found + not_found) + end + + it "returns the conference that contains the hashtag" do + result = Decidim::Conference.filtered_by_hashtag("city") + expect(result).to match_array(found) + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 00000000..1bbb8dea --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# This file is copied to spec/ when you run 'rails generate rspec:install' + +require "spec_helper" +require "simplecov" +SimpleCov.start "rails" +ENV["RAILS_ENV"] ||= "test" + +require File.expand_path("../config/environment", __dir__) +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require "rspec/rails" +# Add additional requires below this line. Rails is not loaded until this point! + +require "decidim/dev" + +Decidim::Dev.dummy_app_path = File.expand_path(File.join(__dir__, "..")) + +require "decidim/dev/test/base_spec_helper" + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 +end +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + # config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..a47eea59 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + # config.disable_monkey_patching! + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = "doc" + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed + + config.order = :random + + # Make :en locale available in tests. + config.before do + I18n.available_locales = [:ca, :en, :es] + I18n.default_locale = :en + Decidim.available_locales = [:ca, :en, :es] + Decidim.default_locale = :en + end + + config.around do |example| + I18n.with_locale(:en) { example.run } + end +end diff --git a/spec/system/conferences_spec.rb b/spec/system/conferences_spec.rb new file mode 100644 index 00000000..f010f320 --- /dev/null +++ b/spec/system/conferences_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "Visit conferences", type: :system do + let!(:organization) { create :organization } + let!(:conference_city) { create :conference, :published, :promoted, organization: organization, hashtag: hashtag_city } + let!(:conference_global) { create :conference, :published, :promoted, organization: organization, hashtag: hashtag_global } + let!(:conference_parallel) { create :conference, :published, :promoted, organization: organization, hashtag: hashtag_parallel } + let(:hashtag_global) { "actesglobals" } + let(:hashtag_parallel) { "actesparalels" } + + before do + switch_to_host(organization.host) + end + + context "when visiting home page" do + let!(:hashtag_city) { "actesciutat" } + + before do + visit decidim.root_path + end + + it "shows the original conference menu" do + within ".main-nav" do + expect(page).to have_content("Conferences") + expect(page).to have_link(href: "/conferences") + end + end + + it "shows the city conference menu" do + within ".main-nav" do + expect(page).to have_content("City conferences") + expect(page).to have_link(href: "/conferences/city") + end + + click_link "City conferences" + + expect(page).to have_content("1 CONFERENCE") + expect(page).to have_content(conference_city.title["en"], count: 2) + expect(page).not_to have_content(conference_global.title["en"]) + expect(page).not_to have_content(conference_parallel.title["en"]) + end + + it "shows the global conference menu" do + within ".main-nav" do + expect(page).to have_content("Conferences") + expect(page).to have_link(href: "/conferences") + end + + click_link "Conferences" + + expect(page).to have_content("1 CONFERENCE") + expect(page).to have_content(conference_global.title["en"], count: 2) + expect(page).not_to have_content(conference_city.title["en"]) + expect(page).not_to have_content(conference_parallel.title["en"]) + end + + it "shows the parallel conference menu" do + within ".main-nav" do + expect(page).to have_content("Parallel conferences") + expect(page).to have_link(href: "/conferences/parallel") + end + + click_link "Parallel conferences" + + expect(page).to have_content("1 CONFERENCE") + expect(page).to have_content(conference_parallel.title["en"], count: 2) + expect(page).not_to have_content(conference_city.title["en"]) + expect(page).not_to have_content(conference_global.title["en"]) + end + end + + context "when hashtag with #" do + let!(:hashtag_city) { "#actesciutat" } + + before do + visit decidim.root_path + end + + it "shows the city conference menu" do + within ".main-nav" do + expect(page).to have_content("City conferences") + expect(page).to have_link(href: "/conferences/city") + end + + click_link "City conferences" + + expect(page).to have_content("1 CONFERENCE") + end + end + + context "when hashtag contains spaces" do + let!(:hashtag_city) { " actesciutat " } + + before do + visit decidim.root_path + end + + it "shows the city conference menu" do + within ".main-nav" do + expect(page).to have_content("City conferences") + expect(page).to have_link(href: "/conferences/city") + end + + click_link "City conferences" + + expect(page).to have_content("1 CONFERENCE") + expect(page).to have_content(conference_city.title["en"]) + expect(page).not_to have_content(conference_global.title["en"]) + expect(page).not_to have_content(conference_parallel.title["en"]) + end + end +end