diff --git a/.env.development b/.env.development index 1d8b98cf..2c42daff 100644 --- a/.env.development +++ b/.env.development @@ -3,6 +3,7 @@ AWS_ACCESS_KEY_ID=fake-key AWS_SECRET_ACCESS_KEY=fake-access-key DYNAMO_REGISTRATIONS_TABLE=registrations-development REGISTRATION_HISTORY_DYNAMO_TABLE=registration-history-development +WAITING_LIST_DYNAMO_TABLE=waiting-lists-development RAILS_LOG_TO_STDOUT=true JWT_SECRET=jwt-development-secret SECRET_KEY_BASE=a003fdc6f113ff7d295596a02192c7116a76724ba6d3071043eefdd16f05971be0dc58f244e67728757b2fb55ae7a41e1eb97c1fe247ddaeb6caa97cea32120c diff --git a/.env.test b/.env.test index 4a0036d1..ef1a8840 100644 --- a/.env.test +++ b/.env.test @@ -3,6 +3,7 @@ AWS_ACCESS_KEY_ID=fake-key AWS_SECRET_ACCESS_KEY=fake-access-key DYNAMO_REGISTRATIONS_TABLE=registrations-development REGISTRATION_HISTORY_DYNAMO_TABLE=registration-history-development +WAITING_LIST_DYNAMO_TABLE=waiting-lists-development JWT_SECRET=jwt-test-secret SECRET_KEY_BASE=a003fdc6f113ff7d295596a02192c7116a76724ba6d3071043eefdd16f05971be0dc58f244e67728757b2fb55ae7a41e1eb97c1fe247ddaeb6caa97cea32120c WCA_HOST=https://worldcubeassociation.org diff --git a/app/controllers/registration_controller.rb b/app/controllers/registration_controller.rb index 409b5a8c..22874f87 100644 --- a/app/controllers/registration_controller.rb +++ b/app/controllers/registration_controller.rb @@ -5,7 +5,7 @@ require 'time' class RegistrationController < ApplicationController - skip_before_action :validate_jwt_token, only: [:list, :list_waiting, :count] + skip_before_action :validate_jwt_token, only: [:list, :count] # The order of the validations is important to not leak any non public info via the API # That's why we should always validate a request first, before taking any other before action # before_actions are triggered in the order they are defined @@ -63,6 +63,7 @@ def validate_create_request @user_id = registration_params[:user_id] RegistrationChecker.create_registration_allowed!(registration_params, CompetitionApi.find(@competition_id), @current_user) rescue RegistrationError => e + Rails.logger.debug { "Create was rejected with error #{e.error} at #{e.backtrace[0]}" } render_error(e.http_status, e.error, e.data) end @@ -78,8 +79,9 @@ def update def validate_update_request @user_id = params[:user_id] @competition_id = params[:competition_id] + @competition_info = CompetitionApi.find!(@competition_id) - RegistrationChecker.update_registration_allowed!(params, CompetitionApi.find!(@competition_id), @current_user) + RegistrationChecker.update_registration_allowed!(params, @competition_info, @current_user) rescue RegistrationError => e render_error(e.http_status, e.error, e.data) end @@ -102,7 +104,8 @@ def bulk_update def validate_bulk_update_request @competition_id = params.require('competition_id') - RegistrationChecker.bulk_update_allowed!(params, CompetitionApi.find!(@competition_id), @current_user) + @competition_info = CompetitionApi.find!(@competition_id) + RegistrationChecker.bulk_update_allowed!(params, @competition_info, @current_user) rescue BulkUpdateError => e render_error(e.http_status, e.errors) rescue NoMethodError @@ -121,7 +124,8 @@ def process_update(update_request) registration = Registration.find("#{@competition_id}-#{user_id}") old_status = registration.competing_status - updated_registration = registration.update_competing_lane!({ status: status, comment: comment, event_ids: event_ids, admin_comment: admin_comment, guests: guests, waiting_list_position: waiting_list_position }) + waiting_list = @competition_info.waiting_list + updated_registration = registration.update_competing_lane!({ status: status, comment: comment, event_ids: event_ids, admin_comment: admin_comment, guests: guests, waiting_list_position: waiting_list_position }, waiting_list) registration.history.add_entry(update_changes(update_request), 'user', @current_user, action_type(update_request)) if old_status == 'accepted' && status != 'accepted' Registration.decrement_competitors_count(@competition_id) @@ -194,24 +198,6 @@ def mine status: :internal_server_error end - def list_waiting - competition_id = list_params - - waiting = Registration.get_registrations_by_status(competition_id, 'waiting_list').map do |registration| - { - user_id: registration[:user_id], - waiting_list_position: registration.competing_waiting_list_position || 0, - } - end - render json: waiting - rescue Dynamoid::Errors::Error => e - # Render an error response - Rails.logger.debug e - Metrics.registration_dynamodb_errors_counter.increment - render json: { error: "Error getting registrations #{e}" }, - status: :internal_server_error - end - # To list Registrations in the admin view you need to be able to administer the competition def validate_list_admin @competition_id = list_params @@ -222,10 +208,10 @@ def validate_list_admin def list_admin registrations = get_registrations(@competition_id) - render json: add_pii(registrations) + registrations_with_pii = add_pii(registrations) + render json: add_waiting_list(@competition_id, registrations_with_pii) rescue Dynamoid::Errors::Error => e Rails.logger.debug e - # Is there a reason we aren't using an error code here? Metrics.registration_dynamodb_errors_counter.increment render json: { error: "Error getting registrations #{e}" }, status: :internal_server_error @@ -301,6 +287,16 @@ def add_pii(registrations) end end + def add_waiting_list(competition_id, registrations) + list = WaitingList.find_or_create!(competition_id).entries + return registrations if list.empty? + registrations.map do |r| + waiting_list_index = list.find_index(r[:user_id]) + r[:competing].merge!(waiting_list_position: waiting_list_index + 1) if waiting_list_index.present? + r + end + end + def get_registrations(competition_id, only_attending: false) if only_attending Registration.where(competition_id: competition_id, competing_status: 'accepted').all.map do |x| @@ -318,7 +314,6 @@ def get_registrations(competition_id, only_attending: false) registered_on: x['created_at'], comment: x.competing_comment, admin_comment: x.admin_comment, - waiting_list_position: x.competing_waiting_list_position, }, payment: { payment_status: x.payment_status, @@ -333,6 +328,12 @@ def get_registrations(competition_id, only_attending: false) def get_single_registration(user_id, competition_id) registration = Registration.find("#{competition_id}-#{user_id}") + waiting_list_position = if registration.competing_status == 'waiting_list' + waiting_list = WaitingList.find_or_create!(competition_id) + registration.waiting_list_position(waiting_list) + else + nil + end { user_id: registration['user_id'], guests: registration.guests, @@ -342,7 +343,7 @@ def get_single_registration(user_id, competition_id) registered_on: registration['created_at'], comment: registration.competing_comment, admin_comment: registration.admin_comment, - waiting_list_position: registration.competing_waiting_list_position, + waiting_list_position: waiting_list_position, }, payment: { payment_status: registration.payment_status, diff --git a/app/models/registration.rb b/app/models/registration.rb index cfcf6950..ddfbca22 100644 --- a/app/models/registration.rb +++ b/app/models/registration.rb @@ -77,10 +77,6 @@ def event_details competing_lane&.lane_details&.[]('event_details') end - def competing_waiting_list_position - competing_lane&.lane_details&.[]('waiting_list_position') - end - def competing_comment competing_lane&.lane_details&.[]('comment') end @@ -101,6 +97,11 @@ def payment_amount payment_lane&.lane_details&.[]('amount_lowest_denominator') end + def waiting_list_position(waiting_list) + return nil if competing_status != 'waiting_list' + waiting_list.entries.find_index(user_id) + 1 + end + def payment_amount_human_readable payment_details = payment_lane&.lane_details unless payment_details.nil? @@ -116,17 +117,7 @@ def payment_history payment_lane&.lane_details&.[]('payment_history') end - def update_competing_waiting_list_position(new_position) - updated_lanes = lanes.map do |lane| - if lane.lane_name == 'competing' - lane.lane_details['waiting_list_position'] = new_position - end - lane - end - update_attributes!(lanes: updated_lanes, competing_status: competing_lane.lane_state, guests: guests) - end - - def update_competing_lane!(update_params) + def update_competing_lane!(update_params, waiting_list) has_waiting_list_changed = waiting_list_changed?(update_params) updated_lanes = lanes.map do |lane| @@ -134,7 +125,7 @@ def update_competing_lane!(update_params) # Update status for lane and events if has_waiting_list_changed - update_waiting_list(lane, update_params) + update_waiting_list(update_params, waiting_list) end if update_params[:status].present? @@ -158,14 +149,7 @@ def update_competing_lane!(update_params) end # TODO: In the future we will need to check if any of the other lanes have a status set to accepted updated_guests = (update_params[:guests].presence || guests) - updated_values = update_attributes!(lanes: updated_lanes, competing_status: competing_lane.lane_state, guests: updated_guests) - if has_waiting_list_changed - # Update waiting list caches - Rails.cache.delete("#{competition_id}-waiting_list_registrations") - Rails.cache.delete("#{competition_id}-waiting_list_boundaries") - competing_lane.get_waiting_list_boundaries(competition_id) - end - updated_values + update_attributes!(lanes: updated_lanes, competing_status: competing_lane.lane_state, guests: updated_guests) end def init_payment_lane(amount, currency_code, id, donation) @@ -187,15 +171,15 @@ def update_payment_lane(id, iso_amount, currency_iso, status) update_attributes!(lanes: updated_lanes) end - def update_waiting_list(lane, update_params) - update_params[:waiting_list_position]&.to_i - lane.add_to_waiting_list(competition_id) if update_params[:status] == 'waiting_list' - lane.accept_from_waiting_list if update_params[:status] == 'accepted' - lane.remove_from_waiting_list(competition_id) if update_params[:status] == 'cancelled' || update_params[:status] == 'pending' - lane.move_within_waiting_list(competition_id, update_params[:waiting_list_position].to_i) if - update_params[:waiting_list_position].present? && update_params[:waiting_list_position] != competing_waiting_list_position - end + def update_waiting_list(update_params, waiting_list) + raise ArgumentError.new('Can only accept waiting list leader') if update_params[:status] == 'accepted' && waiting_list_position(waiting_list) != 1 + waiting_list.add(self.user_id) if update_params[:status] == 'waiting_list' + waiting_list.remove(self.user_id) if update_params[:status] == 'accepted' + waiting_list.remove(self.user_id) if update_params[:status] == 'cancelled' || update_params[:status] == 'pending' + waiting_list.move_to_position(self.user_id, update_params[:waiting_list_position].to_i) if + update_params[:waiting_list_position].present? + end # Fields field :user_id, :integer field :guests, :integer @@ -224,8 +208,7 @@ def waiting_list_changed?(update_params) end def waiting_list_position_changed?(update_params) - return false if update_params[:waiting_list_position].blank? - update_params[:waiting_list_position] != competing_waiting_list_position + update_params[:waiting_list_position].present? end def waiting_list_status_changed?(update_params) diff --git a/app/models/waiting_list.rb b/app/models/waiting_list.rb new file mode 100644 index 00000000..45d74ba7 --- /dev/null +++ b/app/models/waiting_list.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class WaitingList + include Dynamoid::Document + + # We autoscale dynamodb + table name: EnvConfig.WAITING_LIST_DYNAMO_TABLE, capacity_mode: nil, key: :id + + field :entries, :array, of: :integer + + def remove(user_id) + update_attributes!(entries: entries - [user_id]) + end + + def add(user_id) + if entries.nil? + update_attributes!(entries: [user_id]) + else + update_attributes!(entries: entries + [user_id]) + end + end + + def move_to_position(user_id, new_position) + raise ArgumentError.new('Target position out of waiting list range') if new_position > entries.length || new_position < 1 + + old_index = entries.find_index(user_id) + return if old_index == new_position-1 + + update_attributes!(entries: entries.insert(new_position-1, entries.delete_at(old_index))) + end + + def self.find_or_create!(id) + WaitingList.find(id) + rescue Dynamoid::Errors::RecordNotFound + WaitingList.create(id: id, entries: []) + end +end diff --git a/app/services/registration_checker.rb b/app/services/registration_checker.rb index 18a73033..154490a2 100644 --- a/app/services/registration_checker.rb +++ b/app/services/registration_checker.rb @@ -38,6 +38,8 @@ def self.update_registration_allowed!(update_request, competition_info, requesti end def self.bulk_update_allowed!(bulk_update_request, competition_info, requesting_user) + @competition_info = competition_info + raise BulkUpdateError.new(:unauthorized, [ErrorCodes::USER_INSUFFICIENT_PERMISSIONS]) unless UserApi.can_administer?(requesting_user, competition_info.id) @@ -45,9 +47,11 @@ def self.bulk_update_allowed!(bulk_update_request, competition_info, requesting_ bulk_update_request['requests'].each do |update_request| update_registration_allowed!(update_request, competition_info, requesting_user) rescue RegistrationError => e + Rails.logger.debug { "Bulk update was rejected with error #{e.error} at #{e.backtrace[0]}" } errors[update_request['user_id']] = e.error end - raise BulkUpdateError.new(:unprocessable_entity, errors) unless errors == {} + + raise BulkUpdateError.new(:unprocessable_entity, errors) unless errors.empty? end class << self @@ -68,8 +72,8 @@ def user_can_create_registration! def user_can_modify_registration! raise RegistrationError.new(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) unless can_administer_or_current_user? + raise RegistrationError.new(:forbidden, ErrorCodes::USER_EDITS_NOT_ALLOWED) unless @competition_info.registration_edits_allowed? || UserApi.can_administer?(@requester_user_id, @competition_info.id) raise RegistrationError.new(:unauthorized, ErrorCodes::REGISTRATION_IS_REJECTED) if user_is_rejected? - raise RegistrationError.new(:forbidden, ErrorCodes::USER_EDITS_NOT_ALLOWED) unless @competition_info.registration_edits_allowed? || @competition_info.is_organizer_or_delegate?(@requester_user_id) raise RegistrationError.new(:forbidden, ErrorCodes::ALREADY_REGISTERED_IN_SERIES) if existing_registration_in_series? end @@ -133,7 +137,7 @@ def validate_comment! def validate_organizer_fields! @organizer_fields = ['organizer_comment', 'waiting_list_position'] - raise RegistrationError.new(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) if contains_organizer_fields? && !@competition_info.is_organizer_or_delegate?(@requester_user_id) + raise RegistrationError.new(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) if contains_organizer_fields? && !UserApi.can_administer?(@requester_user_id, @competition_info.id) end def validate_organizer_comment! @@ -152,13 +156,10 @@ def validate_waiting_list_position! converted_position = Integer(waiting_list_position, exception: false) raise RegistrationError.new(:unprocessable_entity, ErrorCodes::INVALID_WAITING_LIST_POSITION) unless converted_position.is_a? Integer - boundaries = @registration.competing_lane.get_waiting_list_boundaries(@competition_info.competition_id) - if boundaries['waiting_list_position_min'].nil? && boundaries['waiting_list_position_max'].nil? - raise RegistrationError.new(:forbidden, ErrorCodes::INVALID_WAITING_LIST_POSITION) if converted_position != 1 - else - raise RegistrationError.new(:forbidden, ErrorCodes::INVALID_WAITING_LIST_POSITION) if - converted_position < boundaries['waiting_list_position_min'] || converted_position > boundaries['waiting_list_position_max'] - end + waiting_list = @competition_info.waiting_list.entries + raise RegistrationError.new(:forbidden, ErrorCodes::INVALID_WAITING_LIST_POSITION) if waiting_list.empty? && converted_position != 1 + raise RegistrationError.new(:forbidden, ErrorCodes::INVALID_WAITING_LIST_POSITION) if converted_position > waiting_list.length + raise RegistrationError.new(:forbidden, ErrorCodes::INVALID_WAITING_LIST_POSITION) if converted_position < 1 end def contains_organizer_fields? @@ -173,11 +174,6 @@ def validate_update_status! raise RegistrationError.new(:forbidden, ErrorCodes::COMPETITOR_LIMIT_REACHED) if new_status == 'accepted' && Registration.accepted_competitors_count(@competition_info.competition_id) == @competition_info.competitor_limit - # Organizers cant accept someone from the waiting list who isn't in the leading position - min_waiting_list_position = @registration.competing_lane.get_waiting_list_boundaries(@registration.competition_id)['waiting_list_position_min'] - raise RegistrationError.new(:forbidden, ErrorCodes::MUST_ACCEPT_WAITING_LIST_LEADER) if - current_status == 'waiting_list' && new_status == 'accepted' && @registration.competing_waiting_list_position != min_waiting_list_position - # Otherwise, organizers can make any status change they want to return if UserApi.can_administer?(@requester_user_id, @competition_info.id) diff --git a/config/routes.rb b/config/routes.rb index 9ad3b78f..5f17f7d3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -21,7 +21,6 @@ get '/api/v1/registrations/mine', to: 'registration#mine' get '/api/v1/registrations/:competition_id', to: 'registration#list' get '/api/v1/registrations/:competition_id/admin', to: 'registration#list_admin' - get '/api/v1/registrations/:competition_id/waiting', to: 'registration#list_waiting' get '/api/v1/:competition_id/payment', to: 'registration#payment_ticket' get '/api/v1/:competition_id/count', to: 'registration#count' post '/api/v1/:competition_id/import', to: 'registration#import' diff --git a/db/seeds.rb b/db/seeds.rb index 6dd94cb9..8bf3e8fd 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -47,6 +47,14 @@ }, ] begin + dynamodb.create_table({ + table_name: EnvConfig.WAITING_LIST_DYNAMO_TABLE, + key_schema: [{ attribute_name: 'id', key_type: 'HASH' }], + provisioned_throughput: provisioned_throughput, + attribute_definitions: [ + { attribute_name: 'id', attribute_type: 'S' }, + ], + }) dynamodb.create_table({ table_name: table_name, key_schema: key_schema, diff --git a/env_config.rb b/env_config.rb index a798e38b..48aeee65 100644 --- a/env_config.rb +++ b/env_config.rb @@ -26,5 +26,6 @@ mandatory :PROMETHEUS_EXPORTER, :string mandatory :DYNAMO_REGISTRATIONS_TABLE, :string mandatory :REGISTRATION_HISTORY_DYNAMO_TABLE, :string + mandatory :WAITING_LIST_DYNAMO_TABLE, :string mandatory :QUEUE_NAME, :string end diff --git a/infra/handler/main.tf b/infra/handler/main.tf index 5d095874..c6f8ba6b 100644 --- a/infra/handler/main.tf +++ b/infra/handler/main.tf @@ -40,6 +40,10 @@ locals { name = "REGISTRATION_HISTORY_DYNAMO_TABLE", value = var.shared_resources.dynamo_registration_history_table.name }, + { + name = "WAITING_LIST_DYNAMO_TABLE", + value = var.shared_resources.dynamo_waiting_list_table.name + }, { name = "QUEUE_NAME", value = var.shared_resources.queue.name @@ -103,7 +107,8 @@ data "aws_iam_policy_document" "task_policy" { "dynamodb:DescribeTable", ] resources = [var.shared_resources.dynamo_registration_table.arn, "${var.shared_resources.dynamo_registration_table.arn}/*", - var.shared_resources.dynamo_registration_history_table.arn, "${var.shared_resources.dynamo_registration_history_table.arn}/*"] + var.shared_resources.dynamo_registration_history_table.arn, "${var.shared_resources.dynamo_registration_history_table.arn}/*", + var.shared_resources.dynamo_waiting_list_table.arn, "${var.shared_resources.dynamo_waiting_list_table.arn}/*"] } statement { effect = "Allow" diff --git a/infra/handler/variables.tf b/infra/handler/variables.tf index 0caafc44..7ef51e10 100644 --- a/infra/handler/variables.tf +++ b/infra/handler/variables.tf @@ -57,6 +57,10 @@ variable "shared_resources" { name: string, arn: string }), + dynamo_waiting_list_table: object({ + name: string, + arn: string + }), queue: object({ arn: string, name: string diff --git a/infra/shared/dynamodb.tf b/infra/shared/dynamodb.tf index 21b124e0..8100516e 100644 --- a/infra/shared/dynamodb.tf +++ b/infra/shared/dynamodb.tf @@ -50,9 +50,7 @@ resource "aws_dynamodb_table" "registrations" { resource "aws_dynamodb_table" "registration_history" { name = "registrations_history" - billing_mode = "PROVISIONED" - read_capacity = 5 - write_capacity = 5 + billing_mode = "PAY_PER_REQUEST" hash_key = "attendee_id" attribute { @@ -70,15 +68,33 @@ resource "aws_dynamodb_table" "registration_history" { } } +resource "aws_dynamodb_table" "waiting_list" { + name = "waiting_list" + billing_mode = "PAY_PER_REQUEST" + hash_key = "id" + + attribute { + name = "id" + type = "S" + } + + tags = { + Env = "prod" + } +} + + output "dynamo_registration_table" { value = aws_dynamodb_table.registrations } output "dynamo_registration_history_table" { value = aws_dynamodb_table.registration_history } +output "dynamo_waiting_list_table" { + value = aws_dynamodb_table.waiting_list +} - -# Add autoscaling +# Add autoscaling for the whole table module "table_autoscaling" { source = "snowplow-devops/dynamodb-autoscaling/aws" version = "0.2.1" @@ -94,3 +110,117 @@ module "table_autoscaling" { write_scale_out_cooldown = 30 write_target_value = 85 } + +# Autoscaling for the GSIs +resource "aws_appautoscaling_target" "read_target_gsi_competition_id" { + max_capacity = 100 + min_capacity = 5 + resource_id = "table/${aws_dynamodb_table.registrations.name}/index/registrations_index_competition_id" + scalable_dimension = "dynamodb:index:ReadCapacityUnits" + service_namespace = "dynamodb" +} + +resource "aws_appautoscaling_target" "write_target_gsi_competition_id" { + max_capacity = 100 + min_capacity = 5 + resource_id = "table/${aws_dynamodb_table.registrations.name}/index/registrations_index_competition_id" + scalable_dimension = "dynamodb:index:WriteCapacityUnits" + service_namespace = "dynamodb" +} + +resource "aws_appautoscaling_policy" "read_policy" { + name = "DynamoDBReadCapacityUtilization:${aws_appautoscaling_target.read_target_gsi_competition_id.resource_id}" + policy_type = "TargetTrackingScaling" + resource_id = aws_appautoscaling_target.read_target_gsi_competition_id.resource_id + scalable_dimension = aws_appautoscaling_target.read_target_gsi_competition_id.scalable_dimension + service_namespace = aws_appautoscaling_target.read_target_gsi_competition_id.service_namespace + + target_tracking_scaling_policy_configuration { + predefined_metric_specification { + predefined_metric_type = "DynamoDBReadCapacityUtilization" + } + + target_value = 85 + scale_in_cooldown = 30 + scale_out_cooldown = 30 + } + + depends_on = [aws_appautoscaling_target.read_target_gsi_competition_id] +} + +resource "aws_appautoscaling_policy" "write_policy" { + name = "DynamoDBWriteCapacityUtilization:${aws_appautoscaling_target.write_target_gsi_competition_id.resource_id}" + policy_type = "TargetTrackingScaling" + resource_id = aws_appautoscaling_target.write_target_gsi_competition_id.resource_id + scalable_dimension = aws_appautoscaling_target.write_target_gsi_competition_id.scalable_dimension + service_namespace = aws_appautoscaling_target.write_target_gsi_competition_id.service_namespace + + target_tracking_scaling_policy_configuration { + predefined_metric_specification { + predefined_metric_type = "DynamoDBWriteCapacityUtilization" + } + + target_value = 85 + scale_in_cooldown = 30 + scale_out_cooldown = 30 + } + + depends_on = [aws_appautoscaling_target.write_target_gsi_competition_id] +} + +resource "aws_appautoscaling_target" "read_target_gsi_user_id" { + max_capacity = 100 + min_capacity = 5 + resource_id = "table/${aws_dynamodb_table.registrations.name}/index/registrations_index_user_id" + scalable_dimension = "dynamodb:index:ReadCapacityUnits" + service_namespace = "dynamodb" +} + +resource "aws_appautoscaling_target" "write_target_gsi_user_id" { + max_capacity = 100 + min_capacity = 5 + resource_id = "table/${aws_dynamodb_table.registrations.name}/index/registrations_index_user_id" + scalable_dimension = "dynamodb:index:WriteCapacityUnits" + service_namespace = "dynamodb" +} + +resource "aws_appautoscaling_policy" "read_policy_user" { + name = "DynamoDBReadCapacityUtilization:${aws_appautoscaling_target.read_target_gsi_user_id.resource_id}" + policy_type = "TargetTrackingScaling" + resource_id = aws_appautoscaling_target.read_target_gsi_user_id.resource_id + scalable_dimension = aws_appautoscaling_target.read_target_gsi_user_id.scalable_dimension + service_namespace = aws_appautoscaling_target.read_target_gsi_user_id.service_namespace + + target_tracking_scaling_policy_configuration { + predefined_metric_specification { + predefined_metric_type = "DynamoDBReadCapacityUtilization" + } + + target_value = 85 + scale_in_cooldown = 30 + scale_out_cooldown = 30 + } + + depends_on = [aws_appautoscaling_target.read_target_gsi_user_id] +} + +resource "aws_appautoscaling_policy" "write_policy_user" { + name = "DynamoDBWriteCapacityUtilization:${aws_appautoscaling_target.write_target_gsi_user_id.resource_id}" + policy_type = "TargetTrackingScaling" + resource_id = aws_appautoscaling_target.write_target_gsi_user_id.resource_id + scalable_dimension = aws_appautoscaling_target.write_target_gsi_user_id.scalable_dimension + service_namespace = aws_appautoscaling_target.write_target_gsi_user_id.service_namespace + + target_tracking_scaling_policy_configuration { + predefined_metric_specification { + predefined_metric_type = "DynamoDBWriteCapacityUtilization" + } + + target_value = 85 + scale_in_cooldown = 30 + scale_out_cooldown = 30 + } + + depends_on = [aws_appautoscaling_target.write_target_gsi_user_id] +} + diff --git a/infra/staging/dynamodb.tf b/infra/staging/dynamodb.tf index 295f2e8a..8af22cc8 100644 --- a/infra/staging/dynamodb.tf +++ b/infra/staging/dynamodb.tf @@ -43,9 +43,7 @@ resource "aws_dynamodb_table" "registrations" { resource "aws_dynamodb_table" "registration_history" { name = "registrations_history-staging" - billing_mode = "PROVISIONED" - read_capacity = 5 - write_capacity = 5 + billing_mode = "PAY_PER_REQUEST" hash_key = "attendee_id" attribute { @@ -65,3 +63,18 @@ resource "aws_dynamodb_table" "registration_history" { Env = "staging" } } + +resource "aws_dynamodb_table" "waiting_list" { + name = "waiting_list-staging" + billing_mode = "PAY_PER_REQUEST" + hash_key = "id" + + attribute { + name = "id" + type = "S" + } + + tags = { + Env = "staging" + } +} diff --git a/infra/staging/main.tf b/infra/staging/main.tf index a8a067eb..9f84d446 100644 --- a/infra/staging/main.tf +++ b/infra/staging/main.tf @@ -44,6 +44,10 @@ locals { name = "REGISTRATION_HISTORY_DYNAMO_TABLE", value = aws_dynamodb_table.registration_history.name }, + { + name = "WAITING_LIST_DYNAMO_TABLE", + value = aws_dynamodb_table.waiting_list.name + }, { name = "PROMETHEUS_EXPORTER" value = var.prometheus_address @@ -111,7 +115,8 @@ data "aws_iam_policy_document" "task_policy" { "dynamodb:DescribeTable", ] resources = [aws_dynamodb_table.registrations.arn,"${aws_dynamodb_table.registrations.arn}/*", - aws_dynamodb_table.registration_history.arn,"${aws_dynamodb_table.registration_history.arn}/*"] + aws_dynamodb_table.registration_history.arn,"${aws_dynamodb_table.registration_history.arn}/*", + aws_dynamodb_table.waiting_list.arn,"${aws_dynamodb_table.waiting_list.arn}/*"] } statement { effect = "Allow" diff --git a/infra/worker/main.tf b/infra/worker/main.tf index ca2dbd7d..d3ef0ded 100644 --- a/infra/worker/main.tf +++ b/infra/worker/main.tf @@ -48,6 +48,10 @@ locals { name = "REGISTRATION_HISTORY_DYNAMO_TABLE", value = var.shared_resources.dynamo_registration_history_table.name }, + { + name = "WAITING_LIST_DYNAMO_TABLE", + value = var.shared_resources.dynamo_waiting_list_table.name + }, { name = "REDIS_URL" value = "redis://${var.shared_resources.aws_elasticache_cluster.cache_nodes.0.address}:${var.shared_resources.aws_elasticache_cluster.cache_nodes.0.port}" @@ -103,7 +107,8 @@ data "aws_iam_policy_document" "task_policy" { "dynamodb:DescribeTable", ] resources = [var.shared_resources.dynamo_registration_table.arn, "${var.shared_resources.dynamo_registration_table.arn}/*", - var.shared_resources.dynamo_registration_history_table.arn, "${var.shared_resources.dynamo_registration_history_table.arn}/*"] + var.shared_resources.dynamo_registration_history_table.arn, "${var.shared_resources.dynamo_registration_history_table.arn}/*", + var.shared_resources.dynamo_waiting_list_table.arn, "${var.shared_resources.dynamo_waiting_list_table.arn}/*"] } statement { effect = "Allow" diff --git a/infra/worker/variables.tf b/infra/worker/variables.tf index e8c055d9..223b4bbe 100644 --- a/infra/worker/variables.tf +++ b/infra/worker/variables.tf @@ -56,6 +56,10 @@ variable "shared_resources" { name: string, arn: string }), + dynamo_waiting_list_table: object({ + name: string, + arn: string + }), queue: object({ arn: string, url: string, diff --git a/lib/competition_info.rb b/lib/competition_info.rb index 59501562..90a5e3ad 100644 --- a/lib/competition_info.rb +++ b/lib/competition_info.rb @@ -7,6 +7,11 @@ def initialize(competition_json) @competition_json = competition_json @competition_id = competition_json['id'] @qualifications = fetch_qualifications + @waiting_list = nil + end + + def waiting_list + @waiting_list ||= WaitingList.find_or_create!(@competition_id) end def start_date diff --git a/lib/error_codes.rb b/lib/error_codes.rb index 1ed96e98..b42cb109 100644 --- a/lib/error_codes.rb +++ b/lib/error_codes.rb @@ -31,7 +31,6 @@ module ErrorCodes REGISTRATION_CLOSED = -4008 ORGANIZER_MUST_CANCEL_REGISTRATION = -4009 INVALID_WAITING_LIST_POSITION = -4010 - MUST_ACCEPT_WAITING_LIST_LEADER = -4011 QUALIFICATION_NOT_MET = -4012 # Payment Errors diff --git a/lib/lane.rb b/lib/lane.rb index d7894519..e87ebe8f 100644 --- a/lib/lane.rb +++ b/lib/lane.rb @@ -25,58 +25,6 @@ def self.dynamoid_load(serialized_str) Lane.new(parsed) end - def move_within_waiting_list(competition_id, new_position) - if new_position < lane_details['waiting_list_position'] - cascade_waiting_list(competition_id, new_position, lane_details['waiting_list_position']+1) - else - cascade_waiting_list(competition_id, lane_details['waiting_list_position'], new_position+1, -1) - end - lane_details['waiting_list_position'] = new_position - end - - def add_to_waiting_list(competition_id) - boundaries = get_waiting_list_boundaries(competition_id) - waiting_list_max = boundaries['waiting_list_position_max'] - waiting_list_min = boundaries['waiting_list_position_min'] - - if waiting_list_max.nil? && waiting_list_min.nil? - lane_details['waiting_list_position'] = 1 - else - lane_details['waiting_list_position'] = waiting_list_max+1 - end - end - - def remove_from_waiting_list(competition_id) - max_position = get_waiting_list_boundaries(competition_id)['waiting_list_position_max'] - cascade_waiting_list(competition_id, lane_details['waiting_list_position'], max_position+1, -1) - lane_details['waiting_list_position'] = nil - end - - def accept_from_waiting_list - lane_details['waiting_list_position'] = nil - end - - def get_waiting_list_boundaries(competition_id) - Rails.cache.fetch("#{competition_id}-waiting_list_boundaries", expires_in: 60.minutes) do - waiting_list_registrations = Registration.get_registrations_by_status(competition_id, 'waiting_list') - - # We aren't just counting the number of registrations in the waiting list. When a registration is - # accepted from the waiting list, we don't "move up" the waiting_list_position of the registrations - # behind it - so we can't assume that the position 1 is the min, or that the count of waiting_list - # registrations is the max. - - waiting_list_positions = waiting_list_registrations.map(&:competing_waiting_list_position).compact - - if waiting_list_positions.any? - waiting_list_position_min, waiting_list_position_max = waiting_list_positions.minmax - else - waiting_list_position_min = waiting_list_position_max = nil - end - - { 'waiting_list_position_min' => waiting_list_position_min, 'waiting_list_position_max' => waiting_list_position_max } - end - end - def update_events(new_event_ids) if @lane_name == 'competing' current_event_ids = @lane_details['event_details'].pluck('event_id') @@ -99,20 +47,4 @@ def update_events(new_event_ids) end end end - - private - - # Used for propagating a change in waiting_list_position to all affected registrations - # increment_value is the value by which position should be shifted - usually 1 or -1 - # Lower waiting_list_position = higher up the waiting list (1 on waiting list will be accepted before 10) - def cascade_waiting_list(competition_id, start_at, stop_at, increment_value = 1) - waiting_list_registrations = Registration.get_registrations_by_status(competition_id, 'waiting_list') - - waiting_list_registrations.each do |reg| - current_position = reg.competing_waiting_list_position.to_i - if current_position >= start_at && current_position < stop_at - reg.update_competing_waiting_list_position(current_position + increment_value) - end - end - end end diff --git a/spec/controllers/registration_controller_spec.rb b/spec/controllers/registration_controller_spec.rb index 64056e39..5c3dfb3b 100644 --- a/spec/controllers/registration_controller_spec.rb +++ b/spec/controllers/registration_controller_spec.rb @@ -136,7 +136,36 @@ ) end - # TODO: Consider refactor into separate contexts with one expect() per it-block + describe 'waiting list bulk updates' do + it 'accepts competitors from the waiting list' do + registration = FactoryBot.create(:registration, :waiting_list) + update = FactoryBot.build(:update_request, user_id: registration[:user_id], competing: { 'status' => 'accepted' }) + registration2 = FactoryBot.create(:registration, :waiting_list) + update2 = FactoryBot.build(:update_request, user_id: registration2[:user_id], competing: { 'status' => 'accepted' }) + registration3 = FactoryBot.create(:registration, :waiting_list) + update3 = FactoryBot.build(:update_request, user_id: registration3[:user_id], competing: { 'status' => 'accepted' }) + + waiting_list = FactoryBot.create(:waiting_list, entries: [registration.user_id, registration2.user_id, registration3.user_id]) + + updates = [update, update2, update3] + bulk_update_request = FactoryBot.build(:bulk_update_request, requests: updates) + + request.headers['Authorization'] = bulk_update_request['jwt_token'] + patch :bulk_update, params: bulk_update_request, as: :json + + expect(response.code).to eq('200') + + updated_registration = Registration.find("#{@competition['id']}-#{registration[:user_id]}") + expect(updated_registration.competing_status).to eq('accepted') + updated_registration2 = Registration.find("#{@competition['id']}-#{registration2[:user_id]}") + expect(updated_registration2.competing_status).to eq('accepted') + updated_registration3 = Registration.find("#{@competition['id']}-#{registration3[:user_id]}") + expect(updated_registration3.competing_status).to eq('accepted') + + expect(waiting_list.reload.entries.empty?).to eq(true) + end + end + it 'returns a 422 if there are validation errors' do registration = FactoryBot.create(:registration) update = FactoryBot.build(:update_request, user_id: registration[:user_id]) @@ -235,4 +264,71 @@ expect(response.code).to eq('400') end end + + describe '#list_admin', :tag do + before do + @competition = FactoryBot.build(:competition) + stub_json(CompetitionApi.url(@competition['id']), 200, @competition.except('qualifications')) + + @requesting_user_id = 1400 + stub_request(:get, UserApi.permissions_path(@requesting_user_id)).to_return( + status: 200, + body: FactoryBot.build(:permissions_response, organized_competitions: [@competition['id']]).to_json, + headers: { 'Content-Type' => 'application/json' }, + ) + end + + it 'returns single registration' do + registration = FactoryBot.create(:registration) + stub_pii([registration[:user_id]]) + + request.headers['Authorization'] = fetch_jwt_token(@requesting_user_id) + get :list_admin, params: { competition_id: @competition['id'] } + + expect(response.code).to eq('200') + + body = response.parsed_body + expect(body.count).to eq(1) + expect(body.first['user_id']).to eq(registration[:user_id]) + expect(body.first.dig('competing', 'registration_status')).to eq(registration.competing_status) + expect(body.first.dig('competing', 'waiting_list_position')).to eq(nil) + end + + it 'returns multiple registrations' do + reg = FactoryBot.create(:registration, :accepted) + reg2 = FactoryBot.create(:registration, :accepted) + reg3 = FactoryBot.create(:registration, :accepted) + + reg4 = FactoryBot.create(:registration, :waiting_list) + reg5 = FactoryBot.create(:registration, :waiting_list) + reg6 = FactoryBot.create(:registration, :waiting_list) + + FactoryBot.create(:waiting_list, entries: [reg4[:user_id], reg5[:user_id], reg6[:user_id]]) + + user_ids = [reg[:user_id], reg2[:user_id], reg3[:user_id], reg4[:user_id], reg5[:user_id], reg6[:user_id]] + stub_pii(user_ids) + + request.headers['Authorization'] = fetch_jwt_token(@requesting_user_id) + get :list_admin, params: { competition_id: @competition['id'] } + + expect(response.code).to eq('200') + + body = response.parsed_body + expect(body.count).to eq(6) + + body.each do |data| + expect(user_ids.include?(data['user_id'])).to eq(true) + if data['user_id'] == reg[:user_id] || data['user_id'] == reg2[:user_id] ||data['user_id'] == reg3[:user_id] + expect(data.dig('competing', 'registration_status')).to eq('accepted') + expect(data.dig('competing', 'waiting_list_position')).to eq(nil) + elsif data['user_id'] == reg4[:user_id] + expect(data.dig('competing', 'waiting_list_position')).to eq(1) + elsif data['user_id'] == reg5[:user_id] + expect(data.dig('competing', 'waiting_list_position')).to eq(2) + elsif data['user_id'] == reg6[:user_id] + expect(data.dig('competing', 'waiting_list_position')).to eq(3) + end + end + end + end end diff --git a/spec/factories/registration_factory.rb b/spec/factories/registration_factory.rb index 8d268017..dcb41a41 100644 --- a/spec/factories/registration_factory.rb +++ b/spec/factories/registration_factory.rb @@ -10,7 +10,6 @@ guests { 0 } registration_status { 'incoming' } organizer_comment { '' } - waiting_list_position { nil } end user_id { rand(100000..200000) } @@ -22,12 +21,19 @@ comment: comment, registration_status: registration_status, admin_comment: organizer_comment, - waiting_list_position: waiting_list_position, )] } history { association :registration_history, attendee_id: "#{competition_id}-#{user_id}" } end + trait :waiting_list do + registration_status { 'waiting_list' } + end + + trait :accepted do + registration_status { 'accepted' } + end + trait :admin do user_id { 15073 } end diff --git a/spec/factories/waiting_list_factory.rb b/spec/factories/waiting_list_factory.rb new file mode 100644 index 00000000..e50a58c2 --- /dev/null +++ b/spec/factories/waiting_list_factory.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'factory_bot_rails' + +FactoryBot.define do + factory :waiting_list do + transient do + populate { nil } + end + + id { 'CubingZANationalChampionship2023' } + entries { [] } + + after(:create) do |waiting_list, evaluator| + unless evaluator.populate.nil? + registrations = FactoryBot.create_list(:registration, evaluator.populate, registration_status: 'waiting_list') + registrations.each { |r| waiting_list.add(r.user_id) } + end + end + end +end diff --git a/spec/lib/competition_info_spec.rb b/spec/lib/competition_info_spec.rb index 20485d4d..9b50fcff 100644 --- a/spec/lib/competition_info_spec.rb +++ b/spec/lib/competition_info_spec.rb @@ -59,7 +59,7 @@ describe '#events_held?' do it 'PASSING true if competition is hosting given events' do # Create a sample competition JSON (adjust as needed) - competition_json = { 'event_ids' => ['333', '444', '555'] } + competition_json = FactoryBot.build(:competition, event_ids: %w[333 444 555]) # Instantiate a CompetitionInfo object with the sample data competition_info = CompetitionInfo.new(competition_json) @@ -73,7 +73,7 @@ it 'PASSING false if events list is empty' do # Create a sample competition JSON (adjust as needed) - competition_json = { 'event_ids' => ['333', '444', '555'] } + competition_json = FactoryBot.build(:competition, event_ids: %w[333 444 555]) # Instantiate a CompetitionInfo object with the sample data competition_info = CompetitionInfo.new(competition_json) @@ -87,7 +87,7 @@ it 'PASSING false if one of the events is not being hosted' do # Create a sample competition JSON (adjust as needed) - competition_json = { 'event_ids' => ['333', '444', '555'] } + competition_json = FactoryBot.build(:competition, event_ids: %w[333 444 555]) # Instantiate a CompetitionInfo object with the sample data competition_info = CompetitionInfo.new(competition_json) @@ -100,14 +100,8 @@ end it 'PASSING returns competition payment info' do - # Create a sample competition JSON (adjust as needed) - competition_json = { - 'base_entry_fee_lowest_denomination' => 1500, - 'currency_code' => 'EUR', - } - # Instantiate a CompetitionInfo object with the sample data - competition_info = CompetitionInfo.new(competition_json) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, base_entry_fee_lowest_denomination: 1500, currency_code: 'EUR')) # Call the method being tested result = competition_info.payment_info diff --git a/spec/models/registration_spec.rb b/spec/models/registration_spec.rb index 30221eef..18de5608 100644 --- a/spec/models/registration_spec.rb +++ b/spec/models/registration_spec.rb @@ -19,290 +19,118 @@ end describe '#update_competing_lane!' do + before do + @waiting_list = FactoryBot.create(:waiting_list) + end + 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' }) + registration.update_competing_lane!({ status: 'accepted' }, @waiting_list) expect(registration.competing_status).to eq('accepted') end it 'accepted given cancelled, it sets competing_status accordingly' do registration = FactoryBot.create(:registration, registration_status: 'accepted') - registration.update_competing_lane!({ status: 'cancelled' }) + registration.update_competing_lane!({ status: 'cancelled' }, @waiting_list) expect(registration.competing_status).to eq('cancelled') end it 'accepted given pending, it sets competing_status accordingly' do registration = FactoryBot.create(:registration, registration_status: 'accepted') - registration.update_competing_lane!({ status: 'pending' }) + registration.update_competing_lane!({ status: 'pending' }, @waiting_list) expect(registration.competing_status).to eq('pending') end it 'accepted given waiting_list, it sets competing_status' do registration = FactoryBot.create(:registration, registration_status: 'accepted') - registration.update_competing_lane!({ status: 'waiting_list' }) + registration.update_competing_lane!({ status: 'waiting_list' }, @waiting_list) expect(registration.competing_status).to eq('waiting_list') end end - describe '#update_competing_lane!.waiting_list' do - # TODO: Needs more logic to test whether the logic paths for update_waiting_list (status are same, not change in waiting list position, etc) - - describe '#waiting_list.get_waiting_list_boundaries' do - it 'both are nil when there are no other registrations' do - registration = FactoryBot.create(:registration, registration_status: 'pending') - - boundaries = registration.competing_lane.get_waiting_list_boundaries(registration.competition_id) - expect(boundaries['waiting_list_position_min']).to eq(nil) - expect(boundaries['waiting_list_position_max']).to eq(nil) - end - - it 'both are 1 when there is only one registrations' do - registration = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - - boundaries = registration.competing_lane.get_waiting_list_boundaries(registration.competition_id) - expect(boundaries['waiting_list_position_min']).to eq(1) - expect(boundaries['waiting_list_position_max']).to eq(1) - end - - it 'equal to lowest and highest position when there are multiple registrations' do - registration = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 2) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 3) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 4) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 5) - - boundaries = registration.competing_lane.get_waiting_list_boundaries(registration.competition_id) - expect(boundaries['waiting_list_position_min']).to eq(1) - expect(boundaries['waiting_list_position_max']).to eq(5) - end - end - - describe '#waiting_list.add_to_waiting_list' do - it 'first competitor in the waiting list gets set to position 1' do - registration = FactoryBot.create(:registration, registration_status: 'pending') - registration.update_competing_lane!({ status: 'waiting_list' }) - expect(registration.competing_waiting_list_position).to eq(1) - end - - it 'second competitor gets set to position 2' do - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - registration = FactoryBot.create(:registration, registration_status: 'pending') - registration.update_competing_lane!({ status: 'waiting_list' }) - expect(registration.competing_waiting_list_position).to eq(2) - end + describe '#competing_waiting_list_position' do + it '1st competitor is at position 1' do + registration = FactoryBot.create(:registration, registration_status: 'waiting_list') + waiting_list = FactoryBot.create(:waiting_list, entries: [registration.user_id]) + expect(registration.waiting_list_position(waiting_list)).to eq(1) end - describe '#waiting_list.caching_tests' do - let(:redis_store) { ActiveSupport::Cache.lookup_store(:redis_cache_store, { url: EnvConfig.REDIS_URL }) } - let(:cache) { Rails.cache } - - before do - allow(Rails).to receive(:cache).and_return(redis_store) - Rails.cache.clear - end - - describe '#waiting_list.cached_waiting_list_boundaries' do - it 'both are nil when there are no other registrations' do - registration = FactoryBot.create(:registration, registration_status: 'pending') - registration.competing_lane.get_waiting_list_boundaries(registration.competition_id) - - boundaries = Rails.cache.read("#{registration.competition_id}-waiting_list_boundaries") - expect(boundaries['waiting_list_position_min']).to eq(nil) - expect(boundaries['waiting_list_position_max']).to eq(nil) - end - - it 'both are 1 when there is only one registrations' do - registration = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - registration.competing_lane.get_waiting_list_boundaries(registration.competition_id) - - boundaries = Rails.cache.read("#{registration.competition_id}-waiting_list_boundaries") - expect(boundaries['waiting_list_position_min']).to eq(1) - expect(boundaries['waiting_list_position_max']).to eq(1) - end - - it 'equal to lowest and highest position when there are multiple registrations' do - registration = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 2) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 3) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 4) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 5) - registration.competing_lane.get_waiting_list_boundaries(registration.competition_id) - - boundaries = Rails.cache.read("#{registration.competition_id}-waiting_list_boundaries") - expect(boundaries['waiting_list_position_min']).to eq(1) - expect(boundaries['waiting_list_position_max']).to eq(5) - end - end - - describe '#waiting_list.verify_caches_after_updates' do - it 'is empty, then has 1 competitor after theyre added to waiting list' do - registration = FactoryBot.create(:registration, registration_status: 'pending') + it '5th competitor is at position 5' do + waiting_list = FactoryBot.create(:waiting_list, id: 'AnotherComp2024', populate: 4) + registration = FactoryBot.create(:registration, registration_status: 'waiting_list') + waiting_list.add(registration.user_id) - waiting_list_registrations = Rails.cache.read("#{registration.competition_id}-waiting_list_registrations") - expect(waiting_list_registrations).to eq(nil) - - registration.update_competing_lane!({ status: 'waiting_list' }) - - waiting_list_registrations = Rails.cache.read("#{registration.competition_id}-waiting_list_registrations") - expect(waiting_list_registrations.count).to eq(1) - end - - it 'two competitors yields an array of 2 stored in cache' do - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - registration = FactoryBot.create(:registration, registration_status: 'pending') - registration.update_competing_lane!({ status: 'waiting_list' }) - - waiting_list_registrations = Rails.cache.read("#{registration.competition_id}-waiting_list_registrations") - expect(waiting_list_registrations.count).to eq(2) - end - - it 'waiting list boundaries are updated after update_competing_lane is called' do - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - registration = FactoryBot.create(:registration, registration_status: 'pending') - registration.update_competing_lane!({ status: 'waiting_list' }) - - boundaries = Rails.cache.read("#{registration.competition_id}-waiting_list_boundaries") - expect(boundaries['waiting_list_position_min']).to eq(1) - expect(boundaries['waiting_list_position_max']).to eq(2) - end - end + expect(registration.waiting_list_position(waiting_list)).to eq(5) end + end - describe '#waiting_list.move_within_waiting_list' do - it 'gets moved to the correct position' do - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 2) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 3) - registration_4 = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 4) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 5) - - registration_4.update_competing_lane!({ waiting_list_position: '2' }) - registration_4.reload - - expect(registration_4.competing_waiting_list_position).to eq(2) - end - - it 'when moved forward on the list, everything between its new and old position gets moved back' do - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - registration_2 = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 2) - registration_3 = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 3) - registration_4 = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 4) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 5) - - registration_4.update_competing_lane!({ waiting_list_position: '2' }) - registration_4.reload - - registration_2.reload - registration_3.reload - - expect(registration_2.competing_waiting_list_position).to eq(3) - expect(registration_3.competing_waiting_list_position).to eq(4) - end - - it 'when moved forward, nothing outside the affected range changes' do - registration_1 = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 2) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 3) - registration_4 = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 4) - registration_5 = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 5) - - registration_4.update_competing_lane!({ waiting_list_position: 2 }) - registration_4.reload - - registration_1.reload - registration_5.reload - - expect(registration_1.competing_waiting_list_position).to eq(1) - expect(registration_5.competing_waiting_list_position).to eq(5) - end - - it 'if moved backward, everything, everything between new and old position gets moved forward' do - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - registration_2 = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 2) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 3) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 4) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 5) - - registration_2.update_competing_lane!({ waiting_list_position: 4 }) - registration_2.reload - - expect(registration_2.competing_waiting_list_position).to eq(4) - end - - it 'if moved backward, everything in front of its old position doesnt change' do - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - registration_2 = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 2) - registration_3 = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 3) - registration_4 = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 4) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 5) - - registration_2.update_competing_lane!({ waiting_list_position: 4 }) - registration_2.reload - - registration_3.reload - registration_4.reload + describe '#update_competing_lane!.waiting_list' do + # TODO: Needs more logic to test whether the logic paths for update_waiting_list (status are same, not change in waiting list position, etc) - expect(registration_3.competing_waiting_list_position).to eq(2) - expect(registration_4.competing_waiting_list_position).to eq(3) - end + before do + @reg1 = FactoryBot.create(:registration, :waiting_list) + @reg2 = FactoryBot.create(:registration, :waiting_list) + @waiting_list = FactoryBot.create(:waiting_list, entries: [@reg1.user_id, @reg2.user_id]) end - describe '#waiting_list.accept_from_waiting_list' do - it 'when accepted, waiting_list_position gets set to nil' do - registration = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - registration.update_competing_lane!({ status: 'accepted' }) - expect(registration.competing_waiting_list_position).to eq(nil) - end + describe '#waiting_list.accept' do + it 'accept waiting list leader' do + @reg1.update_competing_lane!({ status: 'accepted' }, @waiting_list) + @waiting_list.reload - it 'if waiting list is empty, new min/max should be nil' do - registration = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - registration.update_competing_lane!({ status: 'accepted' }) - waiting_list_boundaries = registration.competing_lane.get_waiting_list_boundaries(registration.competition_id) - expect(waiting_list_boundaries['waiting_list_position_min']).to eq(nil) - expect(waiting_list_boundaries['waiting_list_position_max']).to eq(nil) + expect(@reg1.competing_status).to eq('accepted') + expect(@reg1.waiting_list_position(@waiting_list)).to eq(nil) + expect(@reg2.waiting_list_position(@waiting_list)).to eq(1) + expect(@waiting_list.entries.include?(@reg1.user_id)).to eq(false) end - it 'if waiting list isnt empty, new min should be one greater than the accepted registrations old waiting list position' do - registration = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 2) - registration.update_competing_lane!({ status: 'accepted' }) - waiting_list_boundaries = registration.competing_lane.get_waiting_list_boundaries(registration.competition_id) - expect(waiting_list_boundaries['waiting_list_position_min']).to eq(2) - expect(waiting_list_boundaries['waiting_list_position_max']).to eq(2) + it 'cant accept if not in leading position of waiting list' do + expect { + @reg2.update_competing_lane!({ status: 'accepted' }, @waiting_list) + }.to raise_error(ArgumentError, 'Can only accept waiting list leader') end end - describe '#waiting_list.remove_from_waiting_list' do + describe '#waiting_list.remove' do it 'change from waiting_list to cancelled' do - registration = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - registration.update_competing_lane!({ status: 'cancelled' }) - expect(registration.competing_status).to eq('cancelled') + @reg1.update_competing_lane!({ status: 'cancelled' }, @waiting_list) + @waiting_list.reload + + expect(@reg1.competing_status).to eq('cancelled') + expect(@reg1.waiting_list_position(@waiting_list)).to eq(nil) + expect(@reg2.waiting_list_position(@waiting_list)).to eq(1) + expect(@waiting_list.entries.include?(@reg1.user_id)).to eq(false) end it 'change from waiting_list to pending' do - registration = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - registration.update_competing_lane!({ status: 'pending' }) - expect(registration.competing_status).to eq('pending') - end + @reg1.update_competing_lane!({ status: 'pending' }, @waiting_list) + @waiting_list.reload - it 'removing from waiting list changes waiting_list_position to nil' do - registration = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - registration.update_competing_lane!({ status: 'pending' }) - expect(registration.competing_waiting_list_position).to eq(nil) + expect(@reg1.competing_status).to eq('pending') + expect(@reg1.waiting_list_position(@waiting_list)).to eq(nil) + expect(@reg2.waiting_list_position(@waiting_list)).to eq(1) + expect(@waiting_list.entries.include?(@reg1.user_id)).to eq(false) end + end - it 'all registrations behind removed registration decrement their waiting_list_position' do - registration = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 1) - registration_2 = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 2) - registration_3 = FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 3) + describe '#waiting_list.move' do + it 'changing to waiting_list has no effect' do + @reg1.update_competing_lane!({ status: 'waiting_list' }, @waiting_list) + @waiting_list.reload - registration.update_competing_lane!({ status: 'pending' }) + expect(@reg1.competing_status).to eq('waiting_list') + expect(@reg1.waiting_list_position(@waiting_list)).to eq(1) + expect(@reg2.waiting_list_position(@waiting_list)).to eq(2) + expect(@waiting_list.entries.include?(@reg1.user_id)).to eq(true) + end - registration_2.reload - registration_3.reload + it 'can reorder waiting list items' do + @reg2.update_competing_lane!({ status: 'waiting_list', waiting_list_position: 1 }, @waiting_list) - expect(registration_2.competing_waiting_list_position).to eq(1) - expect(registration_3.competing_waiting_list_position).to eq(2) + expect(@reg1.competing_status).to eq('waiting_list') + expect(@reg1.waiting_list_position(@waiting_list)).to eq(2) + expect(@reg2.waiting_list_position(@waiting_list)).to eq(1) + expect(@waiting_list.entries.include?(@reg1.user_id)).to eq(true) end end end diff --git a/spec/models/waiting_list_spec.rb b/spec/models/waiting_list_spec.rb new file mode 100644 index 00000000..721caa28 --- /dev/null +++ b/spec/models/waiting_list_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe WaitingList do + describe '#add_to_waiting_list' do + before do + @waiting_list = FactoryBot.create(:waiting_list) + end + + it 'first competitor in the waiting list gets set to position 1' do + registration = FactoryBot.create(:registration, registration_status: 'pending') + registration.update_competing_lane!({ status: 'waiting_list' }, @waiting_list) + expect(registration.waiting_list_position(@waiting_list)).to eq(1) + end + + it 'second competitor gets set to position 2' do + @waiting_list.add(FactoryBot.create(:registration, :waiting_list).user_id) + registration = FactoryBot.create(:registration, registration_status: 'pending') + registration.update_competing_lane!({ status: 'waiting_list' }, @waiting_list) + expect(registration.waiting_list_position(@waiting_list)).to eq(2) + end + end + + describe '#waiting_list.move_within_waiting_list' do + before do + @waiting_list = FactoryBot.create(:waiting_list) + + @registration1 = FactoryBot.create(:registration, :waiting_list) + @registration2 = FactoryBot.create(:registration, :waiting_list) + @registration3 = FactoryBot.create(:registration, :waiting_list) + @registration4 = FactoryBot.create(:registration, :waiting_list) + @registration5 = FactoryBot.create(:registration, :waiting_list) + + @waiting_list.add(@registration1.user_id) + @waiting_list.add(@registration2.user_id) + @waiting_list.add(@registration3.user_id) + @waiting_list.add(@registration4.user_id) + @waiting_list.add(@registration5.user_id) + end + + it 'can be moved forward in the list' do + @waiting_list.move_to_position(@registration4.user_id, 2) + + expect(@registration1.waiting_list_position(@waiting_list)).to eq(1) + expect(@registration2.waiting_list_position(@waiting_list)).to eq(3) + expect(@registration3.waiting_list_position(@waiting_list)).to eq(4) + expect(@registration4.waiting_list_position(@waiting_list)).to eq(2) + expect(@registration5.waiting_list_position(@waiting_list)).to eq(5) + end + + it 'can be moved backward in the list' do + @waiting_list.move_to_position(@registration2.user_id, 5) + + expect(@registration1.waiting_list_position(@waiting_list)).to eq(1) + expect(@registration2.waiting_list_position(@waiting_list)).to eq(5) + expect(@registration3.waiting_list_position(@waiting_list)).to eq(2) + expect(@registration4.waiting_list_position(@waiting_list)).to eq(3) + expect(@registration5.waiting_list_position(@waiting_list)).to eq(4) + end + + it 'can be moved to the first position in the list' do + @waiting_list.move_to_position(@registration5.user_id, 1) + + expect(@registration1.waiting_list_position(@waiting_list)).to eq(2) + expect(@registration2.waiting_list_position(@waiting_list)).to eq(3) + expect(@registration3.waiting_list_position(@waiting_list)).to eq(4) + expect(@registration4.waiting_list_position(@waiting_list)).to eq(5) + expect(@registration5.waiting_list_position(@waiting_list)).to eq(1) + end + + it 'nothing happens if you move an item to its current position' do + @waiting_list.move_to_position(@registration3.user_id, 3) + + expect(@registration1.waiting_list_position(@waiting_list)).to eq(1) + expect(@registration2.waiting_list_position(@waiting_list)).to eq(2) + expect(@registration3.waiting_list_position(@waiting_list)).to eq(3) + expect(@registration4.waiting_list_position(@waiting_list)).to eq(4) + expect(@registration5.waiting_list_position(@waiting_list)).to eq(5) + end + + it 'can be moved to the last position in the list' do + @waiting_list.move_to_position(@registration2.user_id, 5) + + expect(@registration1.waiting_list_position(@waiting_list)).to eq(1) + expect(@registration2.waiting_list_position(@waiting_list)).to eq(5) + expect(@registration3.waiting_list_position(@waiting_list)).to eq(2) + expect(@registration4.waiting_list_position(@waiting_list)).to eq(3) + expect(@registration5.waiting_list_position(@waiting_list)).to eq(4) + end + + it 'cant be moved to a position greater than the list length' do + expect { + @waiting_list.move_to_position(@registration2.user_id, 6) + }.to raise_error(ArgumentError, 'Target position out of waiting list range') + end + + it 'cant be moved to a negative position' do + expect { + @waiting_list.move_to_position(@registration2.user_id, -1) + }.to raise_error(ArgumentError, 'Target position out of waiting list range') + end + + it 'cant be moved to position 0' do + expect { + @waiting_list.move_to_position(@registration2.user_id, 0) + }.to raise_error(ArgumentError, 'Target position out of waiting list range') + end + end +end diff --git a/spec/services/registration_checker_spec.rb b/spec/services/registration_checker_spec.rb index 4da71007..339cf133 100644 --- a/spec/services/registration_checker_spec.rb +++ b/spec/services/registration_checker_spec.rb @@ -701,6 +701,11 @@ it 'user cant update registration if registration edits arent allowed' do override_competition_info = CompetitionInfo.new(FactoryBot.build(:competition, allow_registration_edits: false)) update_request = FactoryBot.build(:update_request, user_id: @registration[:user_id]) + stub_request(:get, UserApi.permissions_path(update_request['submitted_by'])).to_return( + status: 200, + body: FactoryBot.build(:permissions_response).to_json, + headers: { content_type: 'application/json' }, + ) expect { RegistrationChecker.update_registration_allowed!(update_request, override_competition_info, update_request['submitted_by']) @@ -713,6 +718,11 @@ it 'user cant change events after event change deadline' do override_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'] }) + stub_request(:get, UserApi.permissions_path(update_request['submitted_by'])).to_return( + status: 200, + body: FactoryBot.build(:permissions_response).to_json, + headers: { content_type: 'application/json' }, + ) expect { RegistrationChecker.update_registration_allowed!(update_request, override_competition_info, update_request['submitted_by']) @@ -837,6 +847,11 @@ it 'user cant change comment after edit events deadline' do override_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' }) + stub_request(:get, UserApi.permissions_path(update_request['submitted_by'])).to_return( + status: 200, + body: FactoryBot.build(:permissions_response).to_json, + headers: { content_type: 'application/json' }, + ) expect { RegistrationChecker.update_registration_allowed!(update_request, override_competition_info, update_request['submitted_by']) @@ -870,7 +885,11 @@ it 'user cant submit an organizer comment' do update_request = FactoryBot.build(:update_request, user_id: @registration[:user_id], competing: { 'organizer_comment' => 'new admin comment' }) - + stub_request(:get, UserApi.permissions_path(update_request['submitted_by'])).to_return( + status: 200, + body: FactoryBot.build(:permissions_response).to_json, + headers: { content_type: 'application/json' }, + ) expect { RegistrationChecker.update_registration_allowed!(update_request, @competition_info, update_request['submitted_by']) }.to raise_error(RegistrationError) do |error| @@ -881,7 +900,11 @@ it 'user cant submit waiting_list_position' do update_request = FactoryBot.build(:update_request, user_id: @registration[:user_id], competing: { 'waiting_list_position' => '1' }) - + stub_request(:get, UserApi.permissions_path(update_request['submitted_by'])).to_return( + status: 200, + body: FactoryBot.build(:permissions_response).to_json, + headers: { content_type: 'application/json' }, + ) expect { RegistrationChecker.update_registration_allowed!(update_request, @competition_info, update_request['submitted_by']) }.to raise_error(RegistrationError) do |error| @@ -996,7 +1019,11 @@ it 'user cant change guests after registration change deadline' do override_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) - + stub_request(:get, UserApi.permissions_path(update_request['submitted_by'])).to_return( + status: 200, + body: FactoryBot.build(:permissions_response).to_json, + headers: { content_type: 'application/json' }, + ) expect { RegistrationChecker.update_registration_allowed!(update_request, override_competition_info, update_request['submitted_by']) }.to raise_error(RegistrationError) do |error| @@ -1055,7 +1082,7 @@ 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') + registration = FactoryBot.create(:registration, registration_status: 'pending') 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' }) @@ -1153,6 +1180,11 @@ it 'user cant cancel registration after registration ends' do override_competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :closed)) update_request = FactoryBot.build(:update_request, user_id: @registration[:user_id], competing: { 'status' => 'cancelled' }) + stub_request(:get, UserApi.permissions_path(update_request['submitted_by'])).to_return( + status: 200, + body: FactoryBot.build(:permissions_response).to_json, + headers: { content_type: 'application/json' }, + ) expect { RegistrationChecker.update_registration_allowed!(update_request, override_competition_info, update_request['submitted_by']) @@ -1319,6 +1351,10 @@ end describe '#update_registration_allowed!.validate_waiting_list_position!' do + before do + @waiting_list = @competition_info.waiting_list + end + it 'must be an integer, not string' do update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: @registration[:user_id], competing: { 'waiting_list_position' => 'b' }) @@ -1331,6 +1367,7 @@ end it 'can be an integer given as a string' do + @waiting_list.add(@registration.user_id) update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: @registration[:user_id], competing: { 'waiting_list_position' => '1' }) expect { @@ -1349,27 +1386,15 @@ end end - it 'organizer cant accept anyone except the min position on the waiting list' do - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => '1') - override_registration = FactoryBot.create(:registration, user_id: 188000, registration_status: 'waiting_list', 'waiting_list_position' => '2') - update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: override_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.http_status).to eq(:forbidden) - expect(error.error).to eq(ErrorCodes::MUST_ACCEPT_WAITING_LIST_LEADER) - end - end - - it 'cannot move to less than current min position' do - override_registration = FactoryBot.create(:registration, user_id: 188000, registration_status: 'waiting_list', 'waiting_list_position' => 1) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 2) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 3) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 4) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 5) + it 'cannot move to less than position 1', :tag2 do + @waiting_list.add(FactoryBot.create(:registration, registration_status: 'waiting_list').user_id) + @waiting_list.add(FactoryBot.create(:registration, registration_status: 'waiting_list').user_id) + @waiting_list.add(FactoryBot.create(:registration, registration_status: 'waiting_list').user_id) + @waiting_list.add(FactoryBot.create(:registration, registration_status: 'waiting_list').user_id) + override_registration = FactoryBot.create(:registration, user_id: 188000, registration_status: 'waiting_list') + @waiting_list.add(override_registration.user_id) - update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: override_registration[:user_id], competing: { 'waiting_list_position' => '10' }) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: override_registration[:user_id], competing: { 'waiting_list_position' => '0' }) expect { RegistrationChecker.update_registration_allowed!(update_request, @competition_info, update_request['submitted_by']) @@ -1379,14 +1404,14 @@ end end - it 'cannot move to greater than current max position' do - override_registration = FactoryBot.create(:registration, user_id: 188000, registration_status: 'waiting_list', 'waiting_list_position' => 6) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 2) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 3) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 4) - FactoryBot.create(:registration, registration_status: 'waiting_list', 'waiting_list_position' => 5) + it 'cannot move to greater than the number of items in the waiting list' do + override_registration = FactoryBot.create(:registration, user_id: 188000, registration_status: 'waiting_list') + @waiting_list.add(FactoryBot.create(:registration, registration_status: 'waiting_list').user_id) + @waiting_list.add(FactoryBot.create(:registration, registration_status: 'waiting_list').user_id) + @waiting_list.add(FactoryBot.create(:registration, registration_status: 'waiting_list').user_id) + @waiting_list.add(FactoryBot.create(:registration, registration_status: 'waiting_list').user_id) - update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: override_registration[:user_id], competing: { 'waiting_list_position' => '1' }) + update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: override_registration[:user_id], competing: { 'waiting_list_position' => '10' }) expect { RegistrationChecker.update_registration_allowed!(update_request, @competition_info, update_request['submitted_by']) diff --git a/spec/support/stub_helper.rb b/spec/support/stub_helper.rb index c6f46bce..79e8eace 100644 --- a/spec/support/stub_helper.rb +++ b/spec/support/stub_helper.rb @@ -8,6 +8,18 @@ def stub_json(url, response_code, payload, type = :get) ) end +def stub_pii(user_ids) + user_pii = user_ids.map do |user_id| + { + id: user_id, + email: "#{user_id}@example.com", + dob: '1950-04-04', + } + end + + stub_json(UserApi.competitor_info_path, 200, user_pii, :post) +end + def stub_qualifications(payload = nil, qualification_data_date = nil) url_regex = %r{#{Regexp.escape(EnvConfig.WCA_HOST)}/api/v0/results/\d+/qualification_data(\?date=\d{4}-\d{2}-\d{2})?} stub_request(:get, url_regex).to_return do |request|