From cf0aa619d5821f5c6e6f909504cab8646754944b Mon Sep 17 00:00:00 2001 From: Duncan <52967253+dunkOnIT@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:58:13 +0200 Subject: [PATCH] Refactor tests to use factories instead of fixtures (#285) RSwag tests and their fixtures have been removed. All RSwag cases are now covered by service tests, as RSwag was being misused. Appropriate RSwag tests will be introduced in a later PR. --- .rubocop.yml | 3 + .ruby-version | 2 +- app/controllers/registration_controller.rb | 97 +- app/helpers/competition_api.rb | 16 +- app/helpers/error_codes.rb | 6 +- app/helpers/lane_factory.rb | 6 +- app/helpers/mocks.rb | 16 +- app/models/registration.rb | 42 +- app/services/registration_checker.rb | 146 +++ app/worker/registration_processor.rb | 2 +- docker-compose.test.yml | 2 +- spec/db/database_functions_spec.rb | 29 - spec/domains/competition_api_spec.rb | 5 - spec/domains/database_spec.rb | 4 - spec/domains/registrations_api_spec.rb | 7 - spec/factories.rb | 46 - spec/factories/competition_factory.rb | 39 +- spec/factories/registration_factory.rb | 39 + spec/factories/request_factory.rb | 91 ++ spec/fixtures/competition_details.json | 10 - spec/fixtures/patches.json | 236 ---- spec/fixtures/registration.json | 38 - spec/fixtures/registrations.json | 1158 -------------------- spec/helpers/competition_api_spec.rb | 6 +- spec/models/registration_spec.rb | 87 ++ spec/requests/cancel_registration_spec.rb | 542 --------- spec/requests/competition_api_spec.rb | 41 - spec/requests/get_registrations_spec.rb | 181 --- spec/requests/post_attendee_spec.rb | 248 ----- spec/requests/update_registration_spec.rb | 417 ------- spec/services/registration_checker_spec.rb | 917 ++++++++++++++++ spec/support/registration_spec_helper.rb | 364 ------ spec/todo/get_attendee_spec.rb | 48 - spec/todo/patch_registration_spec.rb | 48 - 34 files changed, 1384 insertions(+), 3555 deletions(-) create mode 100644 app/services/registration_checker.rb delete mode 100644 spec/db/database_functions_spec.rb delete mode 100644 spec/domains/competition_api_spec.rb delete mode 100644 spec/domains/database_spec.rb delete mode 100644 spec/domains/registrations_api_spec.rb delete mode 100644 spec/factories.rb create mode 100644 spec/factories/registration_factory.rb create mode 100644 spec/factories/request_factory.rb delete mode 100644 spec/fixtures/competition_details.json delete mode 100644 spec/fixtures/patches.json delete mode 100644 spec/fixtures/registration.json delete mode 100644 spec/fixtures/registrations.json create mode 100644 spec/models/registration_spec.rb delete mode 100644 spec/requests/cancel_registration_spec.rb delete mode 100644 spec/requests/competition_api_spec.rb delete mode 100644 spec/requests/get_registrations_spec.rb delete mode 100644 spec/requests/post_attendee_spec.rb delete mode 100644 spec/requests/update_registration_spec.rb create mode 100644 spec/services/registration_checker_spec.rb delete mode 100644 spec/support/registration_spec_helper.rb delete mode 100644 spec/todo/get_attendee_spec.rb delete mode 100644 spec/todo/patch_registration_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 66cac1a9..178db268 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -32,6 +32,9 @@ Naming/AccessorMethodName: Style/Alias: Enabled: false +Style/NumericLiterals: + Enabled: false + Style/EmptyMethod: EnforcedStyle: expanded diff --git a/.ruby-version b/.ruby-version index 9e79f6c4..be94e6f5 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-3.2.2 +3.2.2 diff --git a/app/controllers/registration_controller.rb b/app/controllers/registration_controller.rb index 4cd32af3..53ba1044 100644 --- a/app/controllers/registration_controller.rb +++ b/app/controllers/registration_controller.rb @@ -49,26 +49,10 @@ def create render json: { status: 'accepted', message: 'Started Registration Process' }, status: :accepted end - # For a user to register they need to - # 1) Need to actually be the user that they are trying to register - # 2) Be Eligible to Compete (complete profile + not banned) - # 3) Register for a competition that is open - # 4) Register for events that are actually held at the competition - # We need to do this in this order, so we don't leak user attributes def validate_create_request - @user_id = registration_params[:user_id] @competition_id = registration_params[:competition_id] - @event_ids = registration_params[:competing]['event_ids'] - @competition = CompetitionApi.find!(@competition_id) - - user_can_create_registration! - - can_compete = UserApi.can_compete?(@user_id) - raise RegistrationError.new(:unauthorized, ErrorCodes::USER_PROFILE_INCOMPLETE) unless can_compete - - validate_events! - raise RegistrationError.new(:unprocessable_entity, ErrorCodes::GUEST_LIMIT_EXCEEDED) if params.key?(:guests) && @competition.guest_limit_exceeded?(params[:guests]) + RegistrationChecker.create_registration_allowed!(registration_params, CompetitionApi.find!(@competition_id), @current_user) rescue RegistrationError => e render_error(e.http_status, e.error) end @@ -134,19 +118,7 @@ def validate_update_request @user_id = params[:user_id] @competition_id = params[:competition_id] - @competition = CompetitionApi.find!(@competition_id) - @registration = Registration.find("#{@competition_id}-#{@user_id}") - - raise RegistrationError.new(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) unless is_admin_or_current_user? - raise RegistrationError.new(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) if admin_fields_present? && !UserApi.can_administer?(@current_user, @competition_id) - raise RegistrationError.new(:unprocessable_entity, ErrorCodes::GUEST_LIMIT_EXCEEDED) if params.key?(:guests) && @competition.guest_limit_exceeded?(params[:guests]) - - if params.key?(:competing) - validate_status! if params['competing'].key?(:status) - validate_events! if params['competing'].key?(:event_ids) - raise RegistrationError.new(:unprocessable_entity, ErrorCodes::USER_COMMENT_TOO_LONG) if params['competing'].key?(:comment) && !comment_valid? - raise RegistrationError.new(:unprocessable_entity, ErrorCodes::REQUIRED_COMMENT_MISSING) if !params['competing'].key?(:comment) && @competition.force_comment? - end + RegistrationChecker.update_registration_allowed!(params, CompetitionApi.find!(@competition_id), @current_user) rescue Dynamoid::Errors::RecordNotFound render_error(:not_found, ErrorCodes::REGISTRATION_NOT_FOUND) rescue RegistrationError => e @@ -300,69 +272,4 @@ def get_single_registration(user_id, competition_id) }, } end - - def registration_exists?(user_id, competition_id) - Registration.find("#{competition_id}-#{user_id}") - true - rescue Dynamoid::Errors::RecordNotFound - false - end - - def comment_valid? - params['competing'][:comment].length <= 240 - end - - def validate_events! - event_ids = params['competing'][:event_ids] - if defined?(@registration) && params['competing'].key?(:status) && params['competing'][:status] == 'cancelled' - # If status is cancelled, events can only be empty or match the old events list - # This allows for edge cases where an API user might send an empty event list/the old event list, or admin might want to remove events - raise RegistrationError.new(:unprocessable_entity, ErrorCodes::INVALID_EVENT_SELECTION) unless event_ids == [] || event_ids == @registration.event_ids - else - # Event submitted must be held at the competition - raise RegistrationError.new(:unprocessable_entity, ErrorCodes::INVALID_EVENT_SELECTION) unless @competition.events_held?(event_ids) - end - - # Events can't be changed outside the edit_events deadline - # TODO: Should an admin be able to override this? - if @competition.event_change_deadline.present? - events_edit_deadline = Time.parse(@competition.event_change_deadline) - raise RegistrationError.new(:forbidden, ErrorCodes::EVENT_EDIT_DEADLINE_PASSED) if events_edit_deadline < Time.now - end - end - - def admin_fields_present? - # There could be different admin fields in different lanes - define the admin fields per lane and check each - competing_admin_fields = ['admin_comment'] - - params.key?('competing') && params['competing'].keys.any? { |key| competing_admin_fields.include?(key) } - end - - def user_can_create_registration! - # Only an admin or the user themselves can create a registration for the user - raise RegistrationError.new(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) unless is_admin_or_current_user? - - # Only admins can register when registration is closed, and they can only register for themselves - not for other users - raise RegistrationError.new(:forbidden, ErrorCodes::REGISTRATION_CLOSED) unless @competition.registration_open? || organizer_signing_up_themselves? - end - - def organizer_signing_up_themselves? - @competition.is_organizer_or_delegate?(@current_user) && (@current_user.to_s == @user_id.to_s) - end - - def is_admin_or_current_user? - # Only an admin or the user themselves can create a registration for the user - # One case where admins need to create registrations for users is if a 3rd-party registration system is being used, and registration data is being - # passed to the Registration Service from it - (@current_user.to_s == @user_id.to_s) || UserApi.can_administer?(@current_user, @competition_id) - end - - def validate_status! - raise RegistrationError.new(:unprocessable_entity, ErrorCodes::INVALID_REQUEST_DATA) unless Registration::REGISTRATION_STATES.include?(params['competing'][:status]) - - raise RegistrationError.new(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) if - Registration::ADMIN_ONLY_STATES.include?(params['competing'][:status]) && !UserApi.can_administer?(@current_user, @competition_id) - - raise RegistrationError.new(:forbidden, ErrorCodes::COMPETITOR_LIMIT_REACHED) if params['competing'][:status] == 'accepted' && Registration.count > @competition.competitor_limit - end end diff --git a/app/helpers/competition_api.rb b/app/helpers/competition_api.rb index 7b881149..ea17fee5 100644 --- a/app/helpers/competition_api.rb +++ b/app/helpers/competition_api.rb @@ -46,12 +46,15 @@ def fetch_competition(competition_id) end class CompetitionInfo + attr_accessor :competition_id + def initialize(competition_json) @competition_json = competition_json + @competition_id = competition_json['id'] end - def event_change_deadline - @competition_json['event_change_deadline_date'] + def within_event_change_deadline? + Time.now < @competition_json['event_change_deadline_date'] end def competitor_limit @@ -59,6 +62,7 @@ def competitor_limit end def guest_limit_exceeded?(guest_count) + return false unless @competition_json['guests_per_registration_limit'].present? @competition_json['guest_entry_status'] == 'restricted' && @competition_json['guests_per_registration_limit'] < guest_count end @@ -93,4 +97,12 @@ def is_organizer_or_delegate?(user_id) def name @competition_json['name'] end + + def registration_edits_allowed? + @competition_json['allow_registration_edits'] && within_event_change_deadline? + end + + def user_can_cancel? + @competition_json['allow_registration_self_delete_after_acceptance'] + end end diff --git a/app/helpers/error_codes.rb b/app/helpers/error_codes.rb index d9c76850..18b9ddf2 100644 --- a/app/helpers/error_codes.rb +++ b/app/helpers/error_codes.rb @@ -11,8 +11,7 @@ module ErrorCodes COMPETITION_API_5XX = -1001 # User Errors - USER_IS_BANNED = -2001 - USER_PROFILE_INCOMPLETE = -2002 + USER_CANNOT_COMPETE = -2001 USER_INSUFFICIENT_PERMISSIONS = -2003 # Registration errors @@ -20,7 +19,7 @@ module ErrorCodes # Request errors INVALID_REQUEST_DATA = -4000 - EVENT_EDIT_DEADLINE_PASSED = -4001 + USER_EDITS_NOT_ALLOWED = -4001 GUEST_LIMIT_EXCEEDED = -4002 USER_COMMENT_TOO_LONG = -4003 INVALID_EVENT_SELECTION = -4004 @@ -28,6 +27,7 @@ module ErrorCodes COMPETITOR_LIMIT_REACHED = -4006 INVALID_REGISTRATION_STATUS = -4007 REGISTRATION_CLOSED = -4008 + ORGANIZER_MUST_CANCEL_REGISTRATION = -4009 # Payment Errors PAYMENT_NOT_ENABLED = -3001 diff --git a/app/helpers/lane_factory.rb b/app/helpers/lane_factory.rb index fedc8ad7..6602b752 100644 --- a/app/helpers/lane_factory.rb +++ b/app/helpers/lane_factory.rb @@ -1,15 +1,14 @@ # frozen_string_literal: true require 'time' -# rubocop:disable Metrics/ParameterLists class LaneFactory - def self.competing_lane(event_ids = [], comment = '', guests = 0, admin_comment = '', registration_status = 'pending') + def self.competing_lane(event_ids: [], comment: '', guests: 0, admin_comment: '', registration_status: 'pending') competing_lane = Lane.new({}) competing_lane.lane_name = 'competing' competing_lane.completed_steps = ['Event Registration'] competing_lane.lane_state = registration_status competing_lane.lane_details = { - event_details: event_ids.map { |event_id| { event_id: event_id } }, + event_details: event_ids.map { |event_id| { event_id: event_id, event_registration_state: registration_status } }, comment: comment, admin_comment: admin_comment, guests: guests, @@ -32,4 +31,3 @@ def self.payment_lane(fee_lowest_denominator, currency_code, payment_id) payment_lane end end -# rubocop:enable Metrics/ParameterLists diff --git a/app/helpers/mocks.rb b/app/helpers/mocks.rb index 9aad3619..852ac78e 100644 --- a/app/helpers/mocks.rb +++ b/app/helpers/mocks.rb @@ -39,24 +39,10 @@ def self.permissions_mock(user_id) 'scope' => '*', }, } - when '209943' # Test banned User + when '209943', '999999' # Test banned/incomplete profile User { 'can_attend_competitions' => { 'scope' => [], - 'reasons' => ErrorCodes::USER_IS_BANNED, - }, - 'can_organize_competitions' => { - 'scope' => [], - }, - 'can_administer_competitions' => { - 'scope' => [], - }, - } - when '999999' # Test incomplete User - { - 'can_attend_competitions' => { - 'scope' => [], - 'reasons' => ErrorCodes::USER_PROFILE_INCOMPLETE, }, 'can_organize_competitions' => { 'scope' => [], diff --git a/app/models/registration.rb b/app/models/registration.rb index 22796fc4..effca50b 100644 --- a/app/models/registration.rb +++ b/app/models/registration.rb @@ -11,11 +11,28 @@ class Registration REGISTRATION_STATES = %w[pending waiting_list accepted cancelled].freeze ADMIN_ONLY_STATES = %w[pending waiting_list accepted].freeze # Only admins are allowed to change registration state to one of these states + # Pre-validations + before_validation :set_is_competing + + # Validations + validate :is_competing_consistency + + # NOTE: There are more efficient ways to do this, see: https://github.com/thewca/wca-registration/issues/330 + def self.accepted_competitors(competition_id) + where(competition_id: competition_id, is_competing: true).count + end + # Returns all event ids irrespective of registration status def event_ids lanes.filter_map { |x| x.lane_details['event_details'].pluck('event_id') if x.lane_name == 'competing' }[0] end + def attendee_id + attendee_id = "#{competition_id}-#{user_id}" + puts "returning attendee id: #{attendee_id}" + "#{competition_id}-#{user_id}" + end + # Returns id's of the events with a non-cancelled state def registered_event_ids event_ids = [] @@ -80,7 +97,7 @@ def update_competing_lane!(update_params) if update_params[:status].present? lane.lane_state = update_params[:status] - lane.lane_details['event_details'].each do |event| + lane.lane_details[:event_details].each do |event| # NOTE: Currently event_registration_state is not used - when per-event registrations are added, we need to add validation logic to support cases like # limited registrations and waiting lists for certain events event['event_registration_state'] = update_params[:status] @@ -97,12 +114,12 @@ def update_competing_lane!(update_params) lane end # TODO: In the future we will need to check if any of the other lanes have a status set to accepted - updated_is_attending = if update_params[:status].present? + updated_is_competing = if update_params[:status].present? update_params[:status] == 'accepted' else - is_attending + is_competing end - update_attributes!(lanes: updated_lanes, is_attending: updated_is_attending) # TODO: Apparently update_attributes is deprecated in favor of update! - should we change? + update_attributes!(lanes: updated_lanes, is_competing: updated_is_competing) # TODO: Apparently update_attributes is deprecated in favor of update! - should we change? end def init_payment_lane(amount, currency_code, id) @@ -137,10 +154,25 @@ def update_payment_lane(id, iso_amount, currency_iso, status) # Fields field :user_id, :string field :competition_id, :string - field :is_attending, :boolean + field :is_competing, :boolean field :hide_name_publicly, :boolean field :lanes, :array, of: Lane global_secondary_index hash_key: :user_id, projected_attributes: :all global_secondary_index hash_key: :competition_id, projected_attributes: :all + + private + + def set_is_competing + puts "executing set is competing for: #{attendee_id}" + self.is_competing = true if competing_status == 'accepted' + end + + def is_competing_consistency + if is_competing + errors.add(:is_competing, 'cant be true unless competing_status is accepted') unless competing_status == 'accepted' + else + errors.add(:is_competing, 'must be true if competing_status is accepted') if competing_status == 'accepted' + end + end end diff --git a/app/services/registration_checker.rb b/app/services/registration_checker.rb new file mode 100644 index 00000000..e5920fd4 --- /dev/null +++ b/app/services/registration_checker.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +COMMENT_CHARACTER_LIMIT = 240 + +class RegistrationChecker + def self.create_registration_allowed!(registration_request, competition_info, requesting_user) + @request = registration_request + @competition_info = competition_info + @requestee_user_id = @request['user_id'] + @requester_user_id = requesting_user.to_s + + user_can_create_registration! + validate_create_events! + validate_guests! + validate_comment! + end + + def self.update_registration_allowed!(update_request, competition_info, requesting_user) + @request = update_request + @competition_info = competition_info + @requestee_user_id = @request['user_id'] + @requester_user_id = requesting_user.to_s + @registration = Registration.find("#{update_request['competition_id']}-#{update_request['user_id']}") + + user_can_modify_registration! + validate_guests! + validate_comment! + validate_organizer_fields! + validate_organizer_comment! + validate_update_status! + validate_update_events! + end + + class << self + def user_can_create_registration! + # Only an organizer or the user themselves can create a registration for the user + raise RegistrationError.new(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) unless is_organizer_or_current_user? + + # Only organizers can register when registration is closed, and they can only register for themselves - not for other users + raise RegistrationError.new(:forbidden, ErrorCodes::REGISTRATION_CLOSED) unless @competition_info.registration_open? || organizer_modifying_own_registration? + + can_compete = UserApi.can_compete?(@request['user_id']) + raise RegistrationError.new(:unauthorized, ErrorCodes::USER_CANNOT_COMPETE) unless can_compete + end + + def user_can_modify_registration! + raise RegistrationError.new(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) unless is_organizer_or_current_user? + raise RegistrationError.new(:forbidden, ErrorCodes::USER_EDITS_NOT_ALLOWED) unless @competition_info.registration_edits_allowed? || @competition_info.is_organizer_or_delegate?(@requester_user_id) + end + + def organizer_modifying_own_registration? + @competition_info.is_organizer_or_delegate?(@requester_user_id) && (@requester_user_id == @request['user_id'].to_s) + end + + def is_organizer_or_current_user? + # Only an organizer or the user themselves can create a registration for the user + # One case where organizers need to create registrations for users is if a 3rd-party registration system is being used, and registration data is being + # passed to the Registration Service from it + (@requester_user_id == @request['user_id'].to_s) || @competition_info.is_organizer_or_delegate?(@requester_user_id) + end + + def validate_create_events! + event_ids = @request['competing']['event_ids'] + # Event submitted must be held at the competition + raise RegistrationError.new(:unprocessable_entity, ErrorCodes::INVALID_EVENT_SELECTION) unless @competition_info.events_held?(event_ids) + end + + def validate_guests! + return unless @request.key?('guests') + + raise RegistrationError.new(:unprocessable_entity, ErrorCodes::INVALID_REQUEST_DATA) if @request['guests'] < 0 + raise RegistrationError.new(:unprocessable_entity, ErrorCodes::GUEST_LIMIT_EXCEEDED) if @competition_info.guest_limit_exceeded?(@request['guests']) + end + + def validate_comment! + if @request.key?('competing') && @request['competing'].key?('comment') + comment = @request['competing']['comment'] + + raise RegistrationError.new(:unprocessable_entity, ErrorCodes::USER_COMMENT_TOO_LONG) if comment.length > COMMENT_CHARACTER_LIMIT + raise RegistrationError.new(:unprocessable_entity, ErrorCodes::REQUIRED_COMMENT_MISSING) if @competition_info.force_comment? && comment == '' + else + raise RegistrationError.new(:unprocessable_entity, ErrorCodes::REQUIRED_COMMENT_MISSING) if @competition_info.force_comment? + end + end + + def validate_organizer_fields! + @organizer_fields = ['organizer_comment'] + + raise RegistrationError.new(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) if contains_organizer_fields? && !@competition_info.is_organizer_or_delegate?(@requester_user_id) + end + + def validate_organizer_comment! + if @request.key?('competing') && @request['competing'].key?('organizer_comment') + organizer_comment = @request['competing']['organizer_comment'] + + raise RegistrationError.new(:unprocessable_entity, ErrorCodes::USER_COMMENT_TOO_LONG) if organizer_comment.length > COMMENT_CHARACTER_LIMIT + end + end + + def contains_organizer_fields? + @request.key?('competing') && @request['competing'].keys.any? { |key| @organizer_fields.include?(key) } + end + + def validate_update_status! + return unless @request.key?('competing') && @request['competing'].key?('status') + + current_status = @registration.competing_status + new_status = @request['competing']['status'] + + raise RegistrationError.new(:unprocessable_entity, ErrorCodes::INVALID_REQUEST_DATA) unless Registration::REGISTRATION_STATES.include?(new_status) + raise RegistrationError.new(:forbidden, ErrorCodes::COMPETITOR_LIMIT_REACHED) if + new_status == 'accepted' && Registration.accepted_competitors(@competition_info.competition_id) >= @competition_info.competitor_limit + + # Organizers can make any status change they want to - no checks performed + return if @competition_info.is_organizer_or_delegate?(@requester_user_id) + + # A user (ie not an organizer) is only allowed to: + # 1. Reactivate their registration if they previously cancelled it (ie, change status from 'cancelled' to 'pending') + # 2. Cancel their registration, assuming they are allowed to cancel + + # User reactivating registration + if new_status == 'pending' + raise RegistrationError.new(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) unless current_status == 'cancelled' + return # No further checks needed if status is pending + end + + # Now that we've checked the 'pending' case, raise an error is the status is not cancelled (cancelling is the only valid action remaining) + raise RegistrationError.new(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) if new_status != 'cancelled' + + # Raise an error if competition prevents users from cancelling a registration once it is accepted + raise RegistrationError.new(:unauthorized, ErrorCodes::ORGANIZER_MUST_CANCEL_REGISTRATION) if + !@competition_info.user_can_cancel? && current_status == 'accepted' + + # Users aren't allowed to change events when cancelling + raise RegistrationError.new(:unprocessable_entity, ErrorCodes::INVALID_REQUEST_DATA) if + @request['competing'].key?('event_ids') && @registration.event_ids != @request['competing']['event_ids'] + end + + def validate_update_events! + return unless @request.key?('competing') && @request['competing'].key?('event_ids') + raise RegistrationError.new(:unprocessable_entity, ErrorCodes::INVALID_EVENT_SELECTION) if !@competition_info.events_held?( + @request['competing']['event_ids'], + ) + end + end +end diff --git a/app/worker/registration_processor.rb b/app/worker/registration_processor.rb index 853b9b9e..faac65cd 100644 --- a/app/worker/registration_processor.rb +++ b/app/worker/registration_processor.rb @@ -44,7 +44,7 @@ def lane_init(competition_id, user_id) def event_registration(competition_id, user_id, event_ids, comment, guests) registration = Registration.find("#{competition_id}-#{user_id}") - competing_lane = LaneFactory.competing_lane(event_ids, comment, guests) + competing_lane = LaneFactory.competing_lane(event_ids: event_ids, comment: comment, guests: guests) if registration.lanes.nil? registration.update_attributes(lanes: [competing_lane]) else diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 5dda8dc9..887b7c0d 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -16,7 +16,7 @@ services: tty: true command: > bash -c 'bundle install && yarn install && bin/rake db:seed && - RAILS_ENV=test bundle exec rspec spec/domains -e PASSING' + RAILS_ENV=test bundle exec rspec' networks: - wca-registration depends_on: diff --git a/spec/db/database_functions_spec.rb b/spec/db/database_functions_spec.rb deleted file mode 100644 index 8a8eced7..00000000 --- a/spec/db/database_functions_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -require 'swagger_helper' -require_relative '../support/registration_spec_helper' - -RSpec.describe 'PASSING testing DynamoID writes', type: :request do - include Helpers::RegistrationHelper - - it 'creates a registration object from a given hash' do - basic_registration = get_registration('CubingZANationalChampionship2023-158816', false) - - registration = Registration.new(basic_registration) - registration.save - - expect(Registration.count).to eq(1) - end -end - -RSpec.describe 'PASSING testing DynamoID reads', type: :request do - include Helpers::RegistrationHelper - include_context 'database seed' - - it 'returns registration by attendee_id as defined in the schema' do - basic_registration = get_registration('CubingZANationalChampionship2023-158816', true) - registration_from_database = Registration.find('CubingZANationalChampionship2023-158816') - - expect(registration_equal(registration_from_database, basic_registration)).to eq(true) - end -end diff --git a/spec/domains/competition_api_spec.rb b/spec/domains/competition_api_spec.rb deleted file mode 100644 index ff821024..00000000 --- a/spec/domains/competition_api_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require_relative '../requests/competition_api_spec' -require_relative '../helpers/competition_api_spec' diff --git a/spec/domains/database_spec.rb b/spec/domains/database_spec.rb deleted file mode 100644 index ba816774..00000000 --- a/spec/domains/database_spec.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require_relative '../db/database_functions_spec' diff --git a/spec/domains/registrations_api_spec.rb b/spec/domains/registrations_api_spec.rb deleted file mode 100644 index 85c9755b..00000000 --- a/spec/domains/registrations_api_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require_relative '../requests/cancel_registration_spec' -require_relative '../requests/get_registrations_spec' -require_relative '../requests/post_attendee_spec' -require_relative '../requests/update_registration_spec' diff --git a/spec/factories.rb b/spec/factories.rb deleted file mode 100644 index a3551674..00000000 --- a/spec/factories.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'factory_bot_rails' - -# Couldn't get the import from a support folder to work, so defining directly in the factory file -def fetch_jwt_token(user_id) - iat = Time.now.to_i - jti_raw = [JwtOptions.secret, iat].join(':').to_s - jti = Digest::MD5.hexdigest(jti_raw) - payload = { user_id: user_id, exp: Time.now.to_i + JwtOptions.expiry, sub: user_id, iat: iat, jti: jti } - token = JWT.encode payload, JwtOptions.secret, JwtOptions.algorithm - "Bearer #{token}" -end - -# TODOS -# x1. Fetch the user_id variable to create JWT token -# 1. Create an admin option -# 2. Figre out how to change values with arguments -# 3 Create a separate competing lane and add that to the registration? - -FactoryBot.define do - factory :registration, class: Hash do - transient do - events { ['333', '333mbf'] } - end - - user_id { '158817' } - competition_id { 'CubingZANationalChampionship2023' } - competing { { event_ids: events, lane_state: 'pending' } } - jwt_token { fetch_jwt_token(user_id) } - - trait :admin do - user_id { '15073' } - jwt_token { fetch_jwt_token(user_id) } - end - - trait :admin_submits do - jwt_token { fetch_jwt_token('15073') } - end - - initialize_with { attributes } - - factory :admin, traits: [:admin] - factory :admin_submits, traits: [:admin_submits] - end -end diff --git a/spec/factories/competition_factory.rb b/spec/factories/competition_factory.rb index 6e4bd216..debbf120 100644 --- a/spec/factories/competition_factory.rb +++ b/spec/factories/competition_factory.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true require 'factory_bot_rails' +require_relative '../../app/helpers/competition_api' FactoryBot.define do - factory :competition_details, class: Hash do + factory :competition, class: Hash do events { ['333', '222', '444', '555', '666', '777', '333bf', '333oh', 'clock', 'minx', 'pyram', 'skewb', 'sq1', '444bf', '555bf', '333mbf'] } registration_opened? { true } - competition_id { 'CubingZANationalChampionship2023' } + id { 'CubingZANationalChampionship2023' } name { 'CubingZA National Championship 2023' } event_ids { events } registration_open { '2023-05-05T04:00:00.000Z' } @@ -29,7 +30,39 @@ guests_per_registration_limit { 2 } event_change_deadline_date { '2024-06-14T00:00:00.000Z' } using_stripe_payments? { true } + force_comment_in_registration { false } + allow_registration_self_delete_after_acceptance { true } + allow_registration_edits { true } + delegates { [{ 'id' => '1306' }] } + organizers { [] } - initialize_with { attributes.transform_keys(&:to_s) } + initialize_with { attributes.stringify_keys } + + transient do + mock_competition { false } + end + + trait :no_guest_limit do + guest_entry_status { 'free' } + guests_per_registration_limit { 'null' } + end + + trait :closed do + registration_opened? { false } + event_change_deadline_date { '2022-06-14T00:00:00.000Z' } + end + + trait :event_change_deadline_passed do + event_change_deadline_date { '2022-06-14T00:00:00.000Z' } + end + + trait :no_guests do + guest_entry_status { '' } + end + + # TODO: Create a flag that returns either the raw JSON (for mocking) or a CompetitionInfo object + after(:create) do |competition, evaluator| + stub_request(:get, comp_api_url(competition['competition_id'])).to_return(status: evalutor.mocked_status_code, body: competition) if evaluator.mock_competition + end end end diff --git a/spec/factories/registration_factory.rb b/spec/factories/registration_factory.rb new file mode 100644 index 00000000..e34a3e98 --- /dev/null +++ b/spec/factories/registration_factory.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'factory_bot_rails' + +FactoryBot.define do + factory :registration do + transient do + events { ['333', '333mbf'] } + comment { '' } + guests { 0 } + registration_status { 'incoming' } + organizer_comment { nil } + end + + user_id { rand(100000..200000).to_s } + competition_id { 'CubingZANationalChampionship2023' } + attendee_id { "#{competition_id}-#{user_id}" } + lanes { [LaneFactory.competing_lane(event_ids: events, comment: comment, guests: guests, registration_status: registration_status)] } + end + + trait :admin do + user_id { '15073' } + end + + trait :organizer do + user_id { '1' } + end + + factory :organizer_registration, traits: [:organizer] + factory :admin_registration, traits: [:admin] + + after(:create) do |registration, evaluator| + unless evaluator.organizer_comment.nil? + attendee_id = "#{evaluator.competition_id}-#{evaluator.user_id}" + registration = Registration.find(attendee_id) + registration.update_competing_lane!({ organizer_comment: evaluator.organizer_comment }) + end + end +end diff --git a/spec/factories/request_factory.rb b/spec/factories/request_factory.rb new file mode 100644 index 00000000..5092be59 --- /dev/null +++ b/spec/factories/request_factory.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'factory_bot_rails' + +# Couldn't get the import from a support folder to work, so defining directly in the factory file +def fetch_jwt_token(user_id) + iat = Time.now.to_i + jti_raw = [JwtOptions.secret, iat].join(':').to_s + jti = Digest::MD5.hexdigest(jti_raw) + payload = { user_id: user_id, exp: Time.now.to_i + JwtOptions.expiry, sub: user_id, iat: iat, jti: jti } + token = JWT.encode payload, JwtOptions.secret, JwtOptions.algorithm + "Bearer #{token}" +end + +FactoryBot.define do + factory :registration_request, class: Hash do + transient do + events { ['333', '333mbf'] } + raw_comment { nil } + end + + user_id { '158817' } + submitted_by { user_id } + competition_id { 'CubingZANationalChampionship2023' } + competing { { 'event_ids' => events, 'lane_state' => 'pending' } } + + jwt_token { fetch_jwt_token(submitted_by) } + guests { 0 } + + trait :comment do + competing { { 'event_ids' => events, 'comment' => raw_comment, 'lane_state' => 'pending' } } + end + + trait :organizer do + user_id { '1306' } + jwt_token { fetch_jwt_token(user_id) } + end + + trait :organizer_submits do + submitted_by { '1306' } + end + + trait :impersonation do + submitted_by { '158810' } + end + + trait :banned do + user_id { '209943' } + end + + trait :incomplete do + user_id { '999999' } + end + + initialize_with { attributes.stringify_keys } + end +end + +FactoryBot.define do + factory :update_request, class: Hash do + user_id { '158817' } + submitted_by { user_id } + jwt_token { fetch_jwt_token(submitted_by) } + competition_id { 'CubingZANationalChampionship2023' } + + transient do + competing { nil } + guests { nil } + end + + trait :organizer_as_user do + user_id { '1306' } + end + + trait :organizer_for_user do + submitted_by { '1306' } + end + + trait :for_another_user do + submitted_by { '158818' } + end + + # initialize_with { attributes } + initialize_with { attributes.stringify_keys } + + after(:build) do |instance, evaluator| + instance['guests'] = evaluator.guests if evaluator.guests + instance['competing'] = evaluator.competing if evaluator.competing + end + end +end diff --git a/spec/fixtures/competition_details.json b/spec/fixtures/competition_details.json deleted file mode 100644 index 8cc2bcb0..00000000 --- a/spec/fixtures/competition_details.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "competitions": [ - {"id":"CubingZANationalChampionship2023","registration_opened?": true,"name":"STUBBED CubingZA National Championship 2023","registration_open":"2023-05-05T04:00:00.000Z","registration_close":"2024-06-14T00:00:00.000Z","announced_at":"2023-05-01T15:59:53.000Z","start_date":"2023-06-16","end_date":"2023-06-18","competitor_limit":120,"cancelled_at":null,"url":"https://www.worldcubeassociation.org/competitions/CubingZANationalChampionship2023","website":"https://www.worldcubeassociation.org/competitions/CubingZANationalChampionship2023","short_name":"CubingZA Nationals 2023","city":"Johannesburg","venue_address":"South Africa, 28 Droste Cres, Droste Park, Johannesburg, 2094","venue_details":"","latitude_degrees":-26.21117,"longitude_degrees":28.06449,"country_iso2":"ZA","event_ids":["333","222","444","555","666","777","333bf","333oh","clock","minx","pyram","skewb","sq1","444bf","555bf","333mbf"],"delegates":[{"id":1306,"created_at":"2015-08-05T19:20:45.000Z","updated_at":"2023-05-30T06:04:43.000Z","name":"Maverick Pearson","delegate_status":"candidate_delegate","wca_id":"2014PEAR02","gender":"m","country_iso2":"ZA","url":"https://www.worldcubeassociation.org/persons/2014PEAR02","country":{"id":"South Africa","name":"South Africa","continentId":"_Africa","iso2":"ZA"},"email":"mpearson@worldcubeassociation.org","region":"South Africa","senior_delegate_id":125297,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2014PEAR02/1616059289.jpg","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2014PEAR02/1616059289_thumb.jpg","is_default":false}},{"id":64330,"created_at":"2017-07-06T17:59:59.000Z","updated_at":"2023-05-30T07:20:41.000Z","name":"Marike Faught","delegate_status":"trainee_delegate","wca_id":"2015FAUG01","gender":"f","country_iso2":"ZA","url":"https://www.worldcubeassociation.org/persons/2015FAUG01","country":{"id":"South Africa","name":"South Africa","continentId":"_Africa","iso2":"ZA"},"email":"marikefaught@gmail.com","region":"South Africa","senior_delegate_id":125297,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2015FAUG01/1670326982.jpeg","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2015FAUG01/1670326982_thumb.jpeg","is_default":false}},{"id":66046,"created_at":"2017-07-17T19:26:43.000Z","updated_at":"2023-05-27T09:12:35.000Z","name":"Timothy Lawrance","delegate_status":"candidate_delegate","wca_id":"2017LAWR04","gender":"m","country_iso2":"ZA","url":"https://www.worldcubeassociation.org/persons/2017LAWR04","country":{"id":"South Africa","name":"South Africa","continentId":"_Africa","iso2":"ZA"},"email":"tlawrance@worldcubeassociation.org","region":"South Africa","senior_delegate_id":125297,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2017LAWR04/1500401257.png","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2017LAWR04/1500401257_thumb.png","is_default":false}},{"id":156416,"created_at":"2019-05-08T06:41:17.000Z","updated_at":"2023-05-31T06:51:11.000Z","name":"Joshua Christian Marais","delegate_status":"trainee_delegate","wca_id":"2019MARA05","gender":"m","country_iso2":"ZA","url":"https://www.worldcubeassociation.org/persons/2019MARA05","country":{"id":"South Africa","name":"South Africa","continentId":"_Africa","iso2":"ZA"},"email":"joshuac.marais@gmail.com","region":"South Africa","senior_delegate_id":125297,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2019MARA05/1654594914.jpg","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2019MARA05/1654594914_thumb.jpg","is_default":false}},{"id":158816,"created_at":"2019-05-26T08:58:39.000Z","updated_at":"2023-05-22T09:15:27.000Z","name":"Duncan Hobbs","delegate_status":"candidate_delegate","wca_id":"2019HOBB02","gender":"m","country_iso2":"ZA","url":"https://www.worldcubeassociation.org/persons/2019HOBB02","country":{"id":"South Africa","name":"South Africa","continentId":"_Africa","iso2":"ZA"},"email":"dhobbs@worldcubeassociation.org","region":"South Africa","senior_delegate_id":125297,"class":"user","teams":[{"id":608,"friendly_id":"wst","leader":false,"name":"Duncan Hobbs","senior_member":true,"wca_id":"2019HOBB02","avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2019HOBB02/1652339003.jpeg","thumb":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2019HOBB02/1652339003_thumb.jpeg"}}}],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2019HOBB02/1652339003.jpeg","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2019HOBB02/1652339003_thumb.jpeg","is_default":false}}],"organizers":[{"id":78952,"created_at":"2017-10-26T16:37:48.000Z","updated_at":"2023-05-31T06:27:56.000Z","name":"Dylan Swarts","delegate_status":null,"wca_id":"2017SWAR03","gender":"m","country_iso2":"ZA","url":"https://www.worldcubeassociation.org/persons/2017SWAR03","country":{"id":"South Africa","name":"South Africa","continentId":"_Africa","iso2":"ZA"},"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2017SWAR03/1586299943.jpg","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2017SWAR03/1586299943_thumb.jpg","is_default":false}},{"id":226445,"created_at":"2021-03-01T14:53:01.000Z","updated_at":"2023-05-25T05:25:10.000Z","name":"Anthony Kalaya Rush","delegate_status":null,"wca_id":"2022RUSH01","gender":"m","country_iso2":"ZA","url":"https://www.worldcubeassociation.org/persons/2022RUSH01","country":{"id":"South Africa","name":"South Africa","continentId":"_Africa","iso2":"ZA"},"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2022RUSH01/1655038462.jpeg","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2022RUSH01/1655038462_thumb.jpeg","is_default":false}}],"class":"competition","guest_entry_status":"restricted","guests_per_registration_limit":2, "event_change_deadline_date":"2024-06-14T00:00:00.000Z"}, - {"id":"CubingZANationalChampionship2024","registration_opened?": true,"name":"STUBBED CubingZA National Championship 2024","registration_open":"2023-05-05T04:00:00.000Z","registration_close":"2024-06-14T00:00:00.000Z","announced_at":"2023-05-01T15:59:53.000Z","start_date":"2023-06-16","end_date":"2023-06-18","competitor_limit":3,"cancelled_at":null,"url":"https://www.worldcubeassociation.org/competitions/CubingZANationalChampionship2023","website":"https://www.worldcubeassociation.org/competitions/CubingZANationalChampionship2023","short_name":"CubingZA Nationals 2023","city":"Johannesburg","venue_address":"South Africa, 28 Droste Cres, Droste Park, Johannesburg, 2094","venue_details":"","latitude_degrees":-26.21117,"longitude_degrees":28.06449,"country_iso2":"ZA","event_ids":["333","222","444","555","666","777","333bf","333oh","clock","minx","pyram","skewb","sq1","444bf","555bf","333mbf"],"delegates":[{"id":1306,"created_at":"2015-08-05T19:20:45.000Z","updated_at":"2023-05-30T06:04:43.000Z","name":"Maverick Pearson","delegate_status":"candidate_delegate","wca_id":"2014PEAR02","gender":"m","country_iso2":"ZA","url":"https://www.worldcubeassociation.org/persons/2014PEAR02","country":{"id":"South Africa","name":"South Africa","continentId":"_Africa","iso2":"ZA"},"email":"mpearson@worldcubeassociation.org","region":"South Africa","senior_delegate_id":125297,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2014PEAR02/1616059289.jpg","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2014PEAR02/1616059289_thumb.jpg","is_default":false}},{"id":64330,"created_at":"2017-07-06T17:59:59.000Z","updated_at":"2023-05-30T07:20:41.000Z","name":"Marike Faught","delegate_status":"trainee_delegate","wca_id":"2015FAUG01","gender":"f","country_iso2":"ZA","url":"https://www.worldcubeassociation.org/persons/2015FAUG01","country":{"id":"South Africa","name":"South Africa","continentId":"_Africa","iso2":"ZA"},"email":"marikefaught@gmail.com","region":"South Africa","senior_delegate_id":125297,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2015FAUG01/1670326982.jpeg","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2015FAUG01/1670326982_thumb.jpeg","is_default":false}},{"id":66046,"created_at":"2017-07-17T19:26:43.000Z","updated_at":"2023-05-27T09:12:35.000Z","name":"Timothy Lawrance","delegate_status":"candidate_delegate","wca_id":"2017LAWR04","gender":"m","country_iso2":"ZA","url":"https://www.worldcubeassociation.org/persons/2017LAWR04","country":{"id":"South Africa","name":"South Africa","continentId":"_Africa","iso2":"ZA"},"email":"tlawrance@worldcubeassociation.org","region":"South Africa","senior_delegate_id":125297,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2017LAWR04/1500401257.png","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2017LAWR04/1500401257_thumb.png","is_default":false}},{"id":156416,"created_at":"2019-05-08T06:41:17.000Z","updated_at":"2023-05-31T06:51:11.000Z","name":"Joshua Christian Marais","delegate_status":"trainee_delegate","wca_id":"2019MARA05","gender":"m","country_iso2":"ZA","url":"https://www.worldcubeassociation.org/persons/2019MARA05","country":{"id":"South Africa","name":"South Africa","continentId":"_Africa","iso2":"ZA"},"email":"joshuac.marais@gmail.com","region":"South Africa","senior_delegate_id":125297,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2019MARA05/1654594914.jpg","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2019MARA05/1654594914_thumb.jpg","is_default":false}},{"id":158816,"created_at":"2019-05-26T08:58:39.000Z","updated_at":"2023-05-22T09:15:27.000Z","name":"Duncan Hobbs","delegate_status":"candidate_delegate","wca_id":"2019HOBB02","gender":"m","country_iso2":"ZA","url":"https://www.worldcubeassociation.org/persons/2019HOBB02","country":{"id":"South Africa","name":"South Africa","continentId":"_Africa","iso2":"ZA"},"email":"dhobbs@worldcubeassociation.org","region":"South Africa","senior_delegate_id":125297,"class":"user","teams":[{"id":608,"friendly_id":"wst","leader":false,"name":"Duncan Hobbs","senior_member":true,"wca_id":"2019HOBB02","avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2019HOBB02/1652339003.jpeg","thumb":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2019HOBB02/1652339003_thumb.jpeg"}}}],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2019HOBB02/1652339003.jpeg","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2019HOBB02/1652339003_thumb.jpeg","is_default":false}}],"organizers":[{"id":78952,"created_at":"2017-10-26T16:37:48.000Z","updated_at":"2023-05-31T06:27:56.000Z","name":"Dylan Swarts","delegate_status":null,"wca_id":"2017SWAR03","gender":"m","country_iso2":"ZA","url":"https://www.worldcubeassociation.org/persons/2017SWAR03","country":{"id":"South Africa","name":"South Africa","continentId":"_Africa","iso2":"ZA"},"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2017SWAR03/1586299943.jpg","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2017SWAR03/1586299943_thumb.jpg","is_default":false}},{"id":226445,"created_at":"2021-03-01T14:53:01.000Z","updated_at":"2023-05-25T05:25:10.000Z","name":"Anthony Kalaya Rush","delegate_status":null,"wca_id":"2022RUSH01","gender":"m","country_iso2":"ZA","url":"https://www.worldcubeassociation.org/persons/2022RUSH01","country":{"id":"South Africa","name":"South Africa","continentId":"_Africa","iso2":"ZA"},"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2022RUSH01/1655038462.jpeg","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2022RUSH01/1655038462_thumb.jpeg","is_default":false}}],"class":"competition","guest_entry_status":"restricted","guests_per_registration_limit":2, "event_change_deadline_date":"2024-06-14T00:00:00.000Z"}, - {"id":"1AVG2013","name":"STUBBED 1 AVG competition 2013","registration_open":"2013-03-31T00:00:00.000Z","registration_close":"2013-04-01T00:00:00.000Z","announced_at":"2013-03-18T12:08:00.000Z","start_date":"2013-04-01","end_date":"2013-04-01","competitor_limit":null,"cancelled_at":null,"url":"https://www.worldcubeassociation.org/competitions/1AVG2013","website":"http://waschbaerli.com/wca/1avg","short_name":"1 AVG 2013","city":"Delft","venue_address":"Baden Powellpad 2, Delft","venue_details":"","latitude_degrees":52.01074,"longitude_degrees":4.356539,"country_iso2":"NL","event_ids":["333","222","444","555","666","777","333bf","333oh","clock","minx","pyram","sq1"],"delegates":[{"id":1,"created_at":"2013-01-12T11:29:06.000Z","updated_at":"2023-06-05T12:11:42.000Z","name":"Ron van Bruchem","delegate_status":"delegate","wca_id":"2003BRUC01","gender":"m","country_iso2":"NL","url":"https://www.worldcubeassociation.org/persons/2003BRUC01","country":{"id":"Netherlands","name":"Netherlands","continentId":"_Europe","iso2":"NL"},"email":"rbruchem@worldcubeassociation.org","region":"Netherlands","senior_delegate_id":454,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2003BRUC01/1678722958.jpg","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2003BRUC01/1678722958_thumb.jpg","is_default":false}}],"organizers":[{"id":57606,"created_at":"2017-05-09T19:46:42.000Z","updated_at":"2018-10-11T12:31:24.000Z","name":"Arnaud van Galen","delegate_status":null,"wca_id":"2006GALE01","gender":"m","country_iso2":"NL","url":"https://www.worldcubeassociation.org/persons/2006GALE01","country":{"id":"Netherlands","name":"Netherlands","continentId":"_Europe","iso2":"NL"},"class":"user","teams":[],"avatar":{"url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","pending_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","thumb_url":"https://www.worldcubeassociation.org/assets/missing_avatar_thumb-d77f478a307a91a9d4a083ad197012a391d5410f6dd26cb0b0e3118a5de71438.png","is_default":true}}],"class":"competition"}, - {"id":"BrizZonSylwesterOpen2023","name":"STUBBED BrizZon Sylwester Open 2023","information":"","venue":"[Klub BrizZon](http://snooker.brizzon.com/)","contact":"[Zespół organizacyjny](mailto:brizzonopen@googlegroups.com)","registration_open":"2023-11-28T18:55:00.000Z","registration_close":"2023-12-22T19:00:00.000Z","use_wca_registration":true,"announced_at":"2022-10-24T11:47:26.000Z","base_entry_fee_lowest_denomination":4000,"currency_code":"PLN","start_date":"2023-12-30","end_date":"2023-12-31","enable_donations":true,"competitor_limit":35,"extra_registration_requirements":"Aby pojawić się na liście zawodników / rezerwowej musisz wypełnić formularz rejestracyjny i opłacić wpisowe.\r\n\r\n---\r\n\r\nIn order to appear on competitor / waiting list you need to submit registration form and pay your registration fee.","on_the_spot_registration":false,"on_the_spot_entry_fee_lowest_denomination":null,"refund_policy_percent":95,"refund_policy_limit_date":"2023-12-22T19:00:00.000Z","guests_entry_fee_lowest_denomination":0,"external_registration_page":"","cancelled_at":null,"waiting_list_deadline_date":"2023-12-22T19:00:00.000Z","event_change_deadline_date":"2023-12-22T19:00:00.000Z","guest_entry_status":"restricted","allow_registration_edits":true,"allow_registration_self_delete_after_acceptance":true,"allow_registration_without_qualification":false,"guests_per_registration_limit":null,"force_comment_in_registration":null,"url":"http://localhost:3000/competitions/BrizZonSylwesterOpen2023","website":"http://localhost:3000/competitions/BrizZonSylwesterOpen2023","short_name":"BrizZon Sylwester Open 2023","city":"Poznań","venue_address":"ul. Karpia 10, 61-619 Poznań","venue_details":"Billiard club","latitude_degrees":52.444748,"longitude_degrees":16.948881,"country_iso2":"PL","event_ids":["444","333bf","clock","minx","sq1"],"registration_opened?":false,"main_event_id":"333bf","number_of_bookmarks":14,"using_stripe_payments?":null,"delegates":[{"id":15073,"created_at":"2015-08-21T11:08:00.000Z","updated_at":"2023-07-10T12:10:56.000Z","name":"Przemysław Rogalski","delegate_status":"delegate","wca_id":"2013ROGA02","gender":"m","country_iso2":"PL","url":"","country":{"id":"Poland","name":"Poland","continentId":"_Europe","iso2":"PL"},"email":"1686@worldcubeassociation.org","region":"Poland (Central)","senior_delegate_id":454,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2013ROGA02/1556616648.jpg","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2013ROGA02/1556616648_thumb.jpg","is_default":false}},{"id":81510,"created_at":"2017-11-15T09:26:58.000Z","updated_at":"2023-07-09T12:43:46.000Z","name":"Krzysztof Bober","delegate_status":"candidate_delegate","wca_id":"2013BOBE01","gender":"m","country_iso2":"PL","url":"","country":{"id":"Poland","name":"Poland","continentId":"_Europe","iso2":"PL"},"email":"81510@worldcubeassociation.org","region":"Poland","senior_delegate_id":454,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2013BOBE01/1636925953.jpg","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2013BOBE01/1636925953_thumb.jpg","is_default":false}}],"organizers":[{"id":1589,"created_at":"2015-08-20T22:08:41.000Z","updated_at":"2023-07-04T18:03:14.000Z","name":"Michał Bogdan","delegate_status":null,"wca_id":"2012BOGD01","gender":"m","country_iso2":"PL","url":"","country":{"id":"Poland","name":"Poland","continentId":"_Europe","iso2":"PL"},"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2012BOGD01/1633865887.jpg","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2012BOGD01/1633865887_thumb.jpg","is_default":false}},{"id":1686,"created_at":"2015-08-21T11:08:00.000Z","updated_at":"2023-07-10T12:10:56.000Z","name":"Przemysław Rogalski","delegate_status":"delegate","wca_id":"2013ROGA02","gender":"m","country_iso2":"PL","url":"","country":{"id":"Poland","name":"Poland","continentId":"_Europe","iso2":"PL"},"email":"1686@worldcubeassociation.org","region":"Poland (Central)","senior_delegate_id":454,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2013ROGA02/1556616648.jpg","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2013ROGA02/1556616648_thumb.jpg","is_default":false}},{"id":46707,"created_at":"2017-02-18T17:25:40.000Z","updated_at":"2023-07-04T17:56:41.000Z","name":"Kamil Przybylski","delegate_status":null,"wca_id":"2016PRZY01","gender":"m","country_iso2":"PL","url":"","country":{"id":"Poland","name":"Poland","continentId":"_Europe","iso2":"PL"},"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2016PRZY01/1633806320.jpg","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2016PRZY01/1633806320_thumb.jpg","is_default":false}},{"id":81510,"created_at":"2017-11-15T09:26:58.000Z","updated_at":"2023-07-09T12:43:46.000Z","name":"Krzysztof Bober","delegate_status":"candidate_delegate","wca_id":"2013BOBE01","gender":"m","country_iso2":"PL","url":"","country":{"id":"Poland","name":"Poland","continentId":"_Europe","iso2":"PL"},"email":"81510@worldcubeassociation.org","region":"Poland","senior_delegate_id":454,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2013BOBE01/1636925953.jpg","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2013BOBE01/1636925953_thumb.jpg","is_default":false}},{"id":105543,"created_at":"2018-04-15T21:23:20.000Z","updated_at":"2023-06-28T14:22:50.000Z","name":"Mateusz Szwugier","delegate_status":null,"wca_id":"2014SZWU01","gender":"m","country_iso2":"PL","url":"","country":{"id":"Poland","name":"Poland","continentId":"_Europe","iso2":"PL"},"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2014SZWU01/1523827783.jpg","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2014SZWU01/1523827783_thumb.jpg","is_default":false}}],"tabs":[],"class":"competition"}, - {"id":"LazarilloOpen2023","name":"STUBBED Lazarillo Open 2023","information":"![](https://www.worldcubeassociation.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBbGs0IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--2fc77e484245394e186eab0decbebe7bd1d84f6a/LazarilloOpen_Logo.png)\r\n\r\nLos socios de la AES recibirán un reembolso del 15% del coste de la inscripción después del final de la competición.\r\n\r\nAES members will receive a 15% refund of their registration fee after the end of the competition.","venue":"Palacio de Congresos de Salamanca","contact":"[Organización](mailto:lazarilloopen@gmail.com)","registration_open":"2023-05-02T13:54:00.000Z","registration_close":"2023-07-17T13:54:00.000Z","use_wca_registration":true,"announced_at":"2023-04-11T00:40:07.000Z","base_entry_fee_lowest_denomination":1500,"currency_code":"EUR","start_date":"2023-07-29","end_date":"2023-07-30","enable_donations":false,"competitor_limit":80,"extra_registration_requirements":"","on_the_spot_registration":false,"on_the_spot_entry_fee_lowest_denomination":null,"refund_policy_percent":0,"refund_policy_limit_date":"2023-07-17T13:54:00.000Z","guests_entry_fee_lowest_denomination":0,"external_registration_page":"","cancelled_at":null,"waiting_list_deadline_date":"2023-07-24T00:00:00.000Z","event_change_deadline_date":"2023-07-24T00:00:00.000Z","guest_entry_status":"free","allow_registration_edits":true,"allow_registration_self_delete_after_acceptance":true,"allow_registration_without_qualification":false,"guests_per_registration_limit":null,"force_comment_in_registration":false,"url":"http://localhost:3000/competitions/LazarilloOpen2023","website":"http://localhost:3000/competitions/LazarilloOpen2023","short_name":"Lazarillo Open 2023","city":"Salamanca","venue_address":"Cuesta de Oviedo s/n, 37008 Salamanca","venue_details":"Sala de Ensayos","latitude_degrees":40.962812,"longitude_degrees":-5.669562,"country_iso2":"ES","event_ids":["333","222","444","333bf","minx","pyram","skewb","sq1","444bf"],"registration_opened?":false,"main_event_id":"333bf","number_of_bookmarks":26,"using_stripe_payments?":null,"delegates":[{"id":6113,"created_at":"2015-10-19T17:54:38.000Z","updated_at":"2023-07-08T11:11:06.000Z","name":"Josete Sánchez","delegate_status":"candidate_delegate","wca_id":"2015SANC18","gender":"m","country_iso2":"ES","url":"","country":{"id":"Spain","name":"Spain","continentId":"_Europe","iso2":"ES"},"email":"6113@worldcubeassociation.org","region":"Spain","senior_delegate_id":454,"class":"user","teams":[{"id":588,"friendly_id":"wdc","leader":false,"name":"Josete Sánchez","senior_member":false,"wca_id":"2015SANC18","avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2015SANC18/1665601824.jpeg","thumb":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2015SANC18/1665601824_thumb.jpeg"}}}],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2015SANC18/1665601824.jpeg","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2015SANC18/1665601824_thumb.jpeg","is_default":false}},{"id":47863,"created_at":"2017-02-27T10:24:09.000Z","updated_at":"2023-07-10T12:59:43.000Z","name":"Alejandro Nicolay","delegate_status":"trainee_delegate","wca_id":"2017NICO01","gender":"m","country_iso2":"ES","url":"","country":{"id":"Spain","name":"Spain","continentId":"_Europe","iso2":"ES"},"email":"47863@worldcubeassociation.org","region":"Spain","senior_delegate_id":454,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2017NICO01/1663356326.jpeg","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2017NICO01/1663356326_thumb.jpeg","is_default":false}}],"organizers":[{"id":24320,"created_at":"2016-07-22T08:17:29.000Z","updated_at":"2023-07-10T06:53:49.000Z","name":"Asociación Española de Speedcubing","delegate_status":null,"wca_id":null,"gender":"o","country_iso2":"ES","url":"","country":{"id":"Spain","name":"Spain","continentId":"_Europe","iso2":"ES"},"class":"user","teams":[],"avatar":{"url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","is_default":true}},{"id":32834,"created_at":"2016-10-23T15:48:25.000Z","updated_at":"2023-07-10T15:05:05.000Z","name":"Agus Wals","delegate_status":null,"wca_id":"2016WALS01","gender":"m","country_iso2":"ES","url":"","country":{"id":"Spain","name":"Spain","continentId":"_Europe","iso2":"ES"},"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2016WALS01/1681228666.jpg","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2016WALS01/1681228666_thumb.jpg","is_default":false}},{"id":112652,"created_at":"2018-06-13T15:31:52.000Z","updated_at":"2023-07-10T08:09:39.000Z","name":"María Ángeles García Franco","delegate_status":null,"wca_id":"2018FRAN17","gender":"f","country_iso2":"ES","url":"","country":{"id":"Spain","name":"Spain","continentId":"_Europe","iso2":"ES"},"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2018FRAN17/1660935969.JPG","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2018FRAN17/1660935969_thumb.JPG","is_default":false}}],"tabs":[{"id":28146,"competition_id":"LazarilloOpen2023","name":"Cómo llegar / How to arrive","content":"# ![Bandera esp](https://i.imgur.com/fMh9pht.png) **Español**\r\n**En coche**\r\nSalamanca es una ciudad muy bien comunicada en cuanto a carreteras. \r\n* *Si llegas desde el norte*\r\nCoger en Valladolid la autovía A-62, que conecta las dos ciudades.\r\n* *Si llegas desde el noroeste*\r\nCoger la Autovía de la Plata (A-66) en Zamora.\r\n* *Si llegas de Madrid*\r\nTomar la Autovía del Noroeste (A-6 y luego AP-6) hacia Villacastín, y ahí cambiar a la AP-51. En Ávila, tomar la A-50.\r\n* *Si llegas del sur*\r\nLo mejor es coger la A-66, que pasa por Cáceres, Béjar y llega a Salamanca.\r\n\r\n**En autobús**\r\nSalamanca está comunicada por autobús con la mayor parte de las capitales de provincia de la Península, además de numerosos pueblos y ciudades.\r\nDesde Madrid salen autobuses con destino a Salamanca cada hora, y en las franjas horarias más demandadas, cada media hora.\r\nAdemás, existe un servicio de autobús directo desde las terminales 1 y 4 del Aeropuerto Adolfo Suárez Madrid-Barajas.\r\nAlgunas de las rutas que paran en Salamanca son:\r\n\r\n* ALSA (http://www.alsa.es): Badajoz - Bilbao / A Coruña - Algeciras.\r\n* Avanzabús (http://www.avanzabus.com): Madrid - Salamanca / Ávila - Salamanca / Barajas - Salamanca / Valladolid - Salamanca / Segovia - Salamanca.\r\n* Zamora Salamanca, S.A (http://www.zamorasalamanca.es/).: Zamora - Salamanca.\r\n\r\n**En tren**\r\nSalamanca cuenta con su propia estación de tren. A ella se puede llegar en tren directo desde ciudades españolas como Madrid, Valladolid o Vitoria. \r\nLa estación de tren está a unos 20 minutos del centro de la ciudad, o incluso menos yendo en bus o taxi.\r\n\r\n**En avión**\r\nLos aeropuertos más cercanos para llegar a Salamanca con el de Villanubla (Valladolid) y el de Barajas (Madrid). Desde estas ciudades se puede llegar a Salamanca en tren o autobús.\r\n\r\n----------------------------\r\n\r\n# ![Bandera UK](https://i.imgur.com/t2O5Zsj.png) **English**\r\n\r\n**By car**\r\nSalamanca is a city with very good road connections. \r\n* *If you are arriving from the north*\r\nTake the A-62 in Valladolid, which connects the two cities.\r\n* *If you are coming from the northwest*.\r\nTake the A-66 in Zamora.\r\n* *If you are arriving from Madrid*\r\nTake the A-6 and then AP-6 towards Villacastín, and there change to the AP-51. In Ávila, take the A-50.\r\n* *If you are coming from the south*\r\nIt is best to take the A-66, which passes through Cáceres, Béjar and reaches Salamanca.\r\n\r\n\r\n**By bus**\r\nSalamanca is connected by bus with most of the cities in Spain. Buses depart from Madrid to Salamanca every hour, and in some occasions, every half hour.\r\nThere is also a direct bus service from T1 and T4 of Adolfo Suárez Madrid-Barajas Airport.\r\nSome of the bus routes that stop in Salamanca are:\r\n\r\n* ALSA (http://www.alsa.es): Badajoz - Bilbao / A Coruña - Algeciras.\r\n* Avanzabús (http://www.avanzabus.com): Madrid - Salamanca / Ávila - Salamanca / Barajas - Salamanca / Valladolid - Salamanca / Segovia - Salamanca.\r\n* Zamora Salamanca, S.A (http://www.zamorasalamanca.es/).: Zamora - Salamanca.\r\n\r\n**By train**\r\nSalamanca has its own train station. There are direct train conection from Madrid, Valladolid or Vitoria.\r\nThe train station is about 20 minutes from the city centre, or less if you take a bus or a taxi.\r\n\r\n**By plane**\r\nThe closest airports to Salamanca are Villanubla (Valladolid) and Barajas (Madrid). From these cities you can travel to Salamanca by train or bus.","display_order":1},{"id":28751,"competition_id":"LazarilloOpen2023","name":"Alojamiento / Accommodation","content":"# ![Bandera esp](https://i.imgur.com/fMh9pht.png) **Español**\r\n**ALOJAMIENTO RECOMENDADO**\r\n\r\nSi vienes de fuera de Salamanca y necesitas alojarte en la ciudad durante los días de la competición, la **Residencia Méndez** colabora con el Lazarillo Open, ofreciendo precios especiales para ese fin de semana. Se trata de una residencia universitaria que está situada a pocos minutos del Palacio de Congresos. Al celebrar el evento en una época donde apenas hay estudiantes, ponen a disposición del Lazarillo Open casi la totalidad de sus 50 habitaciones individuales y dobles.\r\n**[Más información y reservas](https://www.residenciamendez.com/)**.\r\n\r\nTambién puedes alojarte en la Residencia de Estudiantes Hernán Cortés. Al estar fuera del calendario lectivo universitario, esta residencia ofrece la opción de contratar alojamiento por días.\r\nAdemás, desde la organización hemos conseguido un **descuento del 15% para los competidores**.\r\nPara conseguir este descuento, tendréis que reservar a través de **[este enlace](https://direct-book.com/resa/properties/hernancortesdirect?locale=es\u0026items%5B0%5D%5Badults%5D=1\u0026items%5B0%5D%5Bchildren%5D=0\u0026items%5B0%5D%5Binfants%5D=0\u0026currency=EUR\u0026checkInDate=2023-07-28\u0026checkOutDate=2023-07-30\u0026trackPage=yes\u0026promocode=LAZARILLO2023)**.\r\nAntes de confirmar la reserva, aseguraos de que el código **LAZARILLO2023** está aplicado.\r\nEste alojamiento se encuentra a tan solo 10 minutos andando del lugar de la competición. \r\n\r\n---------------------------\r\n# ![Bandera UK](https://i.imgur.com/t2O5Zsj.png) **English**\r\n**RECOMMENDED ACCOMMODATION**\r\n\r\nIf you come from outside Salamanca, and you need to stay in the city during the competition weekend, **Residencia Méndez** collaborates with Lazarillo Open. They offer special prices for housing that weekend. Residencia Méndez is a student housing, located a few minutes away from Palacio de Congresos. Since we celebrate this open in summer, when there is no students in Salamanca, they offer to the competitors almost all their 50 single and double rooms.\r\n**[More information and reservations](https://www.residenciamendez.com/)**\r\n\r\nYou can also rest in Residencia de Estudiantes Hernán Cortés. Due to the University School calendar, this housing brings the opportunity to pay accommodation for days.\r\nMoreover, the organization of the competition has a **15% discount for competitors and their companions**.\r\nTo benefit from this discount, you have to booking at **[this link](https://direct-book.com/resa/properties/hernancortesdirect?locale=es\u0026items%5B0%5D%5Badults%5D=1\u0026items%5B0%5D%5Bchildren%5D=0\u0026items%5B0%5D%5Binfants%5D=0\u0026currency=EUR\u0026checkInDate=2023-07-28\u0026checkOutDate=2023-07-30\u0026trackPage=yes\u0026promocode=LAZARILLO2023)**.\r\nBefore making the payment, please confirm that you are using the promotional code **LAZARILLO2023**\r\nThis housing is only 10 minutes away from the competition venue.","display_order":2},{"id":28819,"competition_id":"LazarilloOpen2023","name":"Comida / Lunch","content":"# ![Bandera esp](https://i.imgur.com/fMh9pht.png) **Español**\r\nEl Palacio de Congresos está a tan solo unos minutos de la Rúa Mayor, una de las zonas con más restaurantes de Salamanca. Estos restaurantes ofrecen menú del día o tapas a muy buen precio.\r\nPara ir a sitios de comida rápida, hay que llegar hasta los alrededores de la Plaza Mayor, donde hay cadenas de pizzerías, hamburgueserías y bocadillos. También hay algunos sitios que ofrecen este tipo de comida sin ser cadenas de comida, como los bares de la calle Obispo Jarrín o la plaza de San Julián. Estos dos lugares se encuentran a unos 10 minutos del Palacio de Congresos.\r\n\r\n---------------------\r\n# ![Bandera UK](https://i.imgur.com/t2O5Zsj.png) **English**\r\nPalacio de Congresos is a few minutes away from Rúa Mayor, one of the streets with more restaurants in Salamanca.These kind of restaurants offer set menus and tapas at very good prices.\r\nIf you want to have lunch in fast food restaurant, most of them are around Plaza Mayor, where you can find pizzas, burgers or sandwiches. You can find other restaurants which offer this kind of food, and they are not franchises, such as Obispo Jarrín street and San Julián square bars. This two places are about ten minutes away from Palacio de Congresos.","display_order":3},{"id":28977,"competition_id":"LazarilloOpen2023","name":"GAN Cube","content":"# ![Bandera esp](https://i.imgur.com/fMh9pht.png) **Español**\r\n[GAN Cube](https://gancube.com/), marca líder en la fabricación de cubos y otros accesorios para speedcubing, ha elegido el **Lazarillo Open 2023** como una de las competiciones de la WCA a las que patrocinar este año.\r\nEsta marca tiene un gran compromiso con la comunidad cubera y una gran pasión por el speedcubing.\r\nPor todo ello, hemos querido que tenga una gran presencia en el Lazarillo Open 2023.\r\n¿Quieres ver lo que estamos preparando de la mano de GAN para el Lazarillo Open?\r\nPara saber más sobre GAN Cube puedes visitar los siguientes enlaces:\r\n* [Web Oficial de GAN Cube](https://www.gancube.com/es/)\r\n* [Facebook de GAN Cube](https://www.facebook.com/Gancube/)\r\n* [Instagram de GAN Cube](https://www.instagram.com/gancube/)\r\n* [Twitter de GAN Cube](https://twitter.com/gancube)\r\n# ![Bandera UK](https://i.imgur.com/t2O5Zsj.png) **English**\r\n[GAN Cube](https://gancube.com/), leading brand in the manufacture of cubes and other accessories for speedcubing, has chosen the **Lazarillo Open 2023** as one of the WCA competitions to sponsor this year.\r\nThis brand has a great commitment to the cubing community and a great passion for speedcubing.\r\nFor all these reasons, we wanted it to have a big presence at the Lazarillo Open 2023.\r\nDo you want to see what we are preparing with GAN for the Lazarillo Open?\r\nIf you want to kno more about GAN Cube, you can visit these links:\r\n* [GAN Cube Official Website](https://www.gancube.com/es/)\r\n* [GAN Cube Facebook](https://www.facebook.com/Gancube/)\r\n* [GAN Cube Instagram](https://www.instagram.com/gancube/)\r\n* [GAN Cube Twitter](https://twitter.com/gancube)\r\n\r\n\r\n![](https://www.worldcubeassociation.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBak02IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--3d245a4d8d9a304bb2c5230a3821649d6923e7eb/GAN.jpg)","display_order":4},{"id":28015,"competition_id":"LazarilloOpen2023","name":"Asociación Española de Speedcubing (AES)","content":"###¿Qué es?\r\nLa [Asociación Española de Speedcubing](https://www.speedcubingspain.org/) (AES) es una asociación sin ánimo de lucro destinada a organizar, colaborar y difundir los eventos y actividades relacionadas con el mundo del speedcubing en España. Estas actividades se centran en:\r\n\r\n* Colaboración en competiciones oficiales.\r\n* Realización anual del Campeonato de España.\r\n* Organización del Campeonato Interescolar. \r\n* Creación de eventos online.\r\n* Conectar a las personas, manteniendo una comunidad sana.\r\n\r\n###Servicios\r\n* **Material:** Aportamos cronómetros, tapacubos, y todo el material necesario para organizar un evento de speedcubing de calidad.\r\n* **Difusión:** Promovemos y damos difusión a los eventos y al speedcubing en general.\r\n* **Respaldo:** Ayudamos a los organizadores y a la comunidad cubera mediante el respaldo de una entidad jurídica.\r\n\r\n###¡Colabora!\r\nComo organización sin ánimo de lucro que funciona con voluntarios al 100%, agradecemos vuestras aportaciones para ayudar a que el mundo del Speedcubing en España crezca. Puedes colaborar realizando una [donación](https://www.paypal.com/paypalme/AESpeedcubing?locale.x=es_ES), o bien haciéndote socio desde tan solo 1,25€ al mes pinchando [aquí](https://speedcubingspain.org/register/), con lo que obtendrás las siguientes ventajas:\r\n\r\n* Al menos el 15% de descuento en todos los eventos que organice o colabore la AES.\r\n* Sorteos y premios exclusivos para socios.\r\n* Aviso vía e-mail de los nuevos eventos de speedcubing de España.\r\n* Participar y tener derecho a voto en las Asambleas Generales.\r\n* Entrada en el grupo de Telegram exclusivo para los socios.\r\n \r\n###¡Síguenos en nuestras redes sociales!\r\n\r\n* Instagram: [@aespeedcubing](https://www.instagram.com/aespeedcubing/?hl=es)\r\n* Facebook: [Asociación Española de Speedcubing (@AESpeedcubing)](https://www.facebook.com/search/top?q=asociaci%C3%B3n%20espa%C3%B1ola%20de%20speedcubing)\r\n* Twitter: [@AESpeedcubing](https://twitter.com/AESpeedcubing)\r\n* Twitch: [AESpeedcubing](https://www.twitch.tv/aespeedcubing)\r\n* YouTube: [Asociación Española de Speedcubing](https://www.youtube.com/channel/UCryvWN5_nrvi9af0EPxEY5g)\r\n\r\n[![](https://www.worldcubeassociation.org/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaHNTIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--b77ebd2ea85a467d70927603c8ac93439c5d47f8/aes_logo_mini.png \"Asociación Española de Speedcubing (AES)\")](https://www.speedcubingspain.org/)","display_order":5},{"id":28508,"competition_id":"LazarilloOpen2023","name":"PMF / FAQ","content":"# ![Bandera esp](https://i.imgur.com/fMh9pht.png) **Español**\r\n**P: ¿Cómo me registro?\r\nR:** En primer lugar tienes que asegurarte de que tengas creada una cuenta de la WCA. Si no es así, se puede hacer de manera muy sencilla haciendo click en \"registrarse\" y creándote una. Una vez hecho esto, vete a la pestaña Registro y sigue los pasos correspondientes.\r\n\r\n**P: ¿Por qué no aparezco en la lista de competidores?\r\nR:** Por favor, asegúrate de haber seguido todas las instruciones de la pestaña Registro. También es posible que estés en la lista de espera. El limite de competidores puedes verlo en la página Información General y la lista de competidores en Competidores. Si crees haber hecho todo correctamente y aun así no apareces, ten paciencia. Los registros se aprueban de manera manual y los Delegados y organizadores no están disponibles en todo momento. Si tu registro no se ha aceptado pasados dos días, mándarnos un correo.\r\n\r\n**P: ¿Puedo cambiar los eventos a los que me presento?\r\nR:** Sí, mientras el plazo de inscripción siga abierto. Contáctanos a través del correo de la organización y dinos qué categorías quieres cambiar. Si el registro ya se ha cerrado, por favor, no mandes ninguna petición de cambio de eventos. Si el dia de la competición decides no participar en alguna categoría simplemente ve a la mesa de mezclas cuando te toque competir y dile a uno de los Delegados que no te vas a presentar.\r\n\r\n**P: Ya no puedo asistir, ¿qué debo hacer?\r\nR:** Lo primero que tienes que hacer es informarnos vía email tan pronto como lo sepas para que otro competidor pueda ocupar tu lugar. No podemos ofrecer reembolsos ya que pagar la tarifa de registro es un contrato de compromiso.\r\n\r\n**P: ¿Cómo de rápido tengo que ser para poder competir?\r\nR:** Te recomendamos que mires la tabla del Horario para informarte de los tiempos límite de cada prueba. La mayoría de gente va a las competiciones para conocer a gente con la que comparten esta afición sin importar los tiempos o sencillamente a tratar de mejorar sus propios tiempos personales, ¡todo el mundo es apto para competir y pasarlo bien!\r\n\r\n**P: ¿Hay categorías para diferentes edades?\r\nR:** Todos los competidores participan en las mismas condiciones y participantes de todas las edades son bienvenidos. La mayoría de gente suele tener unos 15-20 años, pero también hay gente más joven y más mayor.\r\n\r\n**P: ¿Tengo que usar mis propios cubos para competir?\r\nR:** ¡Sí! Asegúrate de traer puzzles para todos los eventos en los que compitas y no los pierdas de vista.\r\n\r\n**P: ¿Puedo ir simplemente como espectador?\r\nR:** Sí, además la entrada es completamente gratuita para los espectadores. Echa un vistazo al horario del campeonato para que puedas venir a ver los eventos que más te interesen o enterarte de cuando son las finales.\r\n\r\n**P: ¿Cúando debo estar en el campeonato?\r\nR:** Recomendamos que te inscribas dentro de los plazos de inscripción que hay asignados en el horario. Si eres un nuevo competidor, te recomendamos encarecidamente que asistas al tutorial de competición que haremos el sábado a primera hora. Si no, está siempre al menos 15 minutos antes de que empiece la primera ronda en la que compitas.\r\n\r\n**P: ¿Qué debo hacer cuando llegue?\r\nR:** Lo primero que hay que hacer es ir a la mesa de registro para que te podamos dar los regalos de inscripción y la acreditación. Si no hay nadie en la mesa, busca a alguien con la camiseta de staff para que te atienda.\r\n\r\n**P: ¿Debo estar durante todo el campeonato?\r\nR:** Sólo es necesario que estés cuando tengas que competir o hacer de juez/runner/mezclador. Se te dará un papel en el registro con todos tus horarios, fuera de ese horario eres libre de irte a disfrutar de la ciudad y la gastronomía. Los grupos también serán publicados en la pestaña Grupos.\r\n\r\n**P: ¿Donde miro los resultados y si paso a la siguiente ronda?\r\nR:** Los tiempos y clasificaciones de la competición se subirán a esta página unos días después de la competición y en directo en la web https://live.worldcubeassociation.org/\r\n\r\n-----------------------------\r\n\r\n# ![Bandera UK](https://i.imgur.com/t2O5Zsj.png)**English**\r\n**Q: How do I register?\r\nA:** First you need to make sure you have created a WCA account. You can do this by going to the sign-up page and creating an account. Once you have created the account and confirmed your email address, go to the Register section and follow the instructions carefully.\r\n\r\n**Q: Why am I not on the registration list yet?\r\nA:** Please make sure that you have followed the instructions in the Register section. You could also be on the waiting list. The competitor limit can be found on the General Info page, and you can see the number on accepted competitors on the Competitors page. If you have done everything correctly and the competition is not full then just be patient. We have to manually approve registrations and the organisers aren't available all the time. If you believe you have followed the steps correctly but still are not on the registration list after 2 days, then feel free to email us at the contact link on the General Info page.\r\n\r\n**Q: Can I change the events I am registered for?\r\nA:** As long as registration is still open, yes you are allowed to change events. Email us with the events you would like to change. If registration is closed then please do not email us with event changes. If at the competition you decide that you do not want to compete in an event, come up to the scramble table when your group is called and simply tell one of the Delegates that you are not going to compete.\r\n\r\n**Q: I am no longer able to attend, what do I do?\r\nA:** The first thing you need to do is tell us as soon as you know via the contact link on the General Info page. Competitions generally fill up quite quickly and letting us know that you can't attend means we can add someone from the waiting list. We cannot offer refunds if you can no longer attend as paying the registration fee is a commitment contract.\r\n\r\n**Q: How fast do I have to be to compete?\r\nA:** We recommend that you check out the Schedule tab - if you can make the time limit for the events you want to compete in, then you're fast enough! Loads of people come to competitions just to beat their own personal bests, and meet likeminded people, with no intention of winning or even making it past the first round.\r\n\r\n**Q: Are there different age categories?\r\nA:** All competitors compete on the same level and all ages are welcome. In general most are aged 10-20 but we have plenty of regulars who are older or younger than this!\r\n\r\n**Q: Do I use my own cubes to compete?\r\nA:** Yes! Make sure to bring cubes for all events you are competing in and look after them, you don't want them to go missing.\r\n\r\n**Q: Can I come only to spectate?\r\nA:** Yes! Spectating this competition will be free for everyone. Take a look at the schedule to come to see the events you are more interested in or to know when the finals are happening.\r\n\r\n**Q: When do I arrive at the competition?\r\nA:** We recommend you to register on the time frames dedicated to that regard, which you can find on the Schedule tab. If you are a new competitor, we highly recommend that you show up for the introduction to competing which is held as the first thing on Saturday. Otherwise, we recommend you turn up at least 15 minutes before your first event.\r\n\r\n**Q: What do I do when I arrive?\r\nA:** The first thing you do when you arrive is find the registration desk if registration is open. If there is nobody present at the registration desk then find a staff member and we will make sure to register you.\r\n\r\n**Q: When can I leave the competition?\r\nA:** It's only necessary to stay when you have to compete or be a judge/runner/scrambler. You will be given a paper with all your schedules, outside of which you are free to go enjoy the city and the gastronomy. The groups will be published on the Groups tab too.\r\n\r\n**Q: How do I find results?\r\nA:** All the results will be found on this page a couple of days after the competition, once they have all been checked and uploaded. Also on the website https://live.worldcubeassociation.org/","display_order":6},{"id":28616,"competition_id":"LazarilloOpen2023","name":"Devoluciones y lista de espera / Refunds and waitlist","content":"# ![Bandera esp](https://i.imgur.com/fMh9pht.png) **Español**\r\n\r\nNo habrá reembolso bajo ninguna circunstancia para aquellos competidores que hayan sido aceptados en el campeonato y se den de baja de forma voluntaria.\r\nSi el campeonato se llena, los competidores que se queden en lista de espera o se retiren de la lista de espera, recibirán el importe del registro, deduciendo la comisión de pago.\r\n\r\n-------------------\r\n\r\n# ![Bandera UK](https://i.imgur.com/t2O5Zsj.png)**English**\r\n\r\nThere will be no refund under any circumstances for those competitors who have been accepted into the open and voluntarily cancel their registration.\r\nIf the competition is full, competitors who remain on the waiting list or withdraw from the waiting list, will receive the registration fee, deducting the transaction fee.\r\n# Lista de espera / Waitlist\r\n1. \tZhiqi Zhou Xie\r\n2. \tSantiago Siguero Gracía\r\n3. \tÁlex Pozuelo","display_order":7},{"id":29229,"competition_id":"LazarilloOpen2023","name":"Patrocinadores / Sponsors","content":"**RESIDENCIA MÉNDEZ**\r\n![](https://www.worldcubeassociation.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaUpFIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--f9ab8d5f14d6b42fc59a0d0d35badd96460ea68e/1.jpg)\r\n[Sitio web](https://www.residenciamendez.com/)\r\nCalle San Claudio, 14, Salamanca\r\nTeléfono: +34 679 125 338\r\n\r\n**LATVERIA STORE**\r\n![](https://www.worldcubeassociation.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaU5FIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--a45b737251ca69bf9c660bedaaa2777f5206748c/2.jpg)\r\n[Sitio web](https://latveriastoresalamanca.catinfog.com/)\r\nCalle Pedro Cojos, 12, Salamanca\r\nTeléfono: +34 624 607 190\r\n\r\n**PAKIPALLÁ**\r\n![](https://www.worldcubeassociation.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaVJFIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--59550c7c55fb58099b9ad0f269b6c55d2d43ccd2/3.jpg)\r\n[Sitio web](https://www.pakipalla.es/)\r\nCalle San Justo, 27, Salamanca\r\nTeléfono: +34 626 707 311\r\n\r\n**GRUPO VÍCTOR GÓMEZ**\r\n![](https://www.worldcubeassociation.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaVZFIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--9333cad21b0eb8e83f4e5ee28f1d0577c12a97db/4.jpg)\r\n[Sitio web](http://www.grupovictorgomez.com/)\r\nCtra. Guijuelo - Salvatierra, km. 1,800 - Apartado de Correos 11\r\nTeléfono: +34 923 580 654\r\n\r\n**ERASMUS INTERNACIONAL**\r\n![](https://www.worldcubeassociation.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaVpFIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--9502e257ce33e3b964d99b73b2ed28c4261cd99a/5.jpg)\r\n[Instagram](https://www.instagram.com/erasmusbruincafe/)\r\nCalle Melendez 7, Salamanca\r\nTeléfono: +34 923 265 742","display_order":8}],"class":"competition"}, - {"id":"LazarilloOpen2024","name":"STUBBED Lazarillo Open 2024","information":"![](https://www.worldcubeassociation.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBbGs0IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--2fc77e484245394e186eab0decbebe7bd1d84f6a/LazarilloOpen_Logo.png)\r\n\r\nLos socios de la AES recibirán un reembolso del 15% del coste de la inscripción después del final de la competición.\r\n\r\nAES members will receive a 15% refund of their registration fee after the end of the competition.","venue":"Palacio de Congresos de Salamanca","contact":"[Organización](mailto:lazarilloopen@gmail.com)","registration_open":"2023-05-02T13:54:00.000Z","registration_close":"2024-07-17T13:54:00.000Z","use_wca_registration":true,"announced_at":"2023-04-11T00:40:07.000Z","base_entry_fee_lowest_denomination":1500,"currency_code":"EUR","start_date":"2023-07-29","end_date":"2023-07-30","enable_donations":false,"competitor_limit":80,"extra_registration_requirements":"","on_the_spot_registration":false,"on_the_spot_entry_fee_lowest_denomination":null,"refund_policy_percent":0,"refund_policy_limit_date":"2023-07-17T13:54:00.000Z","guests_entry_fee_lowest_denomination":0,"external_registration_page":"","cancelled_at":null,"waiting_list_deadline_date":"2023-07-24T00:00:00.000Z","event_change_deadline_date":"2024-07-24T00:00:00.000Z","guest_entry_status":"free","allow_registration_edits":true,"allow_registration_self_delete_after_acceptance":true,"allow_registration_without_qualification":false,"guests_per_registration_limit":null,"force_comment_in_registration":true,"url":"http://localhost:3000/competitions/LazarilloOpen2024","website":"http://localhost:3000/competitions/LazarilloOpen2023","short_name":"Lazarillo Open 2023","city":"Salamanca","venue_address":"Cuesta de Oviedo s/n, 37008 Salamanca","venue_details":"Sala de Ensayos","latitude_degrees":40.962812,"longitude_degrees":-5.669562,"country_iso2":"ES","event_ids":["333","222","444","333bf","minx","pyram","skewb","sq1","444bf"],"registration_opened?":true,"main_event_id":"333bf","number_of_bookmarks":26,"using_stripe_payments?":null,"delegates":[{"id":6113,"created_at":"2015-10-19T17:54:38.000Z","updated_at":"2023-07-08T11:11:06.000Z","name":"Josete Sánchez","delegate_status":"candidate_delegate","wca_id":"2015SANC18","gender":"m","country_iso2":"ES","url":"","country":{"id":"Spain","name":"Spain","continentId":"_Europe","iso2":"ES"},"email":"6113@worldcubeassociation.org","region":"Spain","senior_delegate_id":454,"class":"user","teams":[{"id":588,"friendly_id":"wdc","leader":false,"name":"Josete Sánchez","senior_member":false,"wca_id":"2015SANC18","avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2015SANC18/1665601824.jpeg","thumb":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2015SANC18/1665601824_thumb.jpeg"}}}],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2015SANC18/1665601824.jpeg","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2015SANC18/1665601824_thumb.jpeg","is_default":false}},{"id":47863,"created_at":"2017-02-27T10:24:09.000Z","updated_at":"2023-07-10T12:59:43.000Z","name":"Alejandro Nicolay","delegate_status":"trainee_delegate","wca_id":"2017NICO01","gender":"m","country_iso2":"ES","url":"","country":{"id":"Spain","name":"Spain","continentId":"_Europe","iso2":"ES"},"email":"47863@worldcubeassociation.org","region":"Spain","senior_delegate_id":454,"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2017NICO01/1663356326.jpeg","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2017NICO01/1663356326_thumb.jpeg","is_default":false}}],"organizers":[{"id":24320,"created_at":"2016-07-22T08:17:29.000Z","updated_at":"2023-07-10T06:53:49.000Z","name":"Asociación Española de Speedcubing","delegate_status":null,"wca_id":null,"gender":"o","country_iso2":"ES","url":"","country":{"id":"Spain","name":"Spain","continentId":"_Europe","iso2":"ES"},"class":"user","teams":[],"avatar":{"url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","is_default":true}},{"id":32834,"created_at":"2016-10-23T15:48:25.000Z","updated_at":"2023-07-10T15:05:05.000Z","name":"Agus Wals","delegate_status":null,"wca_id":"2016WALS01","gender":"m","country_iso2":"ES","url":"","country":{"id":"Spain","name":"Spain","continentId":"_Europe","iso2":"ES"},"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2016WALS01/1681228666.jpg","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2016WALS01/1681228666_thumb.jpg","is_default":false}},{"id":112652,"created_at":"2018-06-13T15:31:52.000Z","updated_at":"2023-07-10T08:09:39.000Z","name":"María Ángeles García Franco","delegate_status":null,"wca_id":"2018FRAN17","gender":"f","country_iso2":"ES","url":"","country":{"id":"Spain","name":"Spain","continentId":"_Europe","iso2":"ES"},"class":"user","teams":[],"avatar":{"url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2018FRAN17/1660935969.JPG","pending_url":"http://localhost:3000/assets/missing_avatar_thumb-ff0c01100dab5125eabebc7b8391dcd796fcbf2cc3aee7bd2deb2400e005a9eb.png","thumb_url":"https://avatars.worldcubeassociation.org/uploads/user/avatar/2018FRAN17/1660935969_thumb.JPG","is_default":false}}],"tabs":[{"id":28146,"competition_id":"LazarilloOpen2023","name":"Cómo llegar / How to arrive","content":"# ![Bandera esp](https://i.imgur.com/fMh9pht.png) **Español**\r\n**En coche**\r\nSalamanca es una ciudad muy bien comunicada en cuanto a carreteras. \r\n* *Si llegas desde el norte*\r\nCoger en Valladolid la autovía A-62, que conecta las dos ciudades.\r\n* *Si llegas desde el noroeste*\r\nCoger la Autovía de la Plata (A-66) en Zamora.\r\n* *Si llegas de Madrid*\r\nTomar la Autovía del Noroeste (A-6 y luego AP-6) hacia Villacastín, y ahí cambiar a la AP-51. En Ávila, tomar la A-50.\r\n* *Si llegas del sur*\r\nLo mejor es coger la A-66, que pasa por Cáceres, Béjar y llega a Salamanca.\r\n\r\n**En autobús**\r\nSalamanca está comunicada por autobús con la mayor parte de las capitales de provincia de la Península, además de numerosos pueblos y ciudades.\r\nDesde Madrid salen autobuses con destino a Salamanca cada hora, y en las franjas horarias más demandadas, cada media hora.\r\nAdemás, existe un servicio de autobús directo desde las terminales 1 y 4 del Aeropuerto Adolfo Suárez Madrid-Barajas.\r\nAlgunas de las rutas que paran en Salamanca son:\r\n\r\n* ALSA (http://www.alsa.es): Badajoz - Bilbao / A Coruña - Algeciras.\r\n* Avanzabús (http://www.avanzabus.com): Madrid - Salamanca / Ávila - Salamanca / Barajas - Salamanca / Valladolid - Salamanca / Segovia - Salamanca.\r\n* Zamora Salamanca, S.A (http://www.zamorasalamanca.es/).: Zamora - Salamanca.\r\n\r\n**En tren**\r\nSalamanca cuenta con su propia estación de tren. A ella se puede llegar en tren directo desde ciudades españolas como Madrid, Valladolid o Vitoria. \r\nLa estación de tren está a unos 20 minutos del centro de la ciudad, o incluso menos yendo en bus o taxi.\r\n\r\n**En avión**\r\nLos aeropuertos más cercanos para llegar a Salamanca con el de Villanubla (Valladolid) y el de Barajas (Madrid). Desde estas ciudades se puede llegar a Salamanca en tren o autobús.\r\n\r\n----------------------------\r\n\r\n# ![Bandera UK](https://i.imgur.com/t2O5Zsj.png) **English**\r\n\r\n**By car**\r\nSalamanca is a city with very good road connections. \r\n* *If you are arriving from the north*\r\nTake the A-62 in Valladolid, which connects the two cities.\r\n* *If you are coming from the northwest*.\r\nTake the A-66 in Zamora.\r\n* *If you are arriving from Madrid*\r\nTake the A-6 and then AP-6 towards Villacastín, and there change to the AP-51. In Ávila, take the A-50.\r\n* *If you are coming from the south*\r\nIt is best to take the A-66, which passes through Cáceres, Béjar and reaches Salamanca.\r\n\r\n\r\n**By bus**\r\nSalamanca is connected by bus with most of the cities in Spain. Buses depart from Madrid to Salamanca every hour, and in some occasions, every half hour.\r\nThere is also a direct bus service from T1 and T4 of Adolfo Suárez Madrid-Barajas Airport.\r\nSome of the bus routes that stop in Salamanca are:\r\n\r\n* ALSA (http://www.alsa.es): Badajoz - Bilbao / A Coruña - Algeciras.\r\n* Avanzabús (http://www.avanzabus.com): Madrid - Salamanca / Ávila - Salamanca / Barajas - Salamanca / Valladolid - Salamanca / Segovia - Salamanca.\r\n* Zamora Salamanca, S.A (http://www.zamorasalamanca.es/).: Zamora - Salamanca.\r\n\r\n**By train**\r\nSalamanca has its own train station. There are direct train conection from Madrid, Valladolid or Vitoria.\r\nThe train station is about 20 minutes from the city centre, or less if you take a bus or a taxi.\r\n\r\n**By plane**\r\nThe closest airports to Salamanca are Villanubla (Valladolid) and Barajas (Madrid). From these cities you can travel to Salamanca by train or bus.","display_order":1},{"id":28751,"competition_id":"LazarilloOpen2023","name":"Alojamiento / Accommodation","content":"# ![Bandera esp](https://i.imgur.com/fMh9pht.png) **Español**\r\n**ALOJAMIENTO RECOMENDADO**\r\n\r\nSi vienes de fuera de Salamanca y necesitas alojarte en la ciudad durante los días de la competición, la **Residencia Méndez** colabora con el Lazarillo Open, ofreciendo precios especiales para ese fin de semana. Se trata de una residencia universitaria que está situada a pocos minutos del Palacio de Congresos. Al celebrar el evento en una época donde apenas hay estudiantes, ponen a disposición del Lazarillo Open casi la totalidad de sus 50 habitaciones individuales y dobles.\r\n**[Más información y reservas](https://www.residenciamendez.com/)**.\r\n\r\nTambién puedes alojarte en la Residencia de Estudiantes Hernán Cortés. Al estar fuera del calendario lectivo universitario, esta residencia ofrece la opción de contratar alojamiento por días.\r\nAdemás, desde la organización hemos conseguido un **descuento del 15% para los competidores**.\r\nPara conseguir este descuento, tendréis que reservar a través de **[este enlace](https://direct-book.com/resa/properties/hernancortesdirect?locale=es\u0026items%5B0%5D%5Badults%5D=1\u0026items%5B0%5D%5Bchildren%5D=0\u0026items%5B0%5D%5Binfants%5D=0\u0026currency=EUR\u0026checkInDate=2023-07-28\u0026checkOutDate=2023-07-30\u0026trackPage=yes\u0026promocode=LAZARILLO2023)**.\r\nAntes de confirmar la reserva, aseguraos de que el código **LAZARILLO2023** está aplicado.\r\nEste alojamiento se encuentra a tan solo 10 minutos andando del lugar de la competición. \r\n\r\n---------------------------\r\n# ![Bandera UK](https://i.imgur.com/t2O5Zsj.png) **English**\r\n**RECOMMENDED ACCOMMODATION**\r\n\r\nIf you come from outside Salamanca, and you need to stay in the city during the competition weekend, **Residencia Méndez** collaborates with Lazarillo Open. They offer special prices for housing that weekend. Residencia Méndez is a student housing, located a few minutes away from Palacio de Congresos. Since we celebrate this open in summer, when there is no students in Salamanca, they offer to the competitors almost all their 50 single and double rooms.\r\n**[More information and reservations](https://www.residenciamendez.com/)**\r\n\r\nYou can also rest in Residencia de Estudiantes Hernán Cortés. Due to the University School calendar, this housing brings the opportunity to pay accommodation for days.\r\nMoreover, the organization of the competition has a **15% discount for competitors and their companions**.\r\nTo benefit from this discount, you have to booking at **[this link](https://direct-book.com/resa/properties/hernancortesdirect?locale=es\u0026items%5B0%5D%5Badults%5D=1\u0026items%5B0%5D%5Bchildren%5D=0\u0026items%5B0%5D%5Binfants%5D=0\u0026currency=EUR\u0026checkInDate=2023-07-28\u0026checkOutDate=2023-07-30\u0026trackPage=yes\u0026promocode=LAZARILLO2023)**.\r\nBefore making the payment, please confirm that you are using the promotional code **LAZARILLO2023**\r\nThis housing is only 10 minutes away from the competition venue.","display_order":2},{"id":28819,"competition_id":"LazarilloOpen2023","name":"Comida / Lunch","content":"# ![Bandera esp](https://i.imgur.com/fMh9pht.png) **Español**\r\nEl Palacio de Congresos está a tan solo unos minutos de la Rúa Mayor, una de las zonas con más restaurantes de Salamanca. Estos restaurantes ofrecen menú del día o tapas a muy buen precio.\r\nPara ir a sitios de comida rápida, hay que llegar hasta los alrededores de la Plaza Mayor, donde hay cadenas de pizzerías, hamburgueserías y bocadillos. También hay algunos sitios que ofrecen este tipo de comida sin ser cadenas de comida, como los bares de la calle Obispo Jarrín o la plaza de San Julián. Estos dos lugares se encuentran a unos 10 minutos del Palacio de Congresos.\r\n\r\n---------------------\r\n# ![Bandera UK](https://i.imgur.com/t2O5Zsj.png) **English**\r\nPalacio de Congresos is a few minutes away from Rúa Mayor, one of the streets with more restaurants in Salamanca.These kind of restaurants offer set menus and tapas at very good prices.\r\nIf you want to have lunch in fast food restaurant, most of them are around Plaza Mayor, where you can find pizzas, burgers or sandwiches. You can find other restaurants which offer this kind of food, and they are not franchises, such as Obispo Jarrín street and San Julián square bars. This two places are about ten minutes away from Palacio de Congresos.","display_order":3},{"id":28977,"competition_id":"LazarilloOpen2023","name":"GAN Cube","content":"# ![Bandera esp](https://i.imgur.com/fMh9pht.png) **Español**\r\n[GAN Cube](https://gancube.com/), marca líder en la fabricación de cubos y otros accesorios para speedcubing, ha elegido el **Lazarillo Open 2023** como una de las competiciones de la WCA a las que patrocinar este año.\r\nEsta marca tiene un gran compromiso con la comunidad cubera y una gran pasión por el speedcubing.\r\nPor todo ello, hemos querido que tenga una gran presencia en el Lazarillo Open 2023.\r\n¿Quieres ver lo que estamos preparando de la mano de GAN para el Lazarillo Open?\r\nPara saber más sobre GAN Cube puedes visitar los siguientes enlaces:\r\n* [Web Oficial de GAN Cube](https://www.gancube.com/es/)\r\n* [Facebook de GAN Cube](https://www.facebook.com/Gancube/)\r\n* [Instagram de GAN Cube](https://www.instagram.com/gancube/)\r\n* [Twitter de GAN Cube](https://twitter.com/gancube)\r\n# ![Bandera UK](https://i.imgur.com/t2O5Zsj.png) **English**\r\n[GAN Cube](https://gancube.com/), leading brand in the manufacture of cubes and other accessories for speedcubing, has chosen the **Lazarillo Open 2023** as one of the WCA competitions to sponsor this year.\r\nThis brand has a great commitment to the cubing community and a great passion for speedcubing.\r\nFor all these reasons, we wanted it to have a big presence at the Lazarillo Open 2023.\r\nDo you want to see what we are preparing with GAN for the Lazarillo Open?\r\nIf you want to kno more about GAN Cube, you can visit these links:\r\n* [GAN Cube Official Website](https://www.gancube.com/es/)\r\n* [GAN Cube Facebook](https://www.facebook.com/Gancube/)\r\n* [GAN Cube Instagram](https://www.instagram.com/gancube/)\r\n* [GAN Cube Twitter](https://twitter.com/gancube)\r\n\r\n\r\n![](https://www.worldcubeassociation.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBak02IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--3d245a4d8d9a304bb2c5230a3821649d6923e7eb/GAN.jpg)","display_order":4},{"id":28015,"competition_id":"LazarilloOpen2023","name":"Asociación Española de Speedcubing (AES)","content":"###¿Qué es?\r\nLa [Asociación Española de Speedcubing](https://www.speedcubingspain.org/) (AES) es una asociación sin ánimo de lucro destinada a organizar, colaborar y difundir los eventos y actividades relacionadas con el mundo del speedcubing en España. Estas actividades se centran en:\r\n\r\n* Colaboración en competiciones oficiales.\r\n* Realización anual del Campeonato de España.\r\n* Organización del Campeonato Interescolar. \r\n* Creación de eventos online.\r\n* Conectar a las personas, manteniendo una comunidad sana.\r\n\r\n###Servicios\r\n* **Material:** Aportamos cronómetros, tapacubos, y todo el material necesario para organizar un evento de speedcubing de calidad.\r\n* **Difusión:** Promovemos y damos difusión a los eventos y al speedcubing en general.\r\n* **Respaldo:** Ayudamos a los organizadores y a la comunidad cubera mediante el respaldo de una entidad jurídica.\r\n\r\n###¡Colabora!\r\nComo organización sin ánimo de lucro que funciona con voluntarios al 100%, agradecemos vuestras aportaciones para ayudar a que el mundo del Speedcubing en España crezca. Puedes colaborar realizando una [donación](https://www.paypal.com/paypalme/AESpeedcubing?locale.x=es_ES), o bien haciéndote socio desde tan solo 1,25€ al mes pinchando [aquí](https://speedcubingspain.org/register/), con lo que obtendrás las siguientes ventajas:\r\n\r\n* Al menos el 15% de descuento en todos los eventos que organice o colabore la AES.\r\n* Sorteos y premios exclusivos para socios.\r\n* Aviso vía e-mail de los nuevos eventos de speedcubing de España.\r\n* Participar y tener derecho a voto en las Asambleas Generales.\r\n* Entrada en el grupo de Telegram exclusivo para los socios.\r\n \r\n###¡Síguenos en nuestras redes sociales!\r\n\r\n* Instagram: [@aespeedcubing](https://www.instagram.com/aespeedcubing/?hl=es)\r\n* Facebook: [Asociación Española de Speedcubing (@AESpeedcubing)](https://www.facebook.com/search/top?q=asociaci%C3%B3n%20espa%C3%B1ola%20de%20speedcubing)\r\n* Twitter: [@AESpeedcubing](https://twitter.com/AESpeedcubing)\r\n* Twitch: [AESpeedcubing](https://www.twitch.tv/aespeedcubing)\r\n* YouTube: [Asociación Española de Speedcubing](https://www.youtube.com/channel/UCryvWN5_nrvi9af0EPxEY5g)\r\n\r\n[![](https://www.worldcubeassociation.org/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaHNTIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--b77ebd2ea85a467d70927603c8ac93439c5d47f8/aes_logo_mini.png \"Asociación Española de Speedcubing (AES)\")](https://www.speedcubingspain.org/)","display_order":5},{"id":28508,"competition_id":"LazarilloOpen2023","name":"PMF / FAQ","content":"# ![Bandera esp](https://i.imgur.com/fMh9pht.png) **Español**\r\n**P: ¿Cómo me registro?\r\nR:** En primer lugar tienes que asegurarte de que tengas creada una cuenta de la WCA. Si no es así, se puede hacer de manera muy sencilla haciendo click en \"registrarse\" y creándote una. Una vez hecho esto, vete a la pestaña Registro y sigue los pasos correspondientes.\r\n\r\n**P: ¿Por qué no aparezco en la lista de competidores?\r\nR:** Por favor, asegúrate de haber seguido todas las instruciones de la pestaña Registro. También es posible que estés en la lista de espera. El limite de competidores puedes verlo en la página Información General y la lista de competidores en Competidores. Si crees haber hecho todo correctamente y aun así no apareces, ten paciencia. Los registros se aprueban de manera manual y los Delegados y organizadores no están disponibles en todo momento. Si tu registro no se ha aceptado pasados dos días, mándarnos un correo.\r\n\r\n**P: ¿Puedo cambiar los eventos a los que me presento?\r\nR:** Sí, mientras el plazo de inscripción siga abierto. Contáctanos a través del correo de la organización y dinos qué categorías quieres cambiar. Si el registro ya se ha cerrado, por favor, no mandes ninguna petición de cambio de eventos. Si el dia de la competición decides no participar en alguna categoría simplemente ve a la mesa de mezclas cuando te toque competir y dile a uno de los Delegados que no te vas a presentar.\r\n\r\n**P: Ya no puedo asistir, ¿qué debo hacer?\r\nR:** Lo primero que tienes que hacer es informarnos vía email tan pronto como lo sepas para que otro competidor pueda ocupar tu lugar. No podemos ofrecer reembolsos ya que pagar la tarifa de registro es un contrato de compromiso.\r\n\r\n**P: ¿Cómo de rápido tengo que ser para poder competir?\r\nR:** Te recomendamos que mires la tabla del Horario para informarte de los tiempos límite de cada prueba. La mayoría de gente va a las competiciones para conocer a gente con la que comparten esta afición sin importar los tiempos o sencillamente a tratar de mejorar sus propios tiempos personales, ¡todo el mundo es apto para competir y pasarlo bien!\r\n\r\n**P: ¿Hay categorías para diferentes edades?\r\nR:** Todos los competidores participan en las mismas condiciones y participantes de todas las edades son bienvenidos. La mayoría de gente suele tener unos 15-20 años, pero también hay gente más joven y más mayor.\r\n\r\n**P: ¿Tengo que usar mis propios cubos para competir?\r\nR:** ¡Sí! Asegúrate de traer puzzles para todos los eventos en los que compitas y no los pierdas de vista.\r\n\r\n**P: ¿Puedo ir simplemente como espectador?\r\nR:** Sí, además la entrada es completamente gratuita para los espectadores. Echa un vistazo al horario del campeonato para que puedas venir a ver los eventos que más te interesen o enterarte de cuando son las finales.\r\n\r\n**P: ¿Cúando debo estar en el campeonato?\r\nR:** Recomendamos que te inscribas dentro de los plazos de inscripción que hay asignados en el horario. Si eres un nuevo competidor, te recomendamos encarecidamente que asistas al tutorial de competición que haremos el sábado a primera hora. Si no, está siempre al menos 15 minutos antes de que empiece la primera ronda en la que compitas.\r\n\r\n**P: ¿Qué debo hacer cuando llegue?\r\nR:** Lo primero que hay que hacer es ir a la mesa de registro para que te podamos dar los regalos de inscripción y la acreditación. Si no hay nadie en la mesa, busca a alguien con la camiseta de staff para que te atienda.\r\n\r\n**P: ¿Debo estar durante todo el campeonato?\r\nR:** Sólo es necesario que estés cuando tengas que competir o hacer de juez/runner/mezclador. Se te dará un papel en el registro con todos tus horarios, fuera de ese horario eres libre de irte a disfrutar de la ciudad y la gastronomía. Los grupos también serán publicados en la pestaña Grupos.\r\n\r\n**P: ¿Donde miro los resultados y si paso a la siguiente ronda?\r\nR:** Los tiempos y clasificaciones de la competición se subirán a esta página unos días después de la competición y en directo en la web https://live.worldcubeassociation.org/\r\n\r\n-----------------------------\r\n\r\n# ![Bandera UK](https://i.imgur.com/t2O5Zsj.png)**English**\r\n**Q: How do I register?\r\nA:** First you need to make sure you have created a WCA account. You can do this by going to the sign-up page and creating an account. Once you have created the account and confirmed your email address, go to the Register section and follow the instructions carefully.\r\n\r\n**Q: Why am I not on the registration list yet?\r\nA:** Please make sure that you have followed the instructions in the Register section. You could also be on the waiting list. The competitor limit can be found on the General Info page, and you can see the number on accepted competitors on the Competitors page. If you have done everything correctly and the competition is not full then just be patient. We have to manually approve registrations and the organisers aren't available all the time. If you believe you have followed the steps correctly but still are not on the registration list after 2 days, then feel free to email us at the contact link on the General Info page.\r\n\r\n**Q: Can I change the events I am registered for?\r\nA:** As long as registration is still open, yes you are allowed to change events. Email us with the events you would like to change. If registration is closed then please do not email us with event changes. If at the competition you decide that you do not want to compete in an event, come up to the scramble table when your group is called and simply tell one of the Delegates that you are not going to compete.\r\n\r\n**Q: I am no longer able to attend, what do I do?\r\nA:** The first thing you need to do is tell us as soon as you know via the contact link on the General Info page. Competitions generally fill up quite quickly and letting us know that you can't attend means we can add someone from the waiting list. We cannot offer refunds if you can no longer attend as paying the registration fee is a commitment contract.\r\n\r\n**Q: How fast do I have to be to compete?\r\nA:** We recommend that you check out the Schedule tab - if you can make the time limit for the events you want to compete in, then you're fast enough! Loads of people come to competitions just to beat their own personal bests, and meet likeminded people, with no intention of winning or even making it past the first round.\r\n\r\n**Q: Are there different age categories?\r\nA:** All competitors compete on the same level and all ages are welcome. In general most are aged 10-20 but we have plenty of regulars who are older or younger than this!\r\n\r\n**Q: Do I use my own cubes to compete?\r\nA:** Yes! Make sure to bring cubes for all events you are competing in and look after them, you don't want them to go missing.\r\n\r\n**Q: Can I come only to spectate?\r\nA:** Yes! Spectating this competition will be free for everyone. Take a look at the schedule to come to see the events you are more interested in or to know when the finals are happening.\r\n\r\n**Q: When do I arrive at the competition?\r\nA:** We recommend you to register on the time frames dedicated to that regard, which you can find on the Schedule tab. If you are a new competitor, we highly recommend that you show up for the introduction to competing which is held as the first thing on Saturday. Otherwise, we recommend you turn up at least 15 minutes before your first event.\r\n\r\n**Q: What do I do when I arrive?\r\nA:** The first thing you do when you arrive is find the registration desk if registration is open. If there is nobody present at the registration desk then find a staff member and we will make sure to register you.\r\n\r\n**Q: When can I leave the competition?\r\nA:** It's only necessary to stay when you have to compete or be a judge/runner/scrambler. You will be given a paper with all your schedules, outside of which you are free to go enjoy the city and the gastronomy. The groups will be published on the Groups tab too.\r\n\r\n**Q: How do I find results?\r\nA:** All the results will be found on this page a couple of days after the competition, once they have all been checked and uploaded. Also on the website https://live.worldcubeassociation.org/","display_order":6},{"id":28616,"competition_id":"LazarilloOpen2023","name":"Devoluciones y lista de espera / Refunds and waitlist","content":"# ![Bandera esp](https://i.imgur.com/fMh9pht.png) **Español**\r\n\r\nNo habrá reembolso bajo ninguna circunstancia para aquellos competidores que hayan sido aceptados en el campeonato y se den de baja de forma voluntaria.\r\nSi el campeonato se llena, los competidores que se queden en lista de espera o se retiren de la lista de espera, recibirán el importe del registro, deduciendo la comisión de pago.\r\n\r\n-------------------\r\n\r\n# ![Bandera UK](https://i.imgur.com/t2O5Zsj.png)**English**\r\n\r\nThere will be no refund under any circumstances for those competitors who have been accepted into the open and voluntarily cancel their registration.\r\nIf the competition is full, competitors who remain on the waiting list or withdraw from the waiting list, will receive the registration fee, deducting the transaction fee.\r\n# Lista de espera / Waitlist\r\n1. \tZhiqi Zhou Xie\r\n2. \tSantiago Siguero Gracía\r\n3. \tÁlex Pozuelo","display_order":7},{"id":29229,"competition_id":"LazarilloOpen2023","name":"Patrocinadores / Sponsors","content":"**RESIDENCIA MÉNDEZ**\r\n![](https://www.worldcubeassociation.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaUpFIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--f9ab8d5f14d6b42fc59a0d0d35badd96460ea68e/1.jpg)\r\n[Sitio web](https://www.residenciamendez.com/)\r\nCalle San Claudio, 14, Salamanca\r\nTeléfono: +34 679 125 338\r\n\r\n**LATVERIA STORE**\r\n![](https://www.worldcubeassociation.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaU5FIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--a45b737251ca69bf9c660bedaaa2777f5206748c/2.jpg)\r\n[Sitio web](https://latveriastoresalamanca.catinfog.com/)\r\nCalle Pedro Cojos, 12, Salamanca\r\nTeléfono: +34 624 607 190\r\n\r\n**PAKIPALLÁ**\r\n![](https://www.worldcubeassociation.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaVJFIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--59550c7c55fb58099b9ad0f269b6c55d2d43ccd2/3.jpg)\r\n[Sitio web](https://www.pakipalla.es/)\r\nCalle San Justo, 27, Salamanca\r\nTeléfono: +34 626 707 311\r\n\r\n**GRUPO VÍCTOR GÓMEZ**\r\n![](https://www.worldcubeassociation.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaVZFIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--9333cad21b0eb8e83f4e5ee28f1d0577c12a97db/4.jpg)\r\n[Sitio web](http://www.grupovictorgomez.com/)\r\nCtra. Guijuelo - Salvatierra, km. 1,800 - Apartado de Correos 11\r\nTeléfono: +34 923 580 654\r\n\r\n**ERASMUS INTERNACIONAL**\r\n![](https://www.worldcubeassociation.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBaVpFIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--9502e257ce33e3b964d99b73b2ed28c4261cd99a/5.jpg)\r\n[Instagram](https://www.instagram.com/erasmusbruincafe/)\r\nCalle Melendez 7, Salamanca\r\nTeléfono: +34 923 265 742","display_order":8}],"class":"competition"} - ] -} diff --git a/spec/fixtures/patches.json b/spec/fixtures/patches.json deleted file mode 100644 index e6ce9859..00000000 --- a/spec/fixtures/patches.json +++ /dev/null @@ -1,236 +0,0 @@ -{ - "800-cancel-no-reg": { - "user_id":"800", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"cancelled" - } - }, - "816-cancel-bad-comp": { - "user_id":"158816", - "competition_id":"InvalidCompID", - "competing": { - "status":"cancelled" - } - }, - "820-missing-comment": { - "user_id":"158820", - "competition_id":"LazarilloOpen2024", - "competing": { - "event_ids":["333", "333bf", "444"] - } - }, - "820-delayed-update": { - "user_id":"158820", - "competition_id":"LazarilloOpen2023", - "competing": { - "event_ids":["333", "333bf", "444"] - } - }, - "816-comment-update": { - "user_id":"158816", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "comment":"updated registration comment" - } - }, - "816-guest-update": { - "user_id":"158816", - "competition_id":"CubingZANationalChampionship2023", - "guests":2 - }, - "816-cancel-full-registration": { - "user_id":"158816", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"cancelled" - } - }, - "816-cancel-and-change-events": { - "user_id":"158816", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"cancelled", - "event_ids":["555","666","777"] - } - }, - "816-events-update": { - "user_id":"158816", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "event_ids":["333", "333mbf", "555","666","777"] - } - }, - "816-status-update-1": { - "user_id":"158816", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"pending" - } - }, - "816-status-update-2": { - "user_id":"158816", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"waiting_list" - } - }, - "816-status-update-3": { - "user_id":"158816", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"bad_status_name" - } - }, - "817-comment-update": { - "user_id":"158817", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "comment":"updated registration comment - had no comment before" - } - }, - "817-comment-update-2": { - "user_id":"158817", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "comment":"comment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characters" - } - }, - "817-guest-update": { - "user_id":"158817", - "competition_id":"CubingZANationalChampionship2023", - "guests":2 - }, - "817-guest-update-2": { - "user_id":"158817", - "competition_id":"CubingZANationalChampionship2023", - "guests":10 - }, - "817-events-update": { - "user_id":"158817", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "event_ids":["333"] - } - }, - "817-events-update-2": { - "user_id":"158817", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "event_ids":[] - } - }, - "817-events-update-4": { - "user_id":"158817", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"cancelled", - "event_ids":[] - } - }, - "817-events-update-5": { - "user_id":"158817", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "event_ids":["333", "333mbf", "333fm"] - } - }, - "817-events-update-6": { - "user_id":"158817", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "event_ids":["333", "333mbf", "888"] - } - }, - "817-cancel-full-registration": { - "user_id":"158817", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"cancelled" - } - }, - "817-status-update-1": { - "user_id":"158817", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"accepted" - } - }, - "817-status-update-2": { - "user_id":"158817", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"waiting_list" - } - }, - "818-cancel-full-registration": { - "user_id":"158818", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"cancelled" - } - }, - "819-cancel-full-registration": { - "user_id":"158819", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"cancelled" - } - }, - "819-status-update-1": { - "user_id":"158819", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"accepted" - } - }, - "819-status-update-2": { - "user_id":"158819", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"pending" - } - }, - "819-status-update-3": { - "user_id":"158819", - "competition_id":"CubingZANationalChampionship2024", - "competing": { - "status":"accepted" - } - }, - "823-cancel-full-registration": { - "user_id":"158823", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"cancelled" - } - }, - "823-cancel-wrong-lane": { - "attendee_id":"CubingZANationalChampionship2023-158823", - "staffing":{ - "status":"cancelled" - } - }, - "816-cancel-full-registration_2": { - "user_id":"158816", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"cancelled", - "admin_comment":"registration delete comment" - } - }, - "073-cancel-full-registration": { - "user_id":"15073", - "competition_id":"BrizZonSylwesterOpen2023", - "competing": { - "status":"cancelled" - } - }, - "1-cancel-full-registration": { - "user_id":"1", - "competition_id":"CubingZANationalChampionship2023", - "competing": { - "status":"cancelled" - } - } -} diff --git a/spec/fixtures/registration.json b/spec/fixtures/registration.json deleted file mode 100644 index 9bc24c36..00000000 --- a/spec/fixtures/registration.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "attendee_id":"CubingZANationalChampionship2023-158816", - "competition_id":"CubingZANationalChampionship2023", - "user_id":"158816", - "is_attending":"True", - "lanes":[{ - "lane_name":"competitor", - "lane_state":"Accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333MBF", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - }, - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158816" - } - ], - "hide_name_publicly":"False" -} diff --git a/spec/fixtures/registrations.json b/spec/fixtures/registrations.json deleted file mode 100644 index eca8bf93..00000000 --- a/spec/fixtures/registrations.json +++ /dev/null @@ -1,1158 +0,0 @@ -[ - { - "attendee_id":"CubingZANationalChampionship2023-158816", - "competition_id":"CubingZANationalChampionship2023", - "user_id":"158816", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - } - ], - "comment":"basic registration comment", - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158816" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"CubingZANationalChampionship2023-1", - "competition_id":"CubingZANationalChampionship2023", - "user_id":"1", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"1" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"CubingZANationalChampionship2023-158817", - "competition_id":"CubingZANationalChampionship2023", - "user_id":"158817", - "lanes":[{ - "lane_name":"competing", - "lane_state":"pending", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"pending" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"pending" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158817" - } - ] - }, - { - "attendee_id":"CubingZANationalChampionship2023-158818", - "competition_id":"CubingZANationalChampionship2023", - "lanes":[{ - "lane_name":"competing", - "lane_state":"update_pending", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"update-pending" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158818" - } - ] - }, - { - "attendee_id":"CubingZANationalChampionship2023-158819", - "competition_id":"CubingZANationalChampionship2023", - "is_attending":false, - "user_id":"158819", - "lanes":[{ - "lane_name":"competing", - "lane_state":"waiting_list", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"waiting_list" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158819" - }] - }, - { - "attendee_id":"CubingZANationalChampionship2023-158820", - "competition_id":"CubingZANationalChampionship2023", - "user_id":"158820", - "hide_name_publicly": false, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158820" - }] - }, - { - "attendee_id":"CubingZANationalChampionship2023-158821", - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158821" - }] - }, - { - "attendee_id":"CubingZANationalChampionship2023-158822", - "competition_id":"CubingZANationalChampionship2023", - "user_id":"158822" - }, - { - "attendee_id":"CubingZANationalChampionship2023-158823", - "competition_id":"CubingZANationalChampionship2023", - "user_id":"158823", - "lanes":[{ - "lane_name":"competing", - "lane_state":"cancelled", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"cancelled" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"cancelled" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"cancelled", - "last_action_datetime":"2023-01-01T00:01:00Z", - "last_action_user":"158823" - }] - }, - { - "attendee_id":"CubingZANationalChampionship2023-158824", - "competition_id":"CubingZANationalChampionship2023", - "user_id":"158824", - "lanes":[{ - "lane_name":"competing", - "lane_state":"pending", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"pending" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"pending" - } - ], - "guests":3, - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0" - }] - }, - { - "attendee_id":"WinchesterWeeknightsIV2023-158816", - "competition_id":"WinchesterWeeknightsIV2023", - "user_id":"158816", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158816" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"WinchesterWeeknightsIV2023-158817", - "competition_id":"WinchesterWeeknightsIV2023", - "user_id":"158817", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158818" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"WinchesterWeeknightsIV2023-158818", - "competition_id":"WinchesterWeeknightsIV2023", - "user_id":"158818", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158818" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"BangaloreCubeOpenJuly2023-158818", - "competition_id":"BangaloreCubeOpenJuly2023", - "user_id":"158818", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158818" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"BangaloreCubeOpenJuly2023-158819", - "competition_id":"BangaloreCubeOpenJuly2023", - "user_id":"158819", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158819" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"LazarilloOpen2023-158820", - "competition_id":"LazarilloOpen2023", - "user_id":"158820", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158820" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"LazarilloOpen2023-158821", - "competition_id":"LazarilloOpen2023", - "user_id":"158821", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158821" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"LazarilloOpen2023-158822", - "competition_id":"LazarilloOpen2023", - "user_id":"158822", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158822" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"LazarilloOpen2023-158823", - "competition_id":"LazarilloOpen2023", - "user_id":"158823", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158823" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"BrizZonSylwesterOpen2023-158817", - "competition_id":"BrizZonSylwesterOpen2023", - "user_id":"158817", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"444", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333bf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158817" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"InvalidCompID-158817", - "competition_id":"InvalidCompID", - "user_id":"158817", - "lanes":[{ - "lane_name":"competing", - "lane_state":"pending", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"pending" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"pending" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158817" - } - ] - }, - { - "attendee_id":"CubingZANationalChampionship2023-209943", - "competition_id":"CubingZANationalChampionship2023", - "user_id":"209943", - "lanes":[{ - "lane_name":"competing", - "lane_state":"pending", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"pending" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"pending" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"209943" - } - ] - }, - { - "attendee_id":"CubingZANationalChampionship2023-999999", - "competition_id":"CubingZANationalChampionship2023", - "user_id":"999999", - "lanes":[{ - "lane_name":"competing", - "lane_state":"pending", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"pending" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"pending" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"999999" - } - ] - }, - { - "attendee_id":"CubingZANationalChampionship2023-158201", - "competition_id":"CubingZANationalChampionship2023", - "user_id":"158201", - "lanes":[{ - "lane_name":"competing", - "lane_state":"pending", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333fm", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"pending" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"pending" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158201" - } - ] - }, - { - "attendee_id":"CubingZANationalChampionship2023-158202", - "competition_id":"CubingZANationalChampionship2023", - "user_id":"158202", - "lanes":[{ - "lane_name":"competing", - "lane_state":"pending", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"888", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"pending" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"pending" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158202" - } - ] - }, - { - "attendee_id":"BrizZonSylwesterOpen2023-15073", - "competition_id":"BrizZonSylwesterOpen2023", - "user_id":"15073", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"444", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333bf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"15073" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"BrizZonSylwesterOpen2023-15074", - "competition_id":"BrizZonSylwesterOpen2023", - "user_id":"15074", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"444", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333bf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"15074" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"LazarilloOpen2024-158820", - "competition_id":"LazarilloOpen2024", - "user_id":"158820", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"waiting-list" - } - ], - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158820" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"CubingZANationalChampionship2024-158816", - "competition_id":"CubingZANationalChampionship2024", - "user_id":"158816", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - } - ], - "comment":"basic registration comment", - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158816" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"CubingZANationalChampionship2024-158817", - "competition_id":"CubingZANationalChampionship2024", - "user_id":"158817", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - } - ], - "comment":"basic registration comment", - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158816" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"CubingZANationalChampionship2024-158818", - "competition_id":"CubingZANationalChampionship2024", - "user_id":"158818", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"accepted", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"accepted" - } - ], - "comment":"basic registration comment", - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158818" - } - ], - "hide_name_publicly": false - }, - { - "attendee_id":"CubingZANationalChampionship2024-158819", - "competition_id":"CubingZANationalChampionship2024", - "user_id":"158819", - "is_attending":true, - "lanes":[{ - "lane_name":"competing", - "lane_state":"pending", - "completed_steps":[1,2,3], - "lane_details":{ - "event_details":[ - { - "event_id":"333", - "event_cost":"5", - "event_cost_currency":"$", - "event_registration_state":"pending" - }, - { - "event_id":"333mbf", - "event_cost":"10.5", - "event_cost_currency":"$", - "event_registration_state":"pending" - } - ], - "comment":"basic registration comment", - "custom_data": {} - }, - "payment_reference":"PI-1235231", - "payment_amount":"10", - "transaction_currency":"$", - "discount_percentage":"0", - "discount_amount":"0", - "last_action":"created", - "last_action_datetime":"2023-01-01T00:00:00Z", - "last_action_user":"158819" - } - ], - "hide_name_publicly": false - } -] diff --git a/spec/helpers/competition_api_spec.rb b/spec/helpers/competition_api_spec.rb index 914d1b7b..b9db4b50 100644 --- a/spec/helpers/competition_api_spec.rb +++ b/spec/helpers/competition_api_spec.rb @@ -5,7 +5,7 @@ describe CompetitionInfo do context 'competition object' do - competition_json = FactoryBot.build(:competition_details) + competition_json = FactoryBot.build(:competition) # TODO: Refactor tests to use a factory, not explicitly defined JSON describe '#registration_open?' do @@ -22,7 +22,7 @@ it 'false when closed' do # Instantiate a CompetitionInfo object with the sample data - competition_json = FactoryBot.build(:competition_details, registration_opened?: false) + competition_json = FactoryBot.build(:competition, registration_opened?: false) competition_info = CompetitionInfo.new(competition_json) # Call the method being tested @@ -47,7 +47,7 @@ it "PASSING false if the competition doesn't use WCA paymet" do # Instantiate a CompetitionInfo object with the sample data - competition_info = CompetitionInfo.new(FactoryBot.build(:competition_details, using_stripe_payments?: false)) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, using_stripe_payments?: false)) # Call the method being tested result = competition_info.using_wca_payment? diff --git a/spec/models/registration_spec.rb b/spec/models/registration_spec.rb new file mode 100644 index 00000000..dd7de803 --- /dev/null +++ b/spec/models/registration_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Registration do + describe 'validations#is_competing_consistency' do + it 'passes if is_competing is true and status is accepted' do + registration = FactoryBot.build(:registration, registration_status: 'accepted', is_competing: true) + + expect(registration).to be_valid + end + + it 'adds an error if is_competing is true and status is not accepted' do + registration = FactoryBot.build(:registration, registration_status: 'pending', is_competing: true) + + expect(registration).not_to be_valid + expect(registration.errors[:is_competing]).to include('cant be true unless competing_status is accepted') + end + end + + describe '#update_competing_lane!' do + it 'given accepted status, it changes the users status to accepted' do + registration = FactoryBot.create(:registration, registration_status: 'pending') + registration.update_competing_lane!({ status: 'accepted' }) + expect(registration.competing_status).to eq('accepted') + end + + it 'given accepted status, it sets is_competing to true' do + registration = FactoryBot.create(:registration, registration_status: 'pending') + registration.update_competing_lane!({ status: 'accepted' }) + expect(registration.is_competing).to eq(true) + end + + it 'accepted given cancelled, it sets is_competing to false' do + registration = FactoryBot.create(:registration, registration_status: 'accepted') + registration.update_competing_lane!({ status: 'cancelled' }) + expect(registration.is_competing).to eq(false) + end + + it 'accepted given pending, it sets is_competing to false' do + registration = FactoryBot.create(:registration, registration_status: 'accepted') + registration.update_competing_lane!({ status: 'pending' }) + expect(registration.is_competing).to eq(false) + end + + it 'accepted given waiting_list, it sets is_competing to false' do + registration = FactoryBot.create(:registration, registration_status: 'accepted') + registration.update_competing_lane!({ status: 'waiting_list' }) + expect(registration.is_competing).to eq(false) + end + end + + describe '#accepted_competitors' do + it 'returns the number of accepted competitors only for a specific competition' do + target_comp = 'TargetCompId' + FactoryBot.create_list(:registration, 3, registration_status: 'accepted') + FactoryBot.create_list(:registration, 3, registration_status: 'accepted', competition_id: target_comp) + + comp_registration_count = Registration.accepted_competitors(target_comp) + + expect(comp_registration_count).to eq(3) + end + + it 'returns only competitors marked as is_competing' do + target_comp = 'TargetCompId' + FactoryBot.create_list(:registration, 3, registration_status: 'accepted') + FactoryBot.create_list(:registration, 3, registration_status: 'accepted', competition_id: target_comp) + FactoryBot.create_list(:registration, 3, registration_status: 'cancelled', competition_id: target_comp) + + comp_registration_count = Registration.accepted_competitors(target_comp) + + expect(comp_registration_count).to eq(3) + end + end + + describe '#set_is_competing' do + it 'persists a true state to the database' do + registration = FactoryBot.create(:registration, registration_status: 'accepted') + expect(Registration.find(registration.attendee_id).is_competing).to eq(true) + end + + it 'persists a false state to the database' do + registration = FactoryBot.create(:registration, registration_status: 'pending') + expect(Registration.find(registration.attendee_id).is_competing).to eq(nil) + end + end +end diff --git a/spec/requests/cancel_registration_spec.rb b/spec/requests/cancel_registration_spec.rb deleted file mode 100644 index 8f4a2372..00000000 --- a/spec/requests/cancel_registration_spec.rb +++ /dev/null @@ -1,542 +0,0 @@ -# frozen_string_literal: true - -require 'swagger_helper' -require_relative '../../app/helpers/error_codes' - -RSpec.describe 'v1 Registrations API', type: :request, document: false do - include Helpers::RegistrationHelper - - path '/api/v1/register' do - patch 'update or cancel an attendee registration' do - security [Bearer: {}] - consumes 'application/json' - parameter name: :registration_update, in: :body, - schema: { '$ref' => '#/components/schemas/updateRegistrationBody' }, required: true - - produces 'application/json' - - context 'SUCCESS: user registration cancellations' do - # Events can't be updated when cancelling registration - # Refactor the registration status checks into a seaprate functionN? (not sure if this is possible but worth a try) - # # test removing events (I guess this is an udpate?) - # Other fields get left alone when cancelling registration - include_context 'competition information' - include_context 'PATCH payloads' - include_context 'database seed' - include_context 'auth_tokens' - - response '200', 'PASSING cancel accepted registration' do - let(:registration_update) { @cancellation_816 } - let(:Authorization) { @jwt_816 } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING cancel accepted registration, event statuses change to "cancelled"' do - let(:registration_update) { @cancellation_816 } - let(:Authorization) { @jwt_816 } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - updated_registration.event_details.each do |event| - expect(event['event_registration_state']).to eq('cancelled') - end - end - end - - response '200', 'PASSING cancel pending registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_817 } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING cancel update_pending registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_818 } - let(:Authorization) { @jwt_818 } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING cancel waiting_list registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_819 } - let(:Authorization) { @jwt_819 } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING cancel cancelled registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_823 } - let(:Authorization) { @jwt_823 } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - end - - context 'SUCCESS: admin registration cancellations' do - include_context 'PATCH payloads' - include_context 'competition information' - include_context 'database seed' - include_context 'auth_tokens' - - response '200', 'PASSING admin cancels their own registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_073 } - let(:Authorization) { @admin_token } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING admin cancel accepted registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_816 } - let(:Authorization) { @admin_token } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING admin cancel accepted registration with comment' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_816_2 } - let(:Authorization) { @admin_token } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - - expect(updated_registration.admin_comment).to eq(registration_update['competing']['admin_comment']) - end - end - - response '200', 'PASSING admin cancel pending registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_817 } - let(:Authorization) { @admin_token } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING admin cancel update_pending registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_818 } - let(:Authorization) { @admin_token } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING admin cancel waiting_list registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_819 } - let(:Authorization) { @admin_token } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING admin cancel cancelled registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_823 } - let(:Authorization) { @admin_token } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING organizer cancels their own registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_1 } - let(:Authorization) { @organizer_token } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING organizer cancel accepted registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_816 } - let(:Authorization) { @organizer_token } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING organizer cancel accepted registration with comment' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_816_2 } - let(:Authorization) { @organizer_token } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - - expect(updated_registration.admin_comment).to eq(registration_update['competing']['admin_comment']) - end - end - - response '200', 'PASSING organizer cancel pending registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_817 } - let(:Authorization) { @admin_token } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING organizer cancel update_pending registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_818 } - let(:Authorization) { @organizer_token } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING organizer cancel waiting_list registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_819 } - let(:Authorization) { @organizer_token } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - - response '200', 'PASSING organizer cancel cancelled registration' do - # This method is not asynchronous so we're looking for a 200 - let(:registration_update) { @cancellation_823 } - let(:Authorization) { @organizer_token } - - run_test! do |response| - # Make sure body contains the values we expect - body = JSON.parse(response.body) - response_data = body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - expect(response_data['competing']['event_ids']).to eq([]) - expect(response_data['competing']['registration_status']).to eq('cancelled') - - # Make sure the registration stored in the dabatase contains teh values we expect - expect(updated_registration.registered_event_ids).to eq([]) - expect(updated_registration.competing_status).to eq('cancelled') - end - end - end - - context 'FAIL: registration cancellations' do - # xAdd bad competition ID - # Add other fields included - # xAdd bad user ID - include_context 'PATCH payloads' - include_context 'database seed' - include_context 'competition information' - include_context 'auth_tokens' - - response '401', 'PASSING user tries to submit an admin payload' do - error_response = { error: ErrorCodes::USER_INSUFFICIENT_PERMISSIONS }.to_json - let(:registration_update) { @cancellation_816_2 } - let(:Authorization) { @jwt_816 } - - run_test! do |response| - expect(response.body).to eq(error_response) - end - end - - response '401', 'PASSING admin submits cancellation for a comp they arent an admin for' do - # This could return an insufficient permissions error instead if we want to somehow determine who should be an admin - error_response = { error: ErrorCodes::USER_INSUFFICIENT_PERMISSIONS }.to_json - let(:registration_update) { @cancellation_073 } - let(:Authorization) { @organizer_token } - - run_test! do |response| - expect(response.body).to eq(error_response) - end - end - - response '401', 'PASSING user submits a cancellation for a different user' do - error_response = { error: ErrorCodes::USER_INSUFFICIENT_PERMISSIONS }.to_json - let(:registration_update) { @cancellation_816 } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - expect(response.body).to eq(error_response) - end - end - - response '404', 'PASSING cancel on competition that doesnt exist' do - registration_error_json = { error: ErrorCodes::COMPETITION_NOT_FOUND }.to_json - let(:registration_update) { @bad_comp_cancellation } - let(:Authorization) { @jwt_816 } - - run_test! do |reponse| - expect(response.body).to eq(registration_error_json) - end - end - - response '404', 'PASSING cancel on competitor ID that isnt registered' do - registration_error_json = { error: ErrorCodes::REGISTRATION_NOT_FOUND }.to_json - let(:registration_update) { @bad_user_cancellation } - let(:Authorization) { @jwt_800 } - - run_test! do |reponse| - expect(response.body).to eq(registration_error_json) - end - end - - response '422', 'PASSING reject cancel with changed event ids' do - # This test is passing, but the expect/to eq logic is wronng. old_event_ids is showing the updated event ids - registration_error_json = { error: ErrorCodes::INVALID_EVENT_SELECTION }.to_json - let(:registration_update) { @cancellation_with_events } - let(:Authorization) { @jwt_816 } - - # Use separate before/it so that we can read the old event IDs before Registration object is updated - before do |example| - @old_event_ids = Registration.find("#{registration_update['competition_id']}-#{registration_update["user_id"]}").event_ids - @response = submit_request(example.metadata) - end - - it 'returns a 422' do |example| - # run_test! do |response| - body = JSON.parse(response.body) - body['registration'] - - updated_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - # Make sure that event_ids from old and update registration match - expect(updated_registration.event_ids).to eq(@old_event_ids) - assert_response_matches_metadata(example.metadata) - expect(response.body).to eq(registration_error_json) - end - end - end - - # context 'SUCCESS: registration updates' do - # end - end - end -end diff --git a/spec/requests/competition_api_spec.rb b/spec/requests/competition_api_spec.rb deleted file mode 100644 index c2e890ab..00000000 --- a/spec/requests/competition_api_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require_relative '../../app/helpers/competition_api' - -describe CompetitionInfo do - describe 'Initializing object' do - it 'PASSING Initializes successfully with a valid competition_id' do - competition = FactoryBot.build(:competition_details) - stub_request(:get, comp_api_url(competition[:competition_id])).to_return(status: 200, body: competition.to_json) - - competition_info = CompetitionApi.find!(competition[:competition_id]) - - expect(competition_info.class).to eq(CompetitionInfo) - end - - it 'PASSING errors when competition_id doesnt exist' do - wca_error_json = { error: 'Competition with id InvalidCompId not found' }.to_json - stub_request(:get, comp_api_url('InvalidCompId')).to_return(status: 404, body: wca_error_json) - - expect { - CompetitionApi.find!('InvalidCompId') - }.to raise_error(RegistrationError) { |error| - expect(error.error).to eq(ErrorCodes::COMPETITION_NOT_FOUND) - expect(error.http_status).to eq(404) - } - end - - it 'PASSING errors when comp api unavailable' do - error_json = { error: 'Internal Server Error for url: /api/v0/competitions/UnavailableComp' }.to_json - stub_request(:get, comp_api_url('UnavailableComp')).to_return(status: 500, body: error_json) - - expect { - CompetitionApi.find!('UnavailableComp') - }.to raise_error(RegistrationError) { |error| - expect(error.error).to eq(ErrorCodes::COMPETITION_API_5XX) - expect(error.http_status).to eq(500) - } - end - end -end diff --git a/spec/requests/get_registrations_spec.rb b/spec/requests/get_registrations_spec.rb deleted file mode 100644 index 1bef9707..00000000 --- a/spec/requests/get_registrations_spec.rb +++ /dev/null @@ -1,181 +0,0 @@ -# frozen_string_literal: true - -require 'swagger_helper' -require_relative '../support/registration_spec_helper' -require_relative '../../app/helpers/error_codes' - -# TODO: Add case where registration for the competition hasn't opened yet, but the competition exists - should return empty list -# FINN TODO: Why doesn't list_admin call competition API? Should it? -# TODO: Check Swaggerized output -# TODO: Brainstorm other tests that could be included -RSpec.describe 'v1 Registrations API', type: :request do - include Helpers::RegistrationHelper - - path '/api/v1/registrations/{competition_id}' do - get 'Public: list registrations for a given competition_id' do - parameter name: :competition_id, in: :path, type: :string, required: true - produces 'application/json' - - context '-> success responses' do - include_context 'competition information' - include_context 'database seed' - - response '200', '-> PASSING request and response conform to schema' do - schema type: :array, items: { '$ref' => '#/components/schemas/registration' } - - let!(:competition_id) { @attending_registrations_only } - - run_test! do |response| - body = JSON.parse(response.body) - expect(body.length).to eq(4) - end - end - - response '200', ' -> PASSING only returns attending registrations' do # waiting_list are being counted as is_attending - not sure how this is set? maybe in the model logic? - let!(:competition_id) { @includes_non_attending_registrations } - - run_test! do |response| - body = JSON.parse(response.body) - expect(body.length).to eq(2) - end - end - - response '200', ' -> PASSING Valid competition_id but no registrations for it' do - let!(:competition_id) { @empty_comp } - - run_test! do |response| - body = JSON.parse(response.body) - expect(body).to eq([]) - end - end - - context 'Competition service down (500) but registrations exist' do - response '200', ' -> PASSING comp service down but registrations exist' do - let!(:competition_id) { @registrations_exist_comp_500 } - - run_test! do |response| - body = JSON.parse(response.body) - expect(body.length).to eq(3) - end - end - end - - context 'Competition service down (502) but registrations exist' do - response '200', ' -> PASSING comp service down but registrations exist' do - let!(:competition_id) { @registrations_exist_comp_502 } - - run_test! do |response| - body = JSON.parse(response.body) - expect(body.length).to eq(2) - end - end - end - end - end - end - - path '/api/v1/registrations/{competition_id}/admin' do - get 'Public: list registrations for a given competition_id' do - security [Bearer: {}] - parameter name: :competition_id, in: :path, type: :string, required: true - produces 'application/json' - - context 'success responses' do - include_context 'competition information' - include_context 'database seed' - include_context 'auth_tokens' - - response '200', ' -> PASSING request and response conform to schema' do - schema type: :array, items: { '$ref' => '#/components/schemas/registrationAdmin' } - - let!(:competition_id) { @attending_registrations_only } - let(:Authorization) { @admin_token } - - run_test! do |response| - body = JSON.parse(response.body) - expect(body.length).to eq(4) - end - end - - response '200', ' -> PASSING admin registration endpoint returns registrations in all states' do - let!(:competition_id) { @includes_non_attending_registrations } - let(:Authorization) { @admin_token } - - run_test! do |response| - body = JSON.parse(response.body) - expect(body.length).to eq(6) - end - end - - response '200', ' -> PASSING organizer can access admin list for their competition' do - let!(:competition_id) { @includes_non_attending_registrations } - let(:Authorization) { @organizer_token } - - run_test! do |response| - body = JSON.parse(response.body) - expect(body.length).to eq(6) - end - end - - context 'user has comp-specific auth for multiple comps' do - response '200', ' -> PASSING organizer has access to comp 1' do - let!(:competition_id) { @includes_non_attending_registrations } - let(:Authorization) { @multi_comp_organizer_token } - - run_test! do |response| - body = JSON.parse(response.body) - expect(body.length).to eq(6) - end - end - - response '200', ' -> PASSING organizer has access to comp 2' do - let!(:competition_id) { @attending_registrations_only } - let(:Authorization) { @multi_comp_organizer_token } - - run_test! do |response| - body = JSON.parse(response.body) - expect(body.length).to eq(4) - end - end - end - end - - context 'fail responses' do - include_context 'competition information' - include_context 'database seed' - include_context 'auth_tokens' - - response '401', ' -> PASSING Attending user cannot get admin registration list' do - schema '$ref' => '#/components/schemas/error_response' - registration_error_json = { error: ErrorCodes::USER_INSUFFICIENT_PERMISSIONS }.to_json - let!(:competition_id) { @attending_registrations_only } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '401', ' -> PASSING organizer cannot access registrations for comps they arent organizing - single comp auth' do - registration_error_json = { error: ErrorCodes::USER_INSUFFICIENT_PERMISSIONS }.to_json - let!(:competition_id) { @attending_registrations_only } - let(:Authorization) { @organizer_token } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '401', ' -> PASSING organizer cannot access registrations for comps they arent organizing - multi comp auth' do - registration_error_json = { error: ErrorCodes::USER_INSUFFICIENT_PERMISSIONS }.to_json - let!(:competition_id) { @registrations_exist_comp_500 } - let(:Authorization) { @multi_comp_organizer_token } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - end - end - end -end diff --git a/spec/requests/post_attendee_spec.rb b/spec/requests/post_attendee_spec.rb deleted file mode 100644 index dba279df..00000000 --- a/spec/requests/post_attendee_spec.rb +++ /dev/null @@ -1,248 +0,0 @@ -# frozen_string_literal: true - -require 'swagger_helper' -require_relative '../support/registration_spec_helper' - -# TODO: Submits registration at guest limit -# TODO: Submits comment at character limit -# TODO: Submits comment over character limit -# TODO: Validate expected vs actual output -# TODO: Add test cases for various JWT token error codes -# TODO: Add test cases for competition API (new file) -# TODO: Add test cases for users API (new file) -# TODO: Add test cases for competition info being returned from endpoint (check that we respond appropriately to different values/conditionals) -# TODO: Check Swaggerized output -RSpec.describe 'v1 Registrations API', type: :request, document: false do - include Helpers::RegistrationHelper - - path '/api/v1/register' do - post 'Add an attendee registration' do - security [Bearer: {}] - consumes 'application/json' - parameter name: :registration, in: :body, - schema: { '$ref' => '#/components/schemas/submitRegistrationBody' }, required: true - produces 'application/json' - - context '-> success registration posts' do - # include_context 'database seed' - # include_context 'auth_tokens' - # include_context 'registration_data' - include_context 'competition information' - - response '202', '-> PASSING competitor submits basic registration', document: true do - schema '$ref' => '#/components/schemas/success_response' - registration = FactoryBot.build(:registration) - let!(:registration) { registration } - let(:Authorization) { registration[:jwt_token] } - - run_test! do |response| - assert_requested :get, "#{@base_comp_url}#{@includes_non_attending_registrations}", times: 1 - end - end - - response '202', '-> PASSING admin registers before registration opens' do - registration = FactoryBot.build(:admin, events: ['444', '333bf'], competition_id: 'BrizZonSylwesterOpen2023') - let(:registration) { registration } - let(:Authorization) { registration[:jwt_token] } - - run_test! do |response| - # TODO: Do a better assertion here - assert_requested :get, "#{@base_comp_url}#{@registrations_not_open}", times: 1 - end - end - - response '202', '-> PASSING admin submits registration for competitor' do - registration = FactoryBot.build(:admin_submits) - let(:registration) { registration } - let(:Authorization) { registration[:jwt_token] } - - run_test! do |response| - assert_requested :get, "#{@base_comp_url}#{@includes_non_attending_registrations}", times: 1 - end - end - end - - # TODO: competitor does not meet qualification requirements - will need to mock users service for this? - investigate what the monolith currently does and replicate that - context 'fail registration posts, from USER' do - include_context 'database seed' - include_context 'auth_tokens' - include_context 'registration_data' - include_context 'competition information' - - response '401', ' -> PASSING user impersonation (no admin permission, JWT token user_id does not match registration user_id)', document: true do - schema '$ref' => '#/components/schemas/error_response' - registration_error_json = { error: ErrorCodes::USER_INSUFFICIENT_PERMISSIONS }.to_json - let(:registration) { @required_fields_only } - let(:Authorization) { @jwt_200 } - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '422', 'PASSING user registration exceeds guest limit', document: true do - schema '$ref' => '#/components/schemas/error_response' - registration_error_json = { error: ErrorCodes::GUEST_LIMIT_EXCEEDED }.to_json - let(:registration) { @too_many_guests } - let(:Authorization) { @jwt_824 } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '403', ' -> PASSING user cant register while registration is closed', document: true do - schema '$ref' => '#/components/schemas/error_response' - registration_error_json = { error: ErrorCodes::REGISTRATION_CLOSED }.to_json - let(:registration) { @comp_not_open } - let(:Authorization) { @jwt_817 } - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '401', '-> PASSING attendee is banned' do - registration_error_json = { error: ErrorCodes::USER_PROFILE_INCOMPLETE }.to_json - let(:registration) { @banned_user_reg } - let(:Authorization) { @banned_user_jwt } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '401', '-> PASSING competitor has incomplete profile' do - registration_error_json = { error: ErrorCodes::USER_PROFILE_INCOMPLETE }.to_json - let(:registration) { @incomplete_user_reg } - let(:Authorization) { @incomplete_user_jwt } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '422', '-> PASSING contains event IDs which are not held at competition' do - registration_error_json = { error: ErrorCodes::INVALID_EVENT_SELECTION }.to_json - let(:registration) { @events_not_held_reg } - let(:Authorization) { @jwt_201 } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '422', '-> PASSING contains event IDs which are not held at competition' do - registration_error_json = { error: ErrorCodes::INVALID_EVENT_SELECTION }.to_json - let(:registration) { @events_not_exist_reg } - let(:Authorization) { @jwt_202 } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '400', ' -> PASSING empty payload provided', document: true do - schema '$ref' => '#/components/schemas/error_response' - registration_error_json = { error: ErrorCodes::INVALID_REQUEST_DATA }.to_json - let(:registration) { @empty_payload } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '404', ' -> PASSING competition does not exist', document: true do - schema '$ref' => '#/components/schemas/error_response' - registration_error_json = { error: ErrorCodes::COMPETITION_NOT_FOUND }.to_json - let(:registration) { @bad_comp_name } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - end - - context 'fail registration posts, from ADMIN' do - # TODO: What is the difference between admin and organizer permissions? Should we add organizer test as well? - # FAIL CASES TO IMPLEMENT: - # convert all existing cases - # user has insufficient permissions (admin of different comp trying to add reg) - - include_context 'database seed' - include_context 'auth_tokens' - include_context 'registration_data' - include_context 'competition information' - - response '403', ' -> PASSING comp not open, admin adds another user' do - registration_error_json = { error: ErrorCodes::REGISTRATION_CLOSED }.to_json - let(:registration) { @comp_not_open } - let(:Authorization) { @admin_token } - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '401', '-> PASSING admin adds banned user' do - registration_error_json = { error: ErrorCodes::USER_PROFILE_INCOMPLETE }.to_json - let(:registration) { @banned_user_reg } - let(:Authorization) { @admin_token } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '401', '-> PASSING admin adds competitor who has incomplete profile' do - registration_error_json = { error: ErrorCodes::USER_PROFILE_INCOMPLETE }.to_json - let(:registration) { @incomplete_user_reg } - let(:Authorization) { @admin_token } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '422', '-> PASSING admins add other user reg which contains event IDs which are not held at competition' do - registration_error_json = { error: ErrorCodes::INVALID_EVENT_SELECTION }.to_json - let(:registration) { @events_not_held_reg } - let(:Authorization) { @admin_token } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '422', '-> PASSING admin adds reg for user which contains event IDs which do not exist' do - registration_error_json = { error: ErrorCodes::INVALID_EVENT_SELECTION }.to_json - let(:registration) { @events_not_exist_reg } - let(:Authorization) { @jwt_202 } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '400', ' -> PASSING admin adds registration with empty payload provided' do # getting a long error on this - not sure why it fails - registration_error_json = { error: ErrorCodes::INVALID_REQUEST_DATA }.to_json - let(:registration) { @empty_payload } - let(:Authorization) { @admin_token } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - - response '404', ' -> PASSING admin adds reg for competition which does not exist' do - registration_error_json = { error: ErrorCodes::COMPETITION_NOT_FOUND }.to_json - let(:registration) { @bad_comp_name } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - expect(response.body).to eq(registration_error_json) - end - end - end - end - end -end diff --git a/spec/requests/update_registration_spec.rb b/spec/requests/update_registration_spec.rb deleted file mode 100644 index 172a9f94..00000000 --- a/spec/requests/update_registration_spec.rb +++ /dev/null @@ -1,417 +0,0 @@ -# frozen_string_literal: true - -require 'swagger_helper' -require_relative '../../app/helpers/error_codes' - -RSpec.describe 'v1 Registrations API', type: :request, document: false do - include Helpers::RegistrationHelper - - path '/api/v1/register' do - patch 'update or cancel an attendee registration' do - security [Bearer: {}] - consumes 'application/json' - parameter name: :registration_update, in: :body, - schema: { '$ref' => '#/components/schemas/updateRegistrationBody' }, required: true - - produces 'application/json' - - context 'USER successful update requests' do - include_context 'competition information' - include_context 'PATCH payloads' - include_context 'database seed' - include_context 'auth_tokens' - - response '200', 'PASSING user passes empty event_ids - with cancelled status' do - let(:registration_update) { @events_update_5 } - let(:Authorization) { @jwt_817 } - - run_test! - end - - response '200', 'PASSING user changes comment', document: true do - schema type: :object, - properties: { - status: { type: :string }, - registration: { '$ref' => '#/components/schemas/registrationAdmin' }, - } - let(:registration_update) { @comment_update } - let(:Authorization) { @jwt_816 } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - expect(target_registration.competing_comment).to eq('updated registration comment') - end - end - - response '200', 'PASSING user adds comment to reg with no comment' do - let(:registration_update) { @comment_update_2 } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - expect(target_registration.competing_comment).to eq('updated registration comment - had no comment before') - end - end - - response '200', 'PASSING user adds guests, none existed before' do - let(:registration_update) { @guest_update_1 } - let(:Authorization) { @jwt_816 } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - expect(target_registration.competing_guests).to eq(2) - end - end - - response '200', 'PASSING user changes number of guests' do - let(:registration_update) { @guest_update_2 } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - expect(target_registration.competing_guests).to eq(2) - end - end - - response '200', 'PASSING user adds events: events list updates' do - let(:registration_update) { @events_update_1 } - let(:Authorization) { @jwt_816 } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - expect(target_registration.event_ids).to eq(registration_update['competing']['event_ids']) - end - end - - response '200', 'PASSING user removes events: events list updates' do - let(:registration_update) { @events_update_2 } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - expect(target_registration.event_ids).to eq(registration_update['competing']['event_ids']) - end - end - - response '200', 'PASSING user adds events: statuses update' do - let(:registration_update) { @events_update_1 } - let(:Authorization) { @jwt_816 } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - event_details = target_registration.event_details - registration_status = target_registration.competing_status - event_details.each do |event| - puts "event: #{event}" - expect(event['event_registration_state']).to eq(registration_status) - end - end - end - - response '200', 'PASSING user removes events: statuses update' do - let(:registration_update) { @events_update_2 } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - - event_details = target_registration.event_details - registration_status = target_registration.competing_status - event_details.each do |event| - puts "event: #{event}" - expect(event['event_registration_state']).to eq(registration_status) - end - end - end - end - - context 'ADMIN successful update requests' do - # Note that delete/cancel tests are handled in cancel_registration_spec.rb - include_context 'competition information' - include_context 'PATCH payloads' - include_context 'database seed' - include_context 'auth_tokens' - - response '200', 'PASSING admin state pending -> accepted' do - let(:registration_update) { @pending_update_1 } - let(:Authorization) { @admin_token } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - competing_status = target_registration.competing_status - event_details = target_registration.event_details - - # Check competing status is correct - expect(competing_status).to eq('accepted') - - # Check that event states are correct - event_details.each do |event| - expect(event['event_registration_state']).to eq('accepted') - end - end - end - - response '200', 'PASSING admin state pending -> waiting_list' do - let(:registration_update) { @pending_update_2 } - let(:Authorization) { @admin_token } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - competing_status = target_registration.competing_status - event_details = target_registration.event_details - - # Check competing status is correct - expect(competing_status).to eq('waiting_list') - - # Check that event states are correct - event_details.each do |event| - expect(event['event_registration_state']).to eq('waiting_list') - end - end - end - - response '200', 'PASSING admin state waiting_list -> accepted' do - let(:registration_update) { @waiting_update_1 } - let(:Authorization) { @admin_token } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - competing_status = target_registration.competing_status - event_details = target_registration.event_details - - # Check competing status is correct - expect(competing_status).to eq('accepted') - - # Check that event states are correct - event_details.each do |event| - expect(event['event_registration_state']).to eq('accepted') - end - end - end - - response '200', 'PASSING admin state waiting_list -> pending' do - let(:registration_update) { @waiting_update_2 } - let(:Authorization) { @admin_token } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - competing_status = target_registration.competing_status - event_details = target_registration.event_details - - # Check competing status is correct - expect(competing_status).to eq('pending') - - # Check that event states are correct - event_details.each do |event| - expect(event['event_registration_state']).to eq('pending') - end - end - end - - response '200', 'PASSING admin state accepted -> pending' do - let(:registration_update) { @accepted_update_1 } - let(:Authorization) { @admin_token } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - competing_status = target_registration.competing_status - event_details = target_registration.event_details - - # Check competing status is correct - expect(competing_status).to eq('pending') - - # Check that event states are correct - event_details.each do |event| - expect(event['event_registration_state']).to eq('pending') - end - end - end - - response '200', 'PASSING admin state accepted -> waiting_list' do - let(:registration_update) { @accepted_update_2 } - let(:Authorization) { @admin_token } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - competing_status = target_registration.competing_status - event_details = target_registration.event_details - - # Check competing status is correct - expect(competing_status).to eq('waiting_list') - - # Check that event states are correct - event_details.each do |event| - expect(event['event_registration_state']).to eq('waiting_list') - end - end - end - end - - context 'USER failed update requests' do - include_context 'competition information' - include_context 'PATCH payloads' - include_context 'database seed' - include_context 'auth_tokens' - - response '422', 'PASSING user does not include required comment', document: true do - schema '$ref' => '#/components/schemas/error_response' - registration_error = { error: ErrorCodes::REQUIRED_COMMENT_MISSING }.to_json - let(:registration_update) { @comment_update_4 } - let(:Authorization) { @jwt_820 } - - run_test! do |response| - expect(response.body).to eq(registration_error) - end - end - - response '422', 'PASSING user submits more guests than allowed' do - registration_error = { error: ErrorCodes::GUEST_LIMIT_EXCEEDED }.to_json - let(:registration_update) { @guest_update_3 } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - expect(response.body).to eq(registration_error) - end - end - - response '422', 'PASSING user submits longer comment than allowed' do - registration_error = { error: ErrorCodes::USER_COMMENT_TOO_LONG }.to_json - let(:registration_update) { @comment_update_3 } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - expect(response.body).to eq(registration_error) - end - end - - response '422', 'PASSING user removes all events - no status provided' do - registration_error = { error: ErrorCodes::INVALID_EVENT_SELECTION }.to_json - let(:registration_update) { @events_update_3 } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - expect(response.body).to eq(registration_error) - end - end - - response '422', 'PASSING user adds events which arent present' do - registration_error = { error: ErrorCodes::INVALID_EVENT_SELECTION }.to_json - let(:registration_update) { @events_update_6 } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - expect(response.body).to eq(registration_error) - end - end - - response '422', 'PASSING user adds events which dont exist' do - registration_error = { error: ErrorCodes::INVALID_EVENT_SELECTION }.to_json - let(:registration_update) { @events_update_7 } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - expect(response.body).to eq(registration_error) - end - end - - response '401', 'PASSING user requests invalid status change to their own reg', document: true do - schema '$ref' => '#/components/schemas/error_response' - registration_error = { error: ErrorCodes::USER_INSUFFICIENT_PERMISSIONS }.to_json - let(:registration_update) { @pending_update_1 } - let(:Authorization) { @jwt_817 } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - competing_status = target_registration.competing_status - event_details = target_registration.event_details - - # Check error message - expect(response.body).to eq(registration_error) - - # Check competing status is correct - expect(competing_status).to eq('pending') - - # Check that event states are correct - event_details.each do |event| - expect(event['event_registration_state']).to eq('pending') - end - end - end - - response '401', 'PASSING user requests status change to someone elses reg' do - registration_error = { error: ErrorCodes::USER_INSUFFICIENT_PERMISSIONS }.to_json - let(:registration_update) { @pending_update_1 } - let(:Authorization) { @jwt_816 } - - run_test! do |response| - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - competing_status = target_registration.competing_status - event_details = target_registration.event_details - - # Check error message - expect(response.body).to eq(registration_error) - - # Check competing status is correct - expect(competing_status).to eq('pending') - - # Check that event states are correct - event_details.each do |event| - expect(event['event_registration_state']).to eq('pending') - end - end - end - - response '403', 'PASSING user changes events / other stuff past deadline', document: true do - schema '$ref' => '#/components/schemas/error_response' - registration_error = { error: ErrorCodes::EVENT_EDIT_DEADLINE_PASSED }.to_json - let(:registration_update) { @delayed_update_1 } - let(:Authorization) { @jwt_820 } - - run_test! do |response| - expect(response.body).to eq(registration_error) - end - end - end - - context 'ADMIN failed update requests' do - include_context 'competition information' - include_context 'PATCH payloads' - include_context 'database seed' - include_context 'auth_tokens' - - response '422', 'PASSING admin changes to status which doesnt exist' do - let(:registration_update) { @invalid_status_update } - let(:Authorization) { @admin_token } - registration_error = { error: ErrorCodes::INVALID_REQUEST_DATA }.to_json - - run_test! do |response| - expect(response.body).to eq(registration_error) - end - end - - response '403', 'PASSING admin cannot advance state when registration full' do - registration_error = { error: ErrorCodes::COMPETITOR_LIMIT_REACHED }.to_json - let(:registration_update) { @pending_update_3 } - let(:Authorization) { @admin_token } - - run_test! do |response| - expect(response.body).to eq(registration_error) - - target_registration = Registration.find("#{registration_update['competition_id']}-#{registration_update['user_id']}") - competing_status = target_registration.competing_status - event_details = target_registration.event_details - - # Check competing status is correct - expect(competing_status).to eq('pending') - - # Check that event states are correct - event_details.each do |event| - expect(event['event_registration_state']).to eq('pending') - end - end - end - end - end - end -end diff --git a/spec/services/registration_checker_spec.rb b/spec/services/registration_checker_spec.rb new file mode 100644 index 00000000..aad6f6fc --- /dev/null +++ b/spec/services/registration_checker_spec.rb @@ -0,0 +1,917 @@ +# frozen_string_literal: true + +require 'rails_helper' +require_relative '../../app/helpers/competition_api' + +# Add a test where one comp has a lot of competitors and another doesnt but you can still accept, to ensure that we're checking the reg count +# for the COMPETITION, not all registrations + +RSpec.shared_examples 'invalid user status updates' do |old_status, new_status| + it "user cant change 'status' => #{old_status} to: #{new_status}" do + registration = FactoryBot.create(:registration, registration_status: old_status) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'status' => new_status }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unauthorized) + expect(error.error).to eq(ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) + end + end +end + +RSpec.shared_examples 'valid organizer status updates' do |old_status, new_status| + it "admin can change 'status' => #{old_status} to: #{new_status} before close" do + registration = FactoryBot.create(:registration, registration_status: old_status) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'status' => new_status }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it "after edit deadline/reg close, organizer can change 'status' => #{old_status} to: #{new_status}" do + registration = FactoryBot.create(:registration, registration_status: old_status) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :closed)) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'status' => new_status }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end +end + +describe RegistrationChecker do + describe '#create_registration_allowed!' do + it 'user can create a registration' do + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + registration_request = FactoryBot.build(:registration_request) + + expect { RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) } + .not_to raise_error + end + + it 'users can only register for themselves' do + registration_request = FactoryBot.build(:registration_request, :impersonation) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unauthorized) + expect(error.error).to eq(ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) + end + end + + it 'user cant register if registration is closed' do + registration_request = FactoryBot.build(:registration_request) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :closed)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:forbidden) + expect(error.error).to eq(ErrorCodes::REGISTRATION_CLOSED) + end + end + + it 'organizers can register before registration opens' do + registration_request = FactoryBot.build(:registration_request, :organizer) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :closed)) + + expect { RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) } + .not_to raise_error + end + + it 'organizers can create registrations for users' do + registration_request = FactoryBot.build(:registration_request, :organizer_submits) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) } + .not_to raise_error + end + + it 'organizers cant register another user before registration opens' do + registration_request = FactoryBot.build(:registration_request, :organizer_submits) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :closed)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:forbidden) + expect(error.error).to eq(ErrorCodes::REGISTRATION_CLOSED) + end + end + + it 'banned user cant register' do + registration_request = FactoryBot.build(:registration_request, :banned) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unauthorized) + expect(error.error).to eq(ErrorCodes::USER_CANNOT_COMPETE) + end + end + + it 'user with incomplete profile cant register' do + registration_request = FactoryBot.build(:registration_request, :incomplete) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unauthorized) + expect(error.error).to eq(ErrorCodes::USER_CANNOT_COMPETE) + end + end + + it 'organizer cant register a banned user' do + registration_request = FactoryBot.build(:registration_request, :banned, :organizer_submits) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unauthorized) + expect(error.error).to eq(ErrorCodes::USER_CANNOT_COMPETE) + end + end + + it 'organizer cant register an incomplete user' do + registration_request = FactoryBot.build(:registration_request, :incomplete, :organizer_submits) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unauthorized) + expect(error.error).to eq(ErrorCodes::USER_CANNOT_COMPETE) + end + end + + it 'doesnt leak data if user tries to register for a banned user' do + registration_request = FactoryBot.build(:registration_request, :banned, :impersonation) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unauthorized) + expect(error.error).to eq(ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) + end + end + + it 'doesnt leak data if organizer tries to register for a banned user' do + registration_request = FactoryBot.build(:registration_request, :incomplete, :impersonation) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unauthorized) + expect(error.error).to eq(ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) + end + end + + it 'user must have events selected' do + registration_request = FactoryBot.build(:registration_request, events: []) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::INVALID_EVENT_SELECTION) + end + end + + it 'events must be held at the competition' do + registration_request = FactoryBot.build(:registration_request, events: ['333', '333fm']) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::INVALID_EVENT_SELECTION) + end + end + + it 'guests can equal the maximum allowed' do + registration_request = FactoryBot.build(:registration_request, guests: 2) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) } + .not_to raise_error + end + + it 'guests may equal 0' do + registration_request = FactoryBot.build(:registration_request, guests: 0) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) } + .not_to raise_error + end + + it 'guests cant exceed 0 if not allowed' do + registration_request = FactoryBot.build(:registration_request, guests: 2) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, guests_per_registration_limit: 0)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::GUEST_LIMIT_EXCEEDED) + end + end + + it 'guests cannot exceed the maximum allowed' do + registration_request = FactoryBot.build(:registration_request, guests: 3) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::GUEST_LIMIT_EXCEEDED) + end + end + + it 'guests cannot be negative' do + registration_request = FactoryBot.build(:registration_request, guests: -1) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::INVALID_REQUEST_DATA) + end + end + + it 'comment cant exceed character limit' do + long_comment = 'comment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer + than 240 characterscomment longer than 240 characters' + + registration_request = FactoryBot.build(:registration_request, :comment, raw_comment: long_comment) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::USER_COMMENT_TOO_LONG) + end + end + + it 'comment can match character limit' do + at_character_limit = 'comment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than' \ + '240 characterscomment longer longer than 240 12345' + + registration_request = FactoryBot.build(:registration_request, :comment, raw_comment: at_character_limit) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) } + .not_to raise_error + end + + it 'comment can be blank' do + comment = '' + registration_request = FactoryBot.build(:registration_request, :comment, raw_comment: comment) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + + expect { RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) } + .not_to raise_error + end + + it 'comment must be included if required' do + registration_request = FactoryBot.build(:registration_request) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, force_comment_in_registration: true)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::REQUIRED_COMMENT_MISSING) + end + end + + it 'comment cant be blank if required' do + registration_request = FactoryBot.build(:registration_request, :comment, raw_comment: '') + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, force_comment_in_registration: true)) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::REQUIRED_COMMENT_MISSING) + end + end + end + + describe '#update_registration_allowed!.user_can_modify_registration!' do + it 'user can change their registration' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id]) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'User A cant change User Bs registration' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, :for_another_user, user_id: registration[:user_id]) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unauthorized) + expect(error.error).to eq(ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) + end + end + + it 'user cant update registration if registration edits arent allowed' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, allow_registration_edits: false)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id]) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:forbidden) + expect(error.error).to eq(ErrorCodes::USER_EDITS_NOT_ALLOWED) + end + end + + it 'user cant change events after event change deadline' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :event_change_deadline_passed)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'event_ids' => ['333', '444', '555'] }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:forbidden) + expect(error.error).to eq(ErrorCodes::USER_EDITS_NOT_ALLOWED) + end + end + + it 'organizer can change user registration' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id]) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'organizer can change registration after change deadline' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :event_change_deadline_passed)) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'comment' => 'this is a new comment' }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + end + + describe '#update_registration_allowed!.validate_comment!' do + it 'user can change comment' do + registration = FactoryBot.create(:registration, 'comment' => 'old comment') + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'comment' => 'new comment' }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'user cant exceed comment length' do + long_comment = 'comment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer + than 240 characterscomment longer than 240 characters' + + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'comment' => long_comment }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::USER_COMMENT_TOO_LONG) + end + end + + it 'user can match comment length' do + at_character_limit = 'comment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than' \ + '240 characterscomment longer longer than 240 12345' + + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'comment' => at_character_limit }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'comment can be blank' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'comment' => '' }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'comment cant be blank if required' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, force_comment_in_registration: true)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'comment' => '' }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::REQUIRED_COMMENT_MISSING) + end + end + + it 'organizer can change user comment' do + registration = FactoryBot.create(:registration, 'comment' => 'original comment') + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'comment' => '' }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'organizer cant exceed comment length' do + long_comment = 'comment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer + than 240 characterscomment longer than 240 characters' + + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'comment' => long_comment }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::USER_COMMENT_TOO_LONG) + end + end + + it 'user cant change comment after edit events deadline' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :event_change_deadline_passed)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'comment' => 'this is a new comment' }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:forbidden) + expect(error.error).to eq(ErrorCodes::USER_EDITS_NOT_ALLOWED) + end + end + end + + describe '#update_registration_allowed!.validate_organizer_fields!' do + it 'organizer can add organizer_comment' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'organizer_comment' => 'this is an admin comment' }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'organizer can change organizer_comment' do + registration = FactoryBot.create(:registration, 'organizer_comment' => 'old admin comment') + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'organizer_comment' => 'new admin comment' }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'user cant submit an organizer comment' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'organizer_comment' => 'new admin comment' }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unauthorized) + expect(error.error).to eq(ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) + end + end + end + + describe '#update_registration_allowed!.validate_organizer_comment!' do + it 'organizer comment cant exceed 240 characters' do + long_comment = 'comment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer + than 240 characterscomment longer than 240 characters' + + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'organizer_comment' => long_comment }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::USER_COMMENT_TOO_LONG) + end + end + + it 'organizer comment can match 240 characters' do + at_character_limit = 'comment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than 240 characterscomment longer than' \ + '240 characterscomment longer longer than 240 12345' + + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'organizer_comment' => at_character_limit }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + end + + describe '#update_registration_allowed!.validate_guests!' do + it 'user can change number of guests' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], guests: 2) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'guests cant exceed guest limit' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], guests: 3) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::GUEST_LIMIT_EXCEEDED) + end + end + + it 'guests can match guest limit' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], guests: 2) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'guests can be zero' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], guests: 0) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'guests cant be negative' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], guests: -1) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::INVALID_REQUEST_DATA) + end + end + + it 'guests have no limit if guest limit not set' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :no_guest_limit)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], guests: 99) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'organizer can change number of guests' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], guests: 2) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'User A cant change User Bs guests' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, :for_another_user, user_id: registration[:user_id], guests: 2) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unauthorized) + expect(error.error).to eq(ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) + end + end + + it 'user cant change guests after registration change deadline' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, event_change_deadline_date: '2022-06-14T00:00:00.000Z')) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], guests: 2) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:forbidden) + expect(error.error).to eq(ErrorCodes::USER_EDITS_NOT_ALLOWED) + end + end + + it 'organizer can change guests after registration change deadline' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, event_change_deadline_date: '2022-06-14T00:00:00.000Z')) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], guests: 2) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + end + + describe '#update_registration_allowed!.validate_update_status!' do + it 'user cant submit an invalid status' do + registration = FactoryBot.create(:registration, registration_status: 'waiting_list') + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'status' => 'random_status' }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::INVALID_REQUEST_DATA) + end + end + + it 'organizer cant submit an invalid status' do + registration = FactoryBot.create(:registration, registration_status: 'waiting_list') + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, :organizer_as_user, user_id: registration[:user_id], competing: { 'status' => 'random_status' }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::INVALID_REQUEST_DATA) + end + end + + it 'organizer cant accept a user when registration list is full' do + FactoryBot.create_list(:registration, 3, registration_status: 'accepted') + registration = FactoryBot.create(:registration, registration_status: 'waiting_list') + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, competitor_limit: 3)) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'status' => 'accepted' }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.error).to eq(ErrorCodes::COMPETITOR_LIMIT_REACHED) + expect(error.http_status).to eq(:forbidden) + end + end + + it 'organizer can accept registrations up to the limit' do + FactoryBot.create_list(:registration, 2, registration_status: 'accepted') + registration = FactoryBot.create(:registration, registration_status: 'waiting_list') + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, competitor_limit: 3)) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'status' => 'accepted' }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'user can change state to cancelled' do + registration = FactoryBot.create(:registration, registration_status: 'waiting_list') + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'status' => 'cancelled' }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'user cant change events when cancelling' do + registration = FactoryBot.create(:registration, registration_status: 'waiting_list') + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build( + :update_request, user_id: registration[:user_id], competing: { 'status' => 'cancelled', 'event_ids' => ['333'] } + ) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::INVALID_REQUEST_DATA) + end + end + + it 'user can change state from cancelled to pending' do + registration = FactoryBot.create(:registration, registration_status: 'cancelled') + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'status' => 'pending' }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + [ + { old_status: 'pending', new_status: 'accepted' }, + { old_status: 'pending', new_status: 'waiting_list' }, + { old_status: 'pending', new_status: 'pending' }, + { old_status: 'waiting_list', new_status: 'pending' }, + { old_status: 'waiting_list', new_status: 'waiting_list' }, + { old_status: 'waiting_list', new_status: 'accepted' }, + { old_status: 'accepted', new_status: 'pending' }, + { old_status: 'accepted', new_status: 'waiting_list' }, + { old_status: 'accepted', new_status: 'accepted' }, + { old_status: 'cancelled', new_status: 'accepted' }, + { old_status: 'cancelled', new_status: 'waiting_list' }, + ].each do |params| + it_behaves_like 'invalid user status updates', params[:old_status], params[:new_status] + end + + it 'user cant cancel accepted registration if competition requires organizers to cancel registration' do + registration = FactoryBot.create(:registration, registration_status: 'accepted') + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, allow_registration_self_delete_after_acceptance: false)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'status' => 'cancelled' }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unauthorized) + expect(error.error).to eq(ErrorCodes::ORGANIZER_MUST_CANCEL_REGISTRATION) + end + end + + it 'user can cancel non-accepted registration if competition requires organizers to cancel registration' do + registration = FactoryBot.create(:registration, registration_status: 'waiting_list') + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, allow_registration_self_delete_after_acceptance: false)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'status' => 'cancelled' }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'user cant cancel registration after registration ends' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :closed)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'status' => 'cancelled' }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:forbidden) + expect(error.error).to eq(ErrorCodes::USER_EDITS_NOT_ALLOWED) + end + end + + [ + { old_status: 'pending', new_status: 'accepted' }, + { old_status: 'pending', new_status: 'waiting_list' }, + { old_status: 'pending', new_status: 'cancelled' }, + { old_status: 'pending', new_status: 'pending' }, + { old_status: 'waiting_list', new_status: 'pending' }, + { old_status: 'waiting_list', new_status: 'cancelled' }, + { old_status: 'waiting_list', new_status: 'waiting_list' }, + { old_status: 'waiting_list', new_status: 'accepted' }, + { old_status: 'accepted', new_status: 'pending' }, + { old_status: 'accepted', new_status: 'cancelled' }, + { old_status: 'accepted', new_status: 'waiting_list' }, + { old_status: 'accepted', new_status: 'accepted' }, + { old_status: 'cancelled', new_status: 'accepted' }, + { old_status: 'cancelled', new_status: 'pending' }, + { old_status: 'cancelled', new_status: 'waiting_list' }, + { old_status: 'cancelled', new_status: 'cancelled' }, + ].each do |params| + it_behaves_like 'valid organizer status updates', params[:old_status], params[:new_status] + end + + it 'organizer can cancel registration after registration ends' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :closed)) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'status' => 'cancelled' }) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + end + + describe '#update_registration_allowed!.validate_update_events!' do + it 'user can add events' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build( + :update_request, user_id: registration[:user_id], competing: { 'event_ids' => ['333', '444', '555', '333mbf'] } + ) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'user can remove events' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build( + :update_request, user_id: registration[:user_id], competing: { 'event_ids' => ['333'] } + ) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'user can remove all old events and register for new ones' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build( + :update_request, user_id: registration[:user_id], competing: { 'event_ids' => ['777', '333bf'] } + ) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'events list cant be blank' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'event_ids' => [] }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::INVALID_EVENT_SELECTION) + end + end + + it 'events must be held at the competition' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'event_ids' => ['333fm', '333'] }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::INVALID_EVENT_SELECTION) + end + end + + it 'events must exist' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'event_ids' => ['888', '333'] }) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::INVALID_EVENT_SELECTION) + end + end + + it 'organizer can change a users events' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build( + :update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'event_ids' => ['333', '666'] } + ) + + expect { RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) } + .not_to raise_error + end + + it 'organizer cant change users events to events not held at competition' do + registration = FactoryBot.create(:registration) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition)) + update_request = FactoryBot.build( + :update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'event_ids' => ['333fm', '333'] } + ) + + expect { + RegistrationChecker.update_registration_allowed!(update_request, competition_info, update_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:unprocessable_entity) + expect(error.error).to eq(ErrorCodes::INVALID_EVENT_SELECTION) + end + end + end +end diff --git a/spec/support/registration_spec_helper.rb b/spec/support/registration_spec_helper.rb deleted file mode 100644 index 7590d1a5..00000000 --- a/spec/support/registration_spec_helper.rb +++ /dev/null @@ -1,364 +0,0 @@ -# frozen_string_literal: true - -module Helpers - module RegistrationHelper - # SHARED CONTEXTS - - RSpec.shared_context 'competition information' do - before do - # Define competition IDs - @includes_non_attending_registrations = 'CubingZANationalChampionship2023' - @attending_registrations_only = 'LazarilloOpen2023' - @empty_comp = '1AVG2013' - @error_comp_404 = 'InvalidCompID' - @error_comp_500 = 'BrightSunOpen2023' - @error_comp_502 = 'GACubersStudyJuly2023' - @registrations_exist_comp_500 = 'WinchesterWeeknightsIV2023' - @registrations_exist_comp_502 = 'BangaloreCubeOpenJuly2023' - @registrations_not_open = 'BrizZonSylwesterOpen2023' - @comment_mandatory = 'LazarilloOpen2024' - @full_competition = 'CubingZANationalChampionship2024' - - @base_comp_url = comp_api_url('') - - # TODO: Refctor these to be single lines that call a "stub competition" method?(how do I customise bodys and codes?) - - # COMP WITH ALL ATTENDING REGISTRATIONS - competition_details = get_competition_details(@attending_registrations_only) - stub_request(:get, "#{@base_comp_url}#{@attending_registrations_only}") - .to_return(status: 200, body: competition_details.to_json) - - # COMP WITH DIFFERENT REGISTATION STATUSES - Stub competition info - competition_details = get_competition_details(@includes_non_attending_registrations) - stub_request(:get, "#{@base_comp_url}#{@includes_non_attending_registrations}") - .to_return(status: 200, body: competition_details.to_json) - - # EMPTY COMP STUB - competition_details = get_competition_details(@empty_comp) - stub_request(:get, "#{@base_comp_url}#{@empty_comp}") - .to_return(status: 200, body: competition_details.to_json) - - # REGISTRATIONS NOT OPEN - competition_details = get_competition_details(@registrations_not_open) - stub_request(:get, "#{@base_comp_url}#{@registrations_not_open}") - .to_return(status: 200, body: competition_details.to_json) - - # COMMENT REQUIRED - competition_details = get_competition_details(@comment_mandatory) - stub_request(:get, "#{@base_comp_url}#{@comment_mandatory}") - .to_return(status: 200, body: competition_details.to_json) - - # COMPETITOR LIMIT REACHED - competition_details = get_competition_details(@full_competition) - stub_request(:get, "#{@base_comp_url}#{@full_competition}") - .to_return(status: 200, body: competition_details.to_json) - - # 404 COMP STUB - wca_error_json = { error: 'Competition with id InvalidCompId not found' }.to_json - stub_request(:get, "#{@base_comp_url}#{@error_comp_404}") - .to_return(status: 404, body: wca_error_json) - - # 500 COMP STUB - error_json = { error: - "Internal Server Error for url: /api/v0/competitions/#{@error_comp_500}" }.to_json - stub_request(:get, "#{@base_comp_url}#{@error_comp_500}") - .to_return(status: 500, body: error_json) - - error_json = { error: - "Internal Server Error for url: /api/v0/competitions/#{@registrations_exist_comp_500}" }.to_json - stub_request(:get, "#{@base_comp_url}#{@registrations_exist_comp_500}") - .to_return(status: 500, body: error_json) - - # 502 COMP STUB - error_json = { error: - "Internal Server Error for url: /api/v0/competitions/#{@error_comp_502}" }.to_json - stub_request(:get, "#{@base_comp_url}#{@error_comp_502}") - .to_return(status: 502, body: error_json) - error_json = { error: - "Internal Server Error for url: /api/v0/competitions/#{@registrations_exist_comp_502}" }.to_json - stub_request(:get, "#{@base_comp_url}#{@registrations_exist_comp_502}") - .to_return(status: 502, body: error_json) - end - end - - RSpec.shared_context 'auth_tokens' do - before do - @jwt_800 = fetch_jwt_token('800') - @jwt_816 = fetch_jwt_token('158816') - @jwt_817 = fetch_jwt_token('158817') - @jwt_818 = fetch_jwt_token('158818') - @jwt_819 = fetch_jwt_token('158819') - @jwt_820 = fetch_jwt_token('158820') - @jwt_823 = fetch_jwt_token('158823') - @jwt_824 = fetch_jwt_token('158824') - @jwt_200 = fetch_jwt_token('158200') - @jwt_201 = fetch_jwt_token('158201') - @jwt_202 = fetch_jwt_token('158202') - @admin_token = fetch_jwt_token('15073') - @admin_token_2 = fetch_jwt_token('15074') - @organizer_token = fetch_jwt_token('1') - @multi_comp_organizer_token = fetch_jwt_token('2') - @banned_user_jwt = fetch_jwt_token('209943') - @incomplete_user_jwt = fetch_jwt_token('999999') - end - end - - RSpec.shared_context 'registration_data' do - before do - # General - @basic_registration = get_registration('CubingZANationalChampionship2023-158816', false) - @required_fields_only = get_registration('CubingZANationalChampionship2023-158817', false) - @no_attendee_id = get_registration('CubingZANationalChampionship2023-158818', false) - @empty_payload = {}.to_json - @reg_2 = get_registration('LazarilloOpen2023-158820', false) - - # Failure cases - @admin_comp_not_open = get_registration('BrizZonSylwesterOpen2023-15074', false) - @comp_not_open = get_registration('BrizZonSylwesterOpen2023-158817', false) - @bad_comp_name = get_registration('InvalidCompID-158817', false) - @banned_user_reg = get_registration('CubingZANationalChampionship2023-209943', false) - @incomplete_user_reg = get_registration('CubingZANationalChampionship2023-999999', false) - @events_not_held_reg = get_registration('CubingZANationalChampionship2023-158201', false) - @events_not_exist_reg = get_registration('CubingZANationalChampionship2023-158202', false) - @too_many_guests = get_registration('CubingZANationalChampionship2023-158824', false) - - # For 'various optional fields' - @with_hide_name_publicly = get_registration('CubingZANationalChampionship2023-158820', false) - - # For 'bad request payloads' - @missing_reg_fields = get_registration('CubingZANationalChampionship2023-158821', false) - @empty_json = get_registration('', false) - @missing_lane = get_registration('CubingZANationalChampionship2023-158822', false) - end - end - - RSpec.shared_context 'PATCH payloads' do - before do - # URL parameters - @competiton_id = 'CubingZANationalChampionship2023' - @user_id_816 = '158816' - @user_id_823 = '158823' - - # Cancel payloads - @bad_comp_cancellation = get_patch('816-cancel-bad-comp') - @cancellation_with_events = get_patch('816-cancel-and-change-events') - @bad_user_cancellation = get_patch('800-cancel-no-reg') - @cancellation_1 = get_patch('1-cancel-full-registration') - @cancellation_816 = get_patch('816-cancel-full-registration') - @cancellation_816_2 = get_patch('816-cancel-full-registration_2') - @cancellation_817 = get_patch('817-cancel-full-registration') - @cancellation_818 = get_patch('818-cancel-full-registration') - @cancellation_819 = get_patch('819-cancel-full-registration') - @cancellation_823 = get_patch('823-cancel-full-registration') - @cancellation_073 = get_patch('073-cancel-full-registration') - @double_cancellation = get_patch('823-cancel-full-registration') - @cancel_wrong_lane = get_patch('823-cancel-wrong-lane') - - # Update payloads - @add_444 = get_patch('CubingZANationalChampionship2023-158816') - @comment_update = get_patch('816-comment-update') - @comment_update_2 = get_patch('817-comment-update') - @comment_update_3 = get_patch('817-comment-update-2') - @comment_update_4 = get_patch('820-missing-comment') - @guest_update_1 = get_patch('816-guest-update') - @guest_update_2 = get_patch('817-guest-update') - @guest_update_3 = get_patch('817-guest-update-2') - @events_update_1 = get_patch('816-events-update') - @events_update_2 = get_patch('817-events-update') - @events_update_3 = get_patch('817-events-update-2') - @events_update_5 = get_patch('817-events-update-4') - @events_update_6 = get_patch('817-events-update-5') - @events_update_7 = get_patch('817-events-update-6') - @pending_update_1 = get_patch('817-status-update-1') - @pending_update_2 = get_patch('817-status-update-2') - @pending_update_3 = get_patch('819-status-update-3') - @waiting_update_1 = get_patch('819-status-update-1') - @waiting_update_2 = get_patch('819-status-update-2') - @accepted_update_1 = get_patch('816-status-update-1') - @accepted_update_2 = get_patch('816-status-update-2') - @invalid_status_update = get_patch('816-status-update-3') - @delayed_update_1 = get_patch('820-delayed-update') - end - end - - RSpec.shared_context 'database seed' do - before do - create_registration(get_registration('CubingZANationalChampionship2023-158816', true)) # Accepted registration - create_registration(get_registration('CubingZANationalChampionship2023-1', true)) # Accepted registration - create_registration(get_registration('CubingZANationalChampionship2023-158817', true)) # Pending registration - create_registration(get_registration('CubingZANationalChampionship2023-158818', true)) # update_pending registration - create_registration(get_registration('CubingZANationalChampionship2023-158819', true)) # waiting_list registration - create_registration(get_registration('CubingZANationalChampionship2023-158823', true)) # Cancelled registration - - # Create registrations for 'WinchesterWeeknightsIV2023' - all accepted - create_registration(get_registration('WinchesterWeeknightsIV2023-158816', true)) - create_registration(get_registration('WinchesterWeeknightsIV2023-158817', true)) - create_registration(get_registration('WinchesterWeeknightsIV2023-158818', true)) - - # Create registrations for 'BangaloreCubeOpenJuly2023' - all accepted - create_registration(get_registration('BangaloreCubeOpenJuly2023-158818', true)) - create_registration(get_registration('BangaloreCubeOpenJuly2023-158819', true)) - - # Create registrations for 'LazarilloOpen2023' - all accepted - create_registration(get_registration('LazarilloOpen2023-158820', true)) - create_registration(get_registration('LazarilloOpen2023-158821', true)) - create_registration(get_registration('LazarilloOpen2023-158822', true)) - create_registration(get_registration('LazarilloOpen2023-158823', true)) - - # Create registrations for LazarilloOpen2024 - all acceptd - create_registration(get_registration('LazarilloOpen2024-158820', true)) - - # Create registrations for CubingZANationals2024 - create_registration(get_registration('CubingZANationalChampionship2024-158816', true)) - create_registration(get_registration('CubingZANationalChampionship2024-158817', true)) - create_registration(get_registration('CubingZANationalChampionship2024-158818', true)) - create_registration(get_registration('CubingZANationalChampionship2024-158819', true)) - - # Create registrations for 'BrizZonSylwesterOpen2023' - create_registration(get_registration('BrizZonSylwesterOpen2023-15073', true)) - end - end - - # HELPER METHODS - - # Create registration from raw registration JSON - def create_registration(registration_data) - registration = Registration.new(registration_data) - registration.save - end - - # For mocking - returns the saved JSON response of /api/v0/competitions for the given competition ID - def get_competition_details(competition_id) - File.open("#{Rails.root}/spec/fixtures/competition_details.json", 'r') do |f| - competition_details = JSON.parse(f.read) - - # Retrieve the competition details when competition_id matches - competition_details['competitions'].each do |competition| - return competition if competition['id'] == competition_id - end - end - end - - # Creates a JWT token for the given user_id - def fetch_jwt_token(user_id) - iat = Time.now.to_i - jti_raw = [JwtOptions.secret, iat].join(':').to_s - jti = Digest::MD5.hexdigest(jti_raw) - payload = { user_id: user_id, exp: Time.now.to_i + JwtOptions.expiry, sub: user_id, iat: iat, jti: jti } - token = JWT.encode payload, JwtOptions.secret, JwtOptions.algorithm - "Bearer #{token}" - end - - # Returns a registration from registrations.json for the given attendee_id - # If raw is true, returns it in the simplified format for submission to the POST registration endpoint - # If raw is false, returns the database-like registration JSON object - def get_registration(attendee_id, raw) - File.open("#{Rails.root}/spec/fixtures/registrations.json", 'r') do |f| - registrations = JSON.parse(f.read) - - # Retrieve the competition details when attendee_id matches - registration = registrations.find { |r| r['attendee_id'] == attendee_id } - begin - registration['lanes'] = registration['lanes'].map { |lane| Lane.new(lane) } - if raw - return registration - end - rescue NoMethodError - # puts e - return registration - end - convert_registration_object_to_payload(registration) - end - end - - # "patch" object is the input to a PATCH request to an API endpoint. - # This function returns a JSON patch object according to its "patch_name" (the key value in a JSON file) - def get_patch(patch_name) - File.open("#{Rails.root}/spec/fixtures/patches.json", 'r') do |f| - patches = JSON.parse(f.read) - - # Retrieve the competition details when attendee_id matches - patch = patches[patch_name] - patch - end - end - - private - - # Converts a raw registration object (database-like format) to a payload which can be sent to the registration API - def convert_registration_object_to_payload(registration) - competing_lane = registration['lanes'].find { |l| l.lane_name == 'competing' } - event_ids = get_event_ids_from_competing_lane(competing_lane) - - registration_payload = { - user_id: registration['user_id'], - competition_id: registration['competition_id'], - competing: { - event_ids: event_ids, - registration_status: competing_lane.lane_state, - }, - } - if competing_lane.lane_details.key?('guests') - registration_payload[:guests] = competing_lane.lane_details['guests'] - end - registration_payload - end - - # Returns an array of event_ids for the given competing lane - # NOTE: Assumes that the given lane is a competing lane - it doesn't validate this - def get_event_ids_from_competing_lane(competing_lane) - event_ids = [] - competing_lane.lane_details['event_details'].each do |event| - # Add the event["event_id"] to the list of event_ids - event_ids << event['event_id'] - end - event_ids - end - - # Determines whether the two given values represent equiivalent registration hashes - def registration_equal(registration_model, registration_hash) - unchecked_attributes = [:created_at, :updated_at] - - registration_model.attributes.each do |k, v| - unless unchecked_attributes.include?(k) - hash_value = registration_hash[k.to_s] - - if v.is_a?(Hash) && hash_value.is_a?(Hash) - return false unless nested_hash_equal?(v, hash_value) - elsif v.is_a?(Array) && hash_value.is_a?(Array) - return false unless lanes_equal(v, hash_value) - elsif hash_value != v - puts "#{hash_value} does not equal #{v}" - return false - end - end - end - - true - end - - # Determines whether the given registration lanes are equivalent - # Helper method to registration_equal - def lanes_equal(lanes1, lanes2) - lanes1.each_with_index do |el, i| - unless el == lanes2[i] - return false - end - end - true - end - - # Helper method to registration_equal - def nested_hash_equal?(hash1, hash2) - hash1.each do |k, v| - if v.is_a?(Hash) && hash2[k].is_a?(Hash) - return false unless nested_hash_equal?(v, hash2[k]) - elsif hash2[k.to_s] != v - puts "#{hash2[k.to_s]} does not equal to #{v}" - return false - end - end - true - end - end -end diff --git a/spec/todo/get_attendee_spec.rb b/spec/todo/get_attendee_spec.rb deleted file mode 100644 index 732a6893..00000000 --- a/spec/todo/get_attendee_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'swagger_helper' -require_relative '../support/registration_spec_helper' - -RSpec.describe 'v1 Registrations API', type: :request do - include Helpers::RegistrationHelper - - path '/api/v1/attendees/{attendee_id}' do - get 'Retrieve attendee registration' do - parameter name: :attendee_id, in: :path, type: :string, required: true - produces 'application/json' - - context 'success get attendee registration' do - existing_attendee = 'CubingZANationalChampionship2023-158816' - - response '200', 'validate endpoint and schema' do - schema '$ref' => '#/components/schemas/registration' - - let!(:attendee_id) { existing_attendee } - - run_test! - end - - response '200', 'check that registration returned matches expected registration' do - include_context 'registration_data' - - let!(:attendee_id) { existing_attendee } - - run_test! do |response| - # TODO: This should use a custom-written comparison script - expect(response.body).to eq(basic_registration) - end - end - end - - context 'fail get attendee registration' do - response '404', 'attendee_id doesnt exist' do - let!(:attendee_id) { 'InvalidAttendeeID' } - - run_test! do |response| - expect(response.body).to eq({ error: "No registration found for attendee_id: #{attendee_id}." }.to_json) - end - end - end - end - end -end diff --git a/spec/todo/patch_registration_spec.rb b/spec/todo/patch_registration_spec.rb deleted file mode 100644 index 171d6ef9..00000000 --- a/spec/todo/patch_registration_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'swagger_helper' -require_relative '../../app/helpers/error_codes' - -RSpec.describe 'v1 Registrations API', type: :request do - include Helpers::RegistrationHelper - - # TODO: Figure out why competiton_id isn't being included in ROUTE path, and fix it on cancel file too - # TODO: What happens to existing registrations if the organisser wants to change currency or price of events that registrations already exist for? Is this allowed? - # TODO: Should we still have last action information if we're going to have a separate logging system for registration changes? - path '/api/v1/registrations/{competition_id}/{user_id}' do - patch 'update or cancel an attendee registration' do - parameter name: :competition_id, in: :path, type: :string, required: true - parameter name: :user_id, in: :path, type: :string, required: true - parameter name: :update, in: :body, required: true - - produces 'application/json' - - context 'SUCCESS: Registration update base cases' do - include_context 'PATCH payloads' - include_context 'database seed' - - response '200', 'add a new event' do - let!(:payload) { @add_444 } - let!(:competition_id) { @competition_id } - let!(:user_id) { @user_id_816 } - - run_test! do - registration = Registrations.find('CubingZANationalChampionship2023-158816') - - reg_for_444 = false - - # NOTE: Breaks if we have more than 1 lane - events = registration[:lanes][0].lane_details['event_details'] - events.each do |event| - if event['event_id'] == '444' - reg_for_444 = true - end - end - - expect(reg_for_444).to eq(true) - end - end - end - end - end -end