diff --git a/Gemfile.lock b/Gemfile.lock index 2571c53..86e2efd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,6 +4,7 @@ PATH decidim-anonymous_codes (1.0) decidim-admin (>= 0.27.0, < 0.28) decidim-core (>= 0.27.0, < 0.28) + decidim-forms (>= 0.27.0, < 0.28) decidim-surveys (>= 0.27.0, < 0.28) deface (>= 1.9.0) diff --git a/Rakefile b/Rakefile index 98db11d..92564cd 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,8 @@ # frozen_string_literal: true -require "decidim/dev/common_rake" require "fileutils" +require "decidim/dev/common_rake" +require "decidim/anonymous_codes/engine" def install_module(path) Dir.chdir(path) do @@ -13,6 +14,7 @@ end def seed_db(path) Dir.chdir(path) do system("bundle exec rake db:seed") + system("bundle exec rake decidim_anonymous_codes:db:seed") end end diff --git a/app/controllers/concerns/decidim/anonymous_codes/surveys_controller_override.rb b/app/controllers/concerns/decidim/anonymous_codes/surveys_controller_override.rb new file mode 100644 index 0000000..c2e67a4 --- /dev/null +++ b/app/controllers/concerns/decidim/anonymous_codes/surveys_controller_override.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Decidim + module AnonymousCodes + module SurveysControllerOverride + extend ActiveSupport::Concern + + included do + before_action do + next unless current_settings.allow_answers? && survey.open? + next if visitor_already_answered? + + if token_groups.any? + next if current_token&.available? + + if current_token.blank? + flash.now[:alert] = I18n.t("decidim.anonymous_codes.invalid_code") if params.has_key?(:token) + elsif current_token.used? + flash.now[:alert] = I18n.t("decidim.anonymous_codes.used_code") + elsif current_token.expired? + flash.now[:alert] = I18n.t("decidim.anonymous_codes.expired_code") + end + render "decidim/anonymous_codes/surveys/code_required" + end + end + + after_action only: :answer do + next unless current_token&.available? + + # find any answer for the current user and questionnaire that would be used as a resource to link the usage counter + answer = Decidim::Forms::Answer.find_by(questionnaire: questionnaire, user: current_user, session_token: @form.context.session_token) + current_token.answers << answer if answer.present? + end + + private + + def token_groups + @token_groups ||= Decidim::AnonymousCodes::Group.where(resource: survey, active: true) + end + + def current_token + @current_token ||= Decidim::AnonymousCodes::Token.where(group: token_groups).find_by(token: token_param) + end + + def token_param + @token_param ||= begin + session[:anonymous_codes_token] = params[:token] if params.has_key?(:token) + session[:anonymous_codes_token] + end + end + end + end + end +end diff --git a/app/models/decidim/anonymous_codes/token.rb b/app/models/decidim/anonymous_codes/token.rb index 8972ebd..122124a 100644 --- a/app/models/decidim/anonymous_codes/token.rb +++ b/app/models/decidim/anonymous_codes/token.rb @@ -10,8 +10,25 @@ class Token < ApplicationRecord throw(:abort) if usage_count.positive? end - belongs_to :group, class_name: "Decidim::AnonymousCodes::Group" - belongs_to :resource, polymorphic: true, optional: true + belongs_to :group, class_name: "Decidim::AnonymousCodes::Group", counter_cache: true + has_many :token_resources, class_name: "Decidim::AnonymousCodes::TokenResource", dependent: :destroy + has_many :answers, through: :token_resources, source_type: "Decidim::Forms::Answer", source: :resource + + delegate :active?, to: :group + validates :token, presence: true + validates :token, uniqueness: { scope: [:group] } + + def available? + !used? && !expired? && active? + end + + def used? + usage_count.to_i >= group.max_reuses.to_i + end + + def expired? + group.expires_at.present? && group.expires_at < Time.current + end end end end diff --git a/app/models/decidim/anonymous_codes/token_resource.rb b/app/models/decidim/anonymous_codes/token_resource.rb new file mode 100644 index 0000000..6d5bf37 --- /dev/null +++ b/app/models/decidim/anonymous_codes/token_resource.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Decidim + module AnonymousCodes + class TokenResource < ApplicationRecord + self.table_name = :decidim_anonymous_codes_token_resources + + belongs_to :token, class_name: "Decidim::AnonymousCodes::Token", counter_cache: :usage_count + belongs_to :resource, polymorphic: true + + validate :max_uses_not_exceeded + validate :valid_parent_questionnaire + + private + + def max_uses_not_exceeded + return unless token.usage_count >= token.group.max_reuses + + errors.add(:base, :max_uses_exceeded) + end + + def valid_parent_questionnaire + return unless resource.try(:questionnaire) != token.group.resource.try(:questionnaire) + + errors.add(:base, :invalid_questionnaire) + end + end + end +end diff --git a/app/views/decidim/anonymous_codes/surveys/code_required.html.erb b/app/views/decidim/anonymous_codes/surveys/code_required.html.erb new file mode 100644 index 0000000..b6afd7d --- /dev/null +++ b/app/views/decidim/anonymous_codes/surveys/code_required.html.erb @@ -0,0 +1,50 @@ +<% add_decidim_meta_tags({ + title: translated_attribute(questionnaire.title), + description: translated_attribute(questionnaire.description), +}) %> + +<%= render partial: "decidim/shared/component_announcement" if current_component.manifest_name == "surveys" %> + +
+

<%= translated_attribute questionnaire.title %>

+
+
+ <%= decidim_sanitize_editor_admin translated_attribute questionnaire.description %> +
+
+
+ +
+
+
+
+ <% unless questionnaire_for.try(:component)&.try(:published?) %> +
+
+

<%= t("questionnaire_not_published.body", scope: "decidim.forms.questionnaires.show") %>

+
+
+ <% end %> + +
+
+

<%= t(".title") %>

+

<%= t(".body") %>

+
+ + +
+
+ <%= text_field_tag :token, nil, class: "input", required: true %> + +
+
+
+
+
+
+
+ +<% content_for :js_content do %> + <%= javascript_pack_tag "decidim_forms" %> +<% end %> diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 133736d..6dbcf99 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -6,6 +6,7 @@ locales: [en] data: external: - "<%= %x[bundle info decidim-core --path].chomp %>/config/locales/%{locale}.yml" + - "<%= %x[bundle info decidim-forms --path].chomp %>/config/locales/%{locale}.yml" ignore_unused: ignore_missing: diff --git a/config/locales/en.yml b/config/locales/en.yml index ecc9f75..7fb2eb1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,5 +1,12 @@ --- en: + activerecord: + errors: + models: + decidim/anonymous_codes/token_resource: + invalid_questionnaire: The attached answer belongs to a different questionnaire. + max_uses_exceeded: The code has already been used the maximum number of + times. decidim: anonymous_codes: admin: @@ -15,3 +22,13 @@ en: still apply if you choose to use this option. groups: 'Edit groups:' new_group: "\U0001F449 Create answer codes here" + expired_code: The introduced code has expired. Please try again. + invalid_code: The introduced code is invalid. Please try again. + surveys: + code_required: + body: This form can only be accessed using a valid code. If you have a valid + code, please enter it below. + label: Code + submit: Continue + title: Form restricted + used_code: The introduced code has already been used. Please try again. diff --git a/db/migrate/20240403091336_create_decidim_anonymous_codes_groups.rb b/db/migrate/20240403091336_create_decidim_anonymous_codes_groups.rb index e4c4ef0..15f7674 100644 --- a/db/migrate/20240403091336_create_decidim_anonymous_codes_groups.rb +++ b/db/migrate/20240403091336_create_decidim_anonymous_codes_groups.rb @@ -8,6 +8,7 @@ def change t.datetime :expires_at t.boolean :active, default: true, null: false t.integer :max_reuses, default: 1, null: false + t.integer :tokens_count, default: 0, null: false t.references :resource, polymorphic: true, null: true, index: { name: "decidim_anonymous_codes_groups_on_resource" } t.references :decidim_organization, null: false, foreign_key: true, index: { name: "decidim_anonymous_codes_groups_on_organization" } diff --git a/db/migrate/20240403091356_create_decidim_anonymous_codes_tokens.rb b/db/migrate/20240403091356_create_decidim_anonymous_codes_tokens.rb index 1c6b7ed..38d3c1f 100644 --- a/db/migrate/20240403091356_create_decidim_anonymous_codes_tokens.rb +++ b/db/migrate/20240403091356_create_decidim_anonymous_codes_tokens.rb @@ -7,7 +7,7 @@ def change t.integer :usage_count, default: 0, null: false t.references :group, null: false, index: { name: "decidim_anonymous_codes_tokens_on_group" } - t.references :resource, polymorphic: true, null: true, index: { name: "decidim_anonymous_codes_tokens_on_resource" } + t.index [:token, :group_id], name: "index_anonymous_codes_token_group_uniqueness", unique: true t.timestamps end end diff --git a/db/migrate/20240403091376_create_decidim_anonymous_codes_token_resources.rb b/db/migrate/20240403091376_create_decidim_anonymous_codes_token_resources.rb new file mode 100644 index 0000000..01b1c6f --- /dev/null +++ b/db/migrate/20240403091376_create_decidim_anonymous_codes_token_resources.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class CreateDecidimAnonymousCodesTokenResources < ActiveRecord::Migration[6.1] + def change + create_table :decidim_anonymous_codes_token_resources do |t| + t.references :token, null: false, index: { name: "decidim_anonymous_codes_token_resources_on_token" } + t.references :resource, polymorphic: true, null: false, index: { name: "decidim_anonymous_codes_token_resources_on_resource" } + t.timestamps + end + end +end diff --git a/db/seeds.rb b/db/seeds.rb index 0bc7ce5..b7e5015 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,33 +1,54 @@ # frozen_string_literal: true if !Rails.env.production? || ENV.fetch("SEED", nil) - print "Creating seeds for decidim_anonymous_codes...\n" unless Rails.env.test? - organization = Decidim::Organization.first + participatory_processes = Decidim::ParticipatoryProcess.where(organization: organization) + + unless participatory_processes.count.positive? + puts "No participatory processes found. Skipping seeds for decidim_anonymous_codes..." + return + end + print "Creating seeds for decidim_anonymous_codes...\n" unless Rails.env.test? - group1 = Decidim::AnonymousCodes::Group.create!( - title: { en: "First Group" }, - organization: organization, - expires_at: 1.week.from_now, - active: true, - max_reuses: 1, - resource: Decidim::Surveys::Survey.first - ) + admin = Decidim::User.where(admin: true).first + survey_components = Decidim::Component.where(manifest_name: "surveys", participatory_space: participatory_processes) + survey_components.each do |component| + form = OpenStruct.new(name: component.name, weight: component.weight, settings: component.settings, default_step_settings: component.default_step_settings, + step_settings: component.step_settings) + form.step_settings.each do |key, _value| + form.step_settings[key]["allow_answers"] = true + form.step_settings[key]["allow_unregistered"] = true + end + Decidim::Admin::UpdateComponent.call(form, component, admin) do + on(:ok) do + puts "Component #{component.id} updated for allowing answers and unregistered users" + end - group2 = Decidim::AnonymousCodes::Group.create!( - title: { en: "Second Group" }, - organization: organization, - expires_at: 1.week.from_now, - active: true, - max_reuses: 1, - resource: Decidim::Surveys::Survey.second - ) + on(:invalid) do + puts "ERROR: Component #{component.id} not updated for allowing answers and unregistered users" + end + end - 5.times do - Decidim::AnonymousCodes::Token.create!(token: Decidim::AnonymousCodes.token_generator, usage_count: 0, group: group1) - end + survey = Decidim::Surveys::Survey.find_by(component: component) + next unless survey - 25.times do - Decidim::AnonymousCodes::Token.create!(token: Decidim::AnonymousCodes.token_generator, usage_count: 0, group: group2) + group = Decidim.traceability.create!( + Decidim::AnonymousCodes::Group, + admin, + { + title: { en: "Group for Survey #{survey.id}" }, + organization: organization, + expires_at: 1.week.from_now, + active: rand(2).zero?, + max_reuses: rand(3), + resource: survey + }, + visibility: "admin-only" + ) + total = rand(1..25) + total.times do + Decidim::AnonymousCodes::Token.create!(token: Decidim::AnonymousCodes.token_generator, group: group) + end + puts "Created #{total} tokens for survey #{survey.id}" end end diff --git a/decidim-anonymous_codes.gemspec b/decidim-anonymous_codes.gemspec index 78a68ae..1ef6fe9 100644 --- a/decidim-anonymous_codes.gemspec +++ b/decidim-anonymous_codes.gemspec @@ -25,6 +25,7 @@ Gem::Specification.new do |spec| spec.add_dependency "decidim-admin", Decidim::AnonymousCodes::COMPAT_DECIDIM_VERSION spec.add_dependency "decidim-core", Decidim::AnonymousCodes::COMPAT_DECIDIM_VERSION + spec.add_dependency "decidim-forms", Decidim::AnonymousCodes::COMPAT_DECIDIM_VERSION spec.add_dependency "decidim-surveys", Decidim::AnonymousCodes::COMPAT_DECIDIM_VERSION spec.add_dependency "deface", ">= 1.9.0" diff --git a/lib/decidim/anonymous_codes/engine.rb b/lib/decidim/anonymous_codes/engine.rb index c37b7d2..1c1f01b 100644 --- a/lib/decidim/anonymous_codes/engine.rb +++ b/lib/decidim/anonymous_codes/engine.rb @@ -9,9 +9,9 @@ class Engine < ::Rails::Engine isolate_namespace Decidim::AnonymousCodes initializer "decidim_anonymous_codes.overrides", after: "decidim.action_controller" do - # config.to_prepare do - - # end + config.to_prepare do + Decidim::Surveys::SurveysController.include(Decidim::AnonymousCodes::SurveysControllerOverride) + end end initializer "decidim-anonymous_codes.webpacker.assets_path" do diff --git a/lib/decidim/anonymous_codes/test/factories.rb b/lib/decidim/anonymous_codes/test/factories.rb index 8cc9a03..5d864b9 100644 --- a/lib/decidim/anonymous_codes/test/factories.rb +++ b/lib/decidim/anonymous_codes/test/factories.rb @@ -12,7 +12,17 @@ factory :anonymous_codes_token, class: "Decidim::AnonymousCodes::Token" do token { Decidim::AnonymousCodes.token_generator } - usage_count { 0 } group { create(:anonymous_codes_group) } + + trait :used do + after(:create) do |object| + object.token_resources << create(:anonymous_codes_token_resource, token: object) + end + end + end + + factory :anonymous_codes_token_resource, class: "Decidim::AnonymousCodes::TokenResource" do + token { create(:anonymous_codes_token) } + resource { create(:answer, questionnaire: token.group.resource.questionnaire) } end end diff --git a/lib/tasks/decidim_anonymous_codes_upgrade_tasks.rake b/lib/tasks/decidim_anonymous_codes_upgrade_tasks.rake index 86db4b4..15da310 100644 --- a/lib/tasks/decidim_anonymous_codes_upgrade_tasks.rake +++ b/lib/tasks/decidim_anonymous_codes_upgrade_tasks.rake @@ -3,3 +3,12 @@ Rake::Task["decidim:choose_target_plugins"].enhance do ENV["FROM"] = "#{ENV.fetch("FROM", nil)},decidim_anonymous_codes" end + +namespace :decidim_anonymous_codes do + namespace :db do + desc "loads all seeds in db/seeds.rb" + task seed: :environment do + Decidim::AnonymousCodes::Engine.load_seed + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index ebe4f82..563b0c0 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -2,5 +2,6 @@ require "decidim/core/test/factories" require "decidim/surveys/test/factories" +require "decidim/forms/test/factories" require "decidim/proposals/test/factories" require "decidim/anonymous_codes/test/factories" diff --git a/spec/lib/overrides_spec.rb b/spec/lib/overrides_spec.rb index 2a805c4..d1da067 100644 --- a/spec/lib/overrides_spec.rb +++ b/spec/lib/overrides_spec.rb @@ -7,8 +7,16 @@ # file should be updated to match any change/bug fix introduced in the core checksums = [ { - package: "decidim-core", + package: "decidim-surveys", files: { + "/app/controllers/decidim/surveys/surveys_controller.rb" => "f443ed19838d251fd9d9b960e5908418" + } + }, + { + package: "decidim-forms", + files: { + "/app/controllers/decidim/forms/concerns/has_questionnaire.rb" => "3e68234b1e489158b3377426236db093", + "/app/views/decidim/forms/questionnaires/show.html.erb" => "b54864ffbdaab74bfc82f7b047cbf170" } } ] diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 136ef92..a1c4a72 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -14,6 +14,10 @@ module AnonymousCodes it { expect(described_class.reflect_on_association(:organization).macro).to eq(:belongs_to) } end + it "has an empty counter cache" do + expect(group.tokens_count).to eq(0) + end + context "when tokens exist" do let!(:token) { create(:anonymous_codes_token, group: group) } @@ -21,6 +25,14 @@ module AnonymousCodes expect(group.organization).to be_a(Decidim::Organization) end + it "has a counter cache" do + expect(group.tokens_count).to eq(1) + end + + it "lowers the counter on destroying tokens" do + expect { token.destroy }.to change(group, :tokens_count).by(-1) + end + it "destroys the tokens when destroyed" do expect { group.destroy }.to change(Token, :count).by(-1) end diff --git a/spec/models/token_spec.rb b/spec/models/token_spec.rb index bcc4f9b..1b819e9 100644 --- a/spec/models/token_spec.rb +++ b/spec/models/token_spec.rb @@ -13,6 +13,130 @@ module AnonymousCodes it { expect(described_class.reflect_on_association(:group).macro).to eq(:belongs_to) } end + it { is_expected.not_to be_used } + + it "has an empty counter cache" do + expect(token.usage_count).to eq(0) + end + + context "when adding an answer" do + let(:resource) { create(:answer, questionnaire: token.group.resource.questionnaire) } + + it "can be associated" do + expect { token.answers << resource }.to change(token.token_resources, :count).by(1) + end + + context "and the answer associated does not belong to the parent questionnaire" do + let(:resource) { create(:answer) } + + it "cannot be associated" do + expect { token.answers << resource }.to raise_error(ActiveRecord::RecordInvalid) + end + + context "when different resource type" do + let(:resource) { create(:questionnaire) } + + it "cannot be associated" do + expect { create(:anonymous_codes_token_resource, token: token, resource: resource) }.to raise_error(ActiveRecord::RecordInvalid) + end + end + end + + context "and max uses have been reached" do + before do + token.answers << resource + end + + it "cannot be associated" do + expect { token.answers << resource }.to raise_error(ActiveRecord::RecordInvalid) + end + end + end + + context "when resource exists" do + let!(:token) { create(:anonymous_codes_token, :used) } + + it { is_expected.to be_used } + + it "has a counter cache" do + expect(token.usage_count).to eq(1) + end + + it "lowers the counter on removing the resource" do + expect(TokenResource.count).to eq(1) + expect { token.update!(token_resources: []) }.to change(token, :usage_count).by(-1) + expect(TokenResource.count).to eq(0) + end + + context "when max_reuses is 2" do + before do + token.group.update!(max_reuses: 2) + end + + it { is_expected.not_to be_used } + end + end + + describe "expired?" do + context "when expires_at is nil" do + it { is_expected.not_to be_expired } + end + + context "when expires_at is in the past" do + before do + token.group.update!(expires_at: 1.minute.ago) + end + + it { is_expected.to be_expired } + end + + context "when expires_at is in the future" do + before do + token.group.update!(expires_at: 1.minute.from_now) + end + + it { is_expected.not_to be_expired } + end + end + + describe "active?" do + it { is_expected.to be_active } + + context "when inactive" do + before do + token.group.update!(active: false) + end + + it { is_expected.not_to be_active } + end + end + + describe "available?" do + it { is_expected.to be_available } + + context "when used" do + let(:token) { create(:anonymous_codes_token, usage_count: 1) } + + it { is_expected.not_to be_available } + end + + context "when expired" do + before do + token.group.update!(expires_at: 1.minute.ago) + end + + it { is_expected.not_to be_available } + end + + context "when inactive" do + before do + token.group.update!(active: false) + end + + it { is_expected.not_to be_available } + end + end + context "when created" do let!(:token) { create(:anonymous_codes_token) } @@ -27,6 +151,22 @@ module AnonymousCodes expect { token.destroy }.not_to change(Token, :count) end end + + context "and another one is created with the same token" do + let(:another_token) { build(:anonymous_codes_token, token: token.token, group: token.group) } + + it "cannot be created" do + expect { another_token.save! }.to raise_error(ActiveRecord::RecordInvalid) + end + + context "and a different group" do + let(:another_token) { build(:anonymous_codes_token, token: token.token) } + + it "can be created" do + expect { another_token.save! }.to change(Token, :count).by(1) + end + end + end end end end diff --git a/spec/system/surveys_answering_spec.rb b/spec/system/surveys_answering_spec.rb new file mode 100644 index 0000000..21bb433 --- /dev/null +++ b/spec/system/surveys_answering_spec.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Surveys Component Settings", type: :system do + let(:organization) { component.organization } + let(:component) { survey.component } + let(:user) { create :user, :confirmed, organization: organization } + let(:another_user) { create :user, :confirmed, organization: organization } + let(:group) { create :anonymous_codes_group, organization: organization, expires_at: expires, resource: resource, active: active } + let!(:token) { create :anonymous_codes_token, group: group } + let!(:another_token) { create :anonymous_codes_token } + let(:active) { true } + let(:expires) { nil } + let(:survey) { create(:survey, questionnaire: questionnaire) } + let(:questionnaire) { create(:questionnaire) } + let!(:question) { create(:questionnaire_question, body: { en: "What's the meaning of life?" }, mandatory: true, question_type: :short_answer, questionnaire: questionnaire) } + let(:resource) { survey } + let(:code) { token.token } + let(:settings) do + { + allow_answers: true, + allow_unregistered: allow_unregistered + } + end + let(:allow_unregistered) { false } + + def visit_component + visit Decidim::EngineRouter.main_proxy(component).survey_path(component.id) + end + + before do + component.update!( + step_settings: { + component.participatory_space.active_step.id => settings + } + ) + + switch_to_host(organization.host) + end + + shared_examples "form is restricted" do + it "shows the restricted message" do + expect(page).to have_content("Form restricted") + expect(page).to have_field("token") + end + end + + shared_examples "form is readonly" do + it "sends the code" do + fill_in :token, with: code + click_button("Continue") + expect(page).to have_content(question.body["en"]) + expect(page).to have_link("Sign in with your account") + expect(page).not_to have_button("Submit") + end + end + + shared_examples "form requires codes" do + it "sends the code and the form" do + fill_in :token, with: code + click_button("Continue") + + expect(token).not_to be_used + + fill_in question.body["en"], with: "42" + check "questionnaire_tos_agreement" + accept_confirm { click_button "Submit" } + expect(page).to have_css(".callout.success") + expect(page).to have_content("Already answered") + + expect(token.reload).to be_used + end + end + + shared_examples "can be answered without codes" do + it "sends the form" do + expect(token).not_to be_used + + fill_in question.body["en"], with: "42" + check "questionnaire_tos_agreement" + accept_confirm { click_button "Submit" } + expect(page).to have_css(".callout.success") + expect(page).to have_content("Already answered") + + expect(token.reload).not_to be_used + end + end + + shared_examples "can be answered with codes" do + it_behaves_like "form requires codes" + + context "and code group is inactive" do + let(:active) { false } + + it_behaves_like "can be answered without codes" + end + + context "and code re-uses have exceeded" do + let!(:token) { create :anonymous_codes_token, group: group, answers: [create(:answer, questionnaire: questionnaire, user: another_user)] } + + it "sends the code and fails" do + fill_in :token, with: code + click_button("Continue") + expect(page).to have_css(".callout.alert") + expect(page).to have_content("The introduced code has already been used.") + expect(page).to have_content("Form restricted") + expect(page).to have_field("token") + expect(page).not_to have_content(question.body["en"]) + end + end + + context "and group has an expiration" do + let(:expires) { 1.hour.from_now } + + it_behaves_like "form requires codes" + end + + context "and group has expired" do + let(:expires) { 1.minute.ago } + + it "sends the code and fails" do + fill_in :token, with: code + click_button("Continue") + expect(page).to have_css(".callout.alert") + expect(page).to have_content("The introduced code has expired.") + expect(page).to have_content("Form restricted") + expect(page).to have_field("token") + expect(page).not_to have_content(question.body["en"]) + end + end + end + + shared_examples "cannot be answered with bad codes" do + it "sends the code and fails" do + fill_in :token, with: "dirty-things" + click_button("Continue") + expect(page).to have_css(".callout.alert") + expect(page).to have_content("The introduced code is invalid.") + expect(page).to have_content("Form restricted") + expect(page).to have_field("token") + expect(page).not_to have_content(question.body["en"]) + end + + it "sends another code and fails" do + fill_in :token, with: another_token.token + click_button("Continue") + expect(page).to have_css(".callout.alert") + expect(page).to have_content("The introduced code is invalid.") + expect(page).to have_content("Form restricted") + expect(page).to have_field("token") + expect(page).not_to have_content(question.body["en"]) + end + end + + context "when user is not logged" do + before do + visit_component + end + + it_behaves_like "form is restricted" + it_behaves_like "form is readonly" + + context "and login is not required" do + let(:allow_unregistered) { true } + + it_behaves_like "can be answered with codes" + it_behaves_like "cannot be answered with bad codes" + + context "and there are no codes required" do + let(:resource) { nil } + + it_behaves_like "can be answered without codes" + end + end + end + + context "when user is logged" do + before do + login_as user, scope: :user + visit_component + end + + it_behaves_like "form is restricted" + it_behaves_like "can be answered with codes" + it_behaves_like "cannot be answered with bad codes" + + context "and there are no codes required" do + let(:resource) { nil } + + it_behaves_like "can be answered without codes" + end + end +end