From f6709be280260afe47f65423dc37a64d838c8caa Mon Sep 17 00:00:00 2001 From: Artur Beljajev Date: Thu, 29 Nov 2018 15:08:22 +0200 Subject: [PATCH] Integrate auction Closes #874 --- app/controllers/api/v1/auctions_controller.rb | 48 ++++ app/controllers/api/v1/base_controller.rb | 18 ++ app/controllers/epp/domains_controller.rb | 69 ++++- app/models/auction.rb | 60 +++++ app/models/concerns/domain/discardable.rb | 15 -- app/models/concerns/domain/releasable.rb | 42 +++ app/models/dns/domain_name.rb | 64 +++++ app/models/domain.rb | 5 + app/models/epp/domain.rb | 10 + app/models/whois/record.rb | 10 + .../epp/domains/info_auction.xml.builder | 16 ++ config/application.rb | 3 + config/auction.yml | 9 + config/domains.yml | 9 + config/locales/en.yml | 2 + config/routes.rb | 2 + .../20181129150515_create_released_domains.rb | 8 + db/migrate/20181212105100_create_auctions.rb | 27 ++ ...1212145456_change_auctions_uuid_default.rb | 5 + ...145914_change_auctions_uuid_to_not_null.rb | 5 + ...rename_released_domains_to_domain_names.rb | 5 + ...ame_domain_names_to_auctionable_domains.rb | 5 + ...5_remove_auctionable_domains_at_auction.rb | 5 + ...181220094738_remove_auctionable_domains.rb | 5 + ...53_remove_auctions_not_null_constraints.rb | 5 + ...23153407_add_auctions_registration_code.rb | 5 + .../20181226211337_change_auctions_status.rb | 9 + ..._payment_not_received_to_auction_status.rb | 9 + ...172042_change_auctions_status_to_string.rb | 9 + ...tions_registration_code_uniq_constraint.rb | 13 + .../20190102114702_change_auctions_uuid.rb | 5 + ...15333_add_auctions_uuid_uniq_constraint.rb | 13 + db/structure.sql | 96 +++++++ doc/api/v1/auctions.md | 72 +++++ doc/epp/domain.md | 3 +- lib/tasks/domain.rake | 13 - lib/tasks/domains/release.rake | 17 ++ test/fixtures/auctions.yml | 4 + .../api/v1/auctions/details_test.rb | 22 ++ test/integration/api/v1/auctions/list_test.rb | 30 +++ .../api/v1/auctions/update_test.rb | 83 ++++++ .../epp/domain/check/auction_test.rb | 87 ++++++ .../epp/domain/create/auction_test.rb | 248 ++++++++++++++++++ .../epp/domain/info/auction_test.rb | 87 ++++++ test/models/auction_test.rb | 112 ++++++++ test/models/dns/domain_name_test.rb | 90 +++++++ test/models/domain/domain_test.rb | 5 + .../domain/releasable/auctionable_test.rb | 51 ++++ .../domain/releasable/discardable_test.rb} | 44 ++-- test/models/domain/releasable_test.rb | 11 + test/models/whois/record_test.rb | 45 ++++ test/tasks/domains/release_test.rb | 19 ++ test/test_helper.rb | 5 + 53 files changed, 1607 insertions(+), 52 deletions(-) create mode 100644 app/controllers/api/v1/auctions_controller.rb create mode 100644 app/controllers/api/v1/base_controller.rb create mode 100644 app/models/auction.rb create mode 100644 app/models/concerns/domain/releasable.rb create mode 100644 app/models/dns/domain_name.rb create mode 100644 app/views/epp/domains/info_auction.xml.builder create mode 100644 config/auction.yml create mode 100644 config/domains.yml create mode 100644 db/migrate/20181129150515_create_released_domains.rb create mode 100644 db/migrate/20181212105100_create_auctions.rb create mode 100644 db/migrate/20181212145456_change_auctions_uuid_default.rb create mode 100644 db/migrate/20181212145914_change_auctions_uuid_to_not_null.rb create mode 100644 db/migrate/20181213113115_rename_released_domains_to_domain_names.rb create mode 100644 db/migrate/20181217144701_rename_domain_names_to_auctionable_domains.rb create mode 100644 db/migrate/20181217144845_remove_auctionable_domains_at_auction.rb create mode 100644 db/migrate/20181220094738_remove_auctionable_domains.rb create mode 100644 db/migrate/20181220095053_remove_auctions_not_null_constraints.rb create mode 100644 db/migrate/20181223153407_add_auctions_registration_code.rb create mode 100644 db/migrate/20181226211337_change_auctions_status.rb create mode 100644 db/migrate/20181227155537_add_payment_not_received_to_auction_status.rb create mode 100644 db/migrate/20181227172042_change_auctions_status_to_string.rb create mode 100644 db/migrate/20181230231015_add_auctions_registration_code_uniq_constraint.rb create mode 100644 db/migrate/20190102114702_change_auctions_uuid.rb create mode 100644 db/migrate/20190102115333_add_auctions_uuid_uniq_constraint.rb create mode 100644 doc/api/v1/auctions.md delete mode 100644 lib/tasks/domain.rake create mode 100644 lib/tasks/domains/release.rake create mode 100644 test/fixtures/auctions.yml create mode 100644 test/integration/api/v1/auctions/details_test.rb create mode 100644 test/integration/api/v1/auctions/list_test.rb create mode 100644 test/integration/api/v1/auctions/update_test.rb create mode 100644 test/integration/epp/domain/check/auction_test.rb create mode 100644 test/integration/epp/domain/create/auction_test.rb create mode 100644 test/integration/epp/domain/info/auction_test.rb create mode 100644 test/models/auction_test.rb create mode 100644 test/models/dns/domain_name_test.rb create mode 100644 test/models/domain/releasable/auctionable_test.rb rename test/{integration/tasks/discard_domain_test.rb => models/domain/releasable/discardable_test.rb} (51%) create mode 100644 test/models/domain/releasable_test.rb create mode 100644 test/models/whois/record_test.rb create mode 100644 test/tasks/domains/release_test.rb diff --git a/app/controllers/api/v1/auctions_controller.rb b/app/controllers/api/v1/auctions_controller.rb new file mode 100644 index 0000000000..f0876cd777 --- /dev/null +++ b/app/controllers/api/v1/auctions_controller.rb @@ -0,0 +1,48 @@ +module Api + module V1 + class AuctionsController < BaseController + before_action :authenticate, except: :index + + def index + render json: Auction.started.map { |auction| serializable_hash(auction) } + end + + def show + auction = Auction.find_by(uuid: params[:uuid]) + render json: serializable_hash(auction) + end + + def update + auction = Auction.find_by(uuid: params[:uuid]) + auction.update!(updatable_params) + + if auction.no_bids? + auction.mark_as_no_bids + elsif auction.payment_received? + auction.mark_as_payment_received + elsif auction.payment_not_received? + auction.mark_as_payment_not_received + end + + render json: serializable_hash_for_update_action(auction) + end + + private + + def updatable_params + return unless Auction.statuses.values.include?(params[:status]) + params.permit(:status) + end + + def serializable_hash(auction) + { id: auction.uuid, domain: auction.domain, status: auction.status } + end + + def serializable_hash_for_update_action(auction) + hash = serializable_hash(auction) + hash[:registration_code] = auction.registration_code if auction.payment_received? + hash + end + end + end +end diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb new file mode 100644 index 0000000000..3153694be4 --- /dev/null +++ b/app/controllers/api/v1/base_controller.rb @@ -0,0 +1,18 @@ +require 'rails5_api_controller_backport' + +module Api + module V1 + class BaseController < ActionController::API + private + + def authenticate + ip_allowed = allowed_ips.include?(request.remote_ip) + head :unauthorized unless ip_allowed + end + + def allowed_ips + Rails.configuration.auction.api_allowed_ips.split(',').map(&:strip) + end + end + end +end diff --git a/app/controllers/epp/domains_controller.rb b/app/controllers/epp/domains_controller.rb index 12b2277ef8..a28405a2df 100644 --- a/app/controllers/epp/domains_controller.rb +++ b/app/controllers/epp/domains_controller.rb @@ -1,9 +1,34 @@ class Epp::DomainsController < EppController - before_action :find_domain, only: [:info, :renew, :update, :transfer, :delete] - before_action :find_password, only: [:info, :update, :transfer, :delete] + before_action :find_domain, only: [:renew, :update, :transfer, :delete] + before_action :find_password, only: [:update, :transfer, :delete] + skip_authorization_check only: :info def info + if Domain.release_to_auction + domain_name = DNS::DomainName.new(params[:parsed_frame].at_css('name').text.strip.downcase) + + if domain_name.at_auction? + @name = domain_name + @status = 'At auction' + render_epp_response '/epp/domains/info_auction' + return + elsif domain_name.awaiting_payment? + @name = domain_name + @status = 'Awaiting payment' + render_epp_response '/epp/domains/info_auction' + return + elsif domain_name.pending_registration? + @name = domain_name + @status = 'Reserved' + render_epp_response '/epp/domains/info_auction' + return + end + end + + find_domain + find_password authorize! :info, @domain, @password + @hosts = params[:parsed_frame].css('name').first['hosts'] || 'all' case @hosts @@ -20,6 +45,40 @@ def info def create authorize! :create, Epp::Domain + + if Domain.release_to_auction + request_domain_name = params[:parsed_frame].css('name').text.strip.downcase + domain_name = DNS::DomainName.new(request_domain_name) + + if domain_name.at_auction? + throw :epp_error, { + code: '2306', + msg: 'Parameter value policy error: domain is at auction', + } + elsif domain_name.awaiting_payment? + throw :epp_error, { + code: '2003', + msg: I18n.t('activerecord.errors.models.epp_domain.attributes.base.required_parameter_missing_reserved'), + } + elsif domain_name.pending_registration? + registration_code = params[:parsed_frame].css('reserved > pw').text + + if registration_code.empty? + throw :epp_error, { + code: '2003', + msg: 'Required parameter missing; reserved>pw element is required' + } + end + + unless domain_name.available_with_code?(registration_code) + throw :epp_error, { + code: '2202', + msg: I18n.t('activerecord.errors.models.epp_domain.attributes.base.invalid_auth_information_reserved'), + } + end + end + end + @domain = Epp::Domain.new_from_epp(params[:parsed_frame], current_user) handle_errors(@domain) and return if @domain.errors.any? @domain.valid? @@ -38,6 +97,12 @@ def create price: @domain_pricelist }) + if Domain.release_to_auction && domain_name.pending_registration? + active_auction = Auction.find_by(domain: domain_name.to_s, + status: Auction.statuses[:payment_received]) + active_auction.domain_registered! + end + render_epp_response '/epp/domains/create' else handle_errors(@domain) diff --git a/app/models/auction.rb b/app/models/auction.rb new file mode 100644 index 0000000000..8ac05d9b23 --- /dev/null +++ b/app/models/auction.rb @@ -0,0 +1,60 @@ +class Auction < ActiveRecord::Base + enum status: { + started: 'started', + awaiting_payment: 'awaiting_payment', + no_bids: 'no_bids', + payment_received: 'payment_received', + payment_not_received: 'payment_not_received', + domain_registered: 'domain_registered' + } + + PENDING_STATUSES = [statuses[:started], + statuses[:awaiting_payment], + statuses[:payment_received]].freeze + private_constant :PENDING_STATUSES + + def self.sell(domain_name) + create!(domain: domain_name.to_s, status: statuses[:started]) + end + + def self.pending(domain_name) + find_by(domain: domain_name.to_s, status: PENDING_STATUSES) + end + + def mark_as_no_bids + DNS::DomainName.new(domain).update_whois + end + + def mark_as_payment_received + self.status = self.class.statuses[:payment_received] + generate_registration_code + save! + end + + def mark_as_payment_not_received + self.status = self.class.statuses[:payment_not_received] + + transaction do + save! + restart + end + end + + def domain_registrable?(registration_code = nil) + payment_received? && registration_code_matches?(registration_code) + end + + private + + def generate_registration_code + self.registration_code = SecureRandom.hex + end + + def restart + self.class.create!(domain: domain, status: self.class.statuses[:started]) + end + + def registration_code_matches?(code) + registration_code == code + end +end diff --git a/app/models/concerns/domain/discardable.rb b/app/models/concerns/domain/discardable.rb index e464922206..bf2b3d8c4c 100644 --- a/app/models/concerns/domain/discardable.rb +++ b/app/models/concerns/domain/discardable.rb @@ -1,21 +1,6 @@ module Concerns::Domain::Discardable extend ActiveSupport::Concern - class_methods do - def discard_domains - domains = where('delete_at < ? AND ? != ALL(coalesce(statuses, array[]::varchar[])) AND' \ - ' ? != ALL(COALESCE(statuses, array[]::varchar[]))', - Time.zone.now, - DomainStatus::SERVER_DELETE_PROHIBITED, - DomainStatus::DELETE_CANDIDATE) - - domains.each do |domain| - domain.discard - yield domain if block_given? - end - end - end - def discard raise 'Domain is already discarded' if discarded? diff --git a/app/models/concerns/domain/releasable.rb b/app/models/concerns/domain/releasable.rb new file mode 100644 index 0000000000..ad1e3af750 --- /dev/null +++ b/app/models/concerns/domain/releasable.rb @@ -0,0 +1,42 @@ +module Concerns::Domain::Releasable + extend ActiveSupport::Concern + + class_methods do + def release_domains + releasable_domains.each do |domain| + domain.release + yield domain if block_given? + end + end + + private + + def releasable_domains + if release_to_auction + where('delete_at < ? AND ? != ALL(coalesce(statuses, array[]::varchar[]))', + Time.zone.now, + DomainStatus::SERVER_DELETE_PROHIBITED) + else + where('delete_at < ? AND ? != ALL(coalesce(statuses, array[]::varchar[])) AND' \ + ' ? != ALL(COALESCE(statuses, array[]::varchar[]))', + Time.zone.now, + DomainStatus::SERVER_DELETE_PROHIBITED, + DomainStatus::DELETE_CANDIDATE) + end + end + end + + included do + mattr_accessor :release_to_auction + self.release_to_auction = Rails.configuration.domains.release_to_auction + end + + def release + if release_to_auction + domain_name.sell_at_auction + destroy! + else + discard + end + end +end diff --git a/app/models/dns/domain_name.rb b/app/models/dns/domain_name.rb new file mode 100644 index 0000000000..2b9edba609 --- /dev/null +++ b/app/models/dns/domain_name.rb @@ -0,0 +1,64 @@ +require 'observer' + +module DNS + # Namespace is needed, because a class with the same name is defined by `domain_name` gem, + # a dependency of `actionmailer`, + class DomainName + def initialize(name) + @name = name + end + + def available? + !unavailable? + end + + def available_with_code?(code) + pending_auction.domain_registrable?(code) + end + + def unavailable? + at_auction? || awaiting_payment? + end + + def unavailability_reason + if at_auction? + :at_auction + elsif awaiting_payment? + :awaiting_payment + end + end + + def sell_at_auction + Auction.sell(self) + update_whois + end + + def at_auction? + pending_auction&.started? + end + + def awaiting_payment? + pending_auction&.awaiting_payment? + end + + def pending_registration? + pending_auction&.payment_received? + end + + def update_whois + Whois::Record.refresh(self) + end + + def to_s + name + end + + private + + attr_reader :name + + def pending_auction + Auction.pending(self) + end + end +end diff --git a/app/models/domain.rb b/app/models/domain.rb index 50ab622421..4e65630cd9 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -8,6 +8,7 @@ class Domain < ActiveRecord::Base include Concerns::Domain::Deletable include Concerns::Domain::Transferable include Concerns::Domain::RegistryLockable + include Concerns::Domain::Releasable has_paper_trail class_name: "DomainVersion", meta: { children: :children_log } @@ -582,6 +583,10 @@ def as_json(_options) hash end + def domain_name + DNS::DomainName.new(name) + end + def self.to_csv CSV.generate do |csv| csv << column_names diff --git a/app/models/epp/domain.rb b/app/models/epp/domain.rb index 2f25f3da59..0d44a2b983 100644 --- a/app/models/epp/domain.rb +++ b/app/models/epp/domain.rb @@ -800,6 +800,16 @@ def check_availability(domains) next end + if Domain.release_to_auction + domain_name = DNS::DomainName.new(x) + + if domain_name.unavailable? + reason = I18n.t("errors.messages.epp_domain_#{domain_name.unavailability_reason}") + res << { name: x, avail: 0, reason: reason } + next + end + end + if ReservedDomain.pw_for(x).present? res << { name: x, avail: 0, reason: I18n.t('errors.messages.epp_domain_reserved') } next diff --git a/app/models/whois/record.rb b/app/models/whois/record.rb index 08a92b6e19..b92d27186f 100644 --- a/app/models/whois/record.rb +++ b/app/models/whois/record.rb @@ -1,5 +1,15 @@ module Whois class Record < Whois::Server self.table_name = 'whois_records' + + def self.refresh(domain_name) + if domain_name.at_auction? + create!(name: domain_name, json: { name: domain_name.to_s, status: 'AtAuction' }) + elsif domain_name.awaiting_payment? || domain_name.pending_registration? + create!(name: domain_name, json: { name: domain_name.to_s, status: 'PendingRegistration' }) + else + find_by(name: domain_name.to_s).destroy! + end + end end end diff --git a/app/views/epp/domains/info_auction.xml.builder b/app/views/epp/domains/info_auction.xml.builder new file mode 100644 index 0000000000..95fe757d4f --- /dev/null +++ b/app/views/epp/domains/info_auction.xml.builder @@ -0,0 +1,16 @@ +xml.epp_head do + xml.response do + xml.result('code' => '1000') do + xml.msg 'Command completed successfully' + end + + xml.resData do + xml.tag!('domain:infData', 'xmlns:domain' => 'https://epp.tld.ee/schema/domain-eis-1.0.xsd') do + xml.tag!('domain:name', @name) + xml.tag!('domain:status', 's' => @status) + end + end + + render('epp/shared/trID', builder: xml) + end +end diff --git a/config/application.rb b/config/application.rb index 1420d3cd3a..f7455207ef 100644 --- a/config/application.rb +++ b/config/application.rb @@ -90,6 +90,9 @@ class Application < Rails::Application config.action_view.default_form_builder = 'DefaultFormBuilder' config.secret_key_base = Figaro.env.secret_key_base + + config.domains = OpenStruct.new(config_for(:domains)) + config.auction = OpenStruct.new(config_for(:auction)) end end diff --git a/config/auction.yml b/config/auction.yml new file mode 100644 index 0000000000..e13ae3b3a0 --- /dev/null +++ b/config/auction.yml @@ -0,0 +1,9 @@ +default: &default + api_allowed_ips: '192.0.2.0, 192.0.2.1' + +development: + <<: *default +test: + <<: *default +production: + <<: *default diff --git a/config/domains.yml b/config/domains.yml new file mode 100644 index 0000000000..10637bf950 --- /dev/null +++ b/config/domains.yml @@ -0,0 +1,9 @@ +default: &default + release_to_auction: false + +development: + <<: *default +test: + <<: *default +production: + <<: *default diff --git a/config/locales/en.yml b/config/locales/en.yml index e9e499def0..73a9396c34 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -197,6 +197,8 @@ en: messages: blank: 'is missing' epp_domain_reserved: 'Domain name is reserved' + epp_domain_at_auction: Domain is at auction + epp_domain_awaiting_payment: Awaiting payment epp_obj_does_not_exist: 'Object does not exist' epp_authorization_error: 'Authorization error' epp_id_taken: 'Contact id already exists' diff --git a/config/routes.rb b/config/routes.rb index 67c76c16a6..4f03ffae8b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,6 +28,8 @@ end resources :contacts, only: %i[index show update], param: :uuid end + + resources :auctions, only: %i[index show update], param: :uuid end match '*all', controller: 'cors', action: 'cors_preflight_check', via: [:options], diff --git a/db/migrate/20181129150515_create_released_domains.rb b/db/migrate/20181129150515_create_released_domains.rb new file mode 100644 index 0000000000..bc0355eb14 --- /dev/null +++ b/db/migrate/20181129150515_create_released_domains.rb @@ -0,0 +1,8 @@ +class CreateReleasedDomains < ActiveRecord::Migration + def change + create_table :released_domains do |t| + t.string :name, null: false + t.boolean :at_auction, default: false, null: false + end + end +end diff --git a/db/migrate/20181212105100_create_auctions.rb b/db/migrate/20181212105100_create_auctions.rb new file mode 100644 index 0000000000..57223c89ca --- /dev/null +++ b/db/migrate/20181212105100_create_auctions.rb @@ -0,0 +1,27 @@ +class CreateAuctions < ActiveRecord::Migration + def up + execute <<-SQL + CREATE TYPE auction_status AS ENUM ( + 'open', + 'closed_without_winner', + 'closed_with_winner', + 'payment_received' + ); + SQL + + create_table :auctions do |t| + t.string :domain, null: false + t.column :status, :auction_status, null: false + t.uuid :uuid, default: 'gen_random_uuid()', null: false + t.datetime :created_at, null: false + end + end + + def down + execute <<-SQL + DROP type auction_status; + SQL + + drop_table :auctions + end +end diff --git a/db/migrate/20181212145456_change_auctions_uuid_default.rb b/db/migrate/20181212145456_change_auctions_uuid_default.rb new file mode 100644 index 0000000000..d1555d08d1 --- /dev/null +++ b/db/migrate/20181212145456_change_auctions_uuid_default.rb @@ -0,0 +1,5 @@ +class ChangeAuctionsUuidDefault < ActiveRecord::Migration + def change + change_column_default :auctions, :uuid, nil + end +end diff --git a/db/migrate/20181212145914_change_auctions_uuid_to_not_null.rb b/db/migrate/20181212145914_change_auctions_uuid_to_not_null.rb new file mode 100644 index 0000000000..a73d31b263 --- /dev/null +++ b/db/migrate/20181212145914_change_auctions_uuid_to_not_null.rb @@ -0,0 +1,5 @@ +class ChangeAuctionsUuidToNotNull < ActiveRecord::Migration + def change + change_column_null :auctions, :uuid, false + end +end diff --git a/db/migrate/20181213113115_rename_released_domains_to_domain_names.rb b/db/migrate/20181213113115_rename_released_domains_to_domain_names.rb new file mode 100644 index 0000000000..f8efc1a23f --- /dev/null +++ b/db/migrate/20181213113115_rename_released_domains_to_domain_names.rb @@ -0,0 +1,5 @@ +class RenameReleasedDomainsToDomainNames < ActiveRecord::Migration + def change + rename_table :released_domains, :domain_names + end +end diff --git a/db/migrate/20181217144701_rename_domain_names_to_auctionable_domains.rb b/db/migrate/20181217144701_rename_domain_names_to_auctionable_domains.rb new file mode 100644 index 0000000000..8951f6ebe4 --- /dev/null +++ b/db/migrate/20181217144701_rename_domain_names_to_auctionable_domains.rb @@ -0,0 +1,5 @@ +class RenameDomainNamesToAuctionableDomains < ActiveRecord::Migration + def change + rename_table :domain_names, :auctionable_domains + end +end diff --git a/db/migrate/20181217144845_remove_auctionable_domains_at_auction.rb b/db/migrate/20181217144845_remove_auctionable_domains_at_auction.rb new file mode 100644 index 0000000000..1c5213150d --- /dev/null +++ b/db/migrate/20181217144845_remove_auctionable_domains_at_auction.rb @@ -0,0 +1,5 @@ +class RemoveAuctionableDomainsAtAuction < ActiveRecord::Migration + def change + remove_column :auctionable_domains, :at_auction + end +end diff --git a/db/migrate/20181220094738_remove_auctionable_domains.rb b/db/migrate/20181220094738_remove_auctionable_domains.rb new file mode 100644 index 0000000000..4762bc148a --- /dev/null +++ b/db/migrate/20181220094738_remove_auctionable_domains.rb @@ -0,0 +1,5 @@ +class RemoveAuctionableDomains < ActiveRecord::Migration + def change + drop_table :auctionable_domains + end +end diff --git a/db/migrate/20181220095053_remove_auctions_not_null_constraints.rb b/db/migrate/20181220095053_remove_auctions_not_null_constraints.rb new file mode 100644 index 0000000000..1257c5e3ba --- /dev/null +++ b/db/migrate/20181220095053_remove_auctions_not_null_constraints.rb @@ -0,0 +1,5 @@ +class RemoveAuctionsNotNullConstraints < ActiveRecord::Migration + def change + change_column_null :auctions, :uuid, true + end +end diff --git a/db/migrate/20181223153407_add_auctions_registration_code.rb b/db/migrate/20181223153407_add_auctions_registration_code.rb new file mode 100644 index 0000000000..523ea52703 --- /dev/null +++ b/db/migrate/20181223153407_add_auctions_registration_code.rb @@ -0,0 +1,5 @@ +class AddAuctionsRegistrationCode < ActiveRecord::Migration + def change + add_column :auctions, :registration_code, :string + end +end diff --git a/db/migrate/20181226211337_change_auctions_status.rb b/db/migrate/20181226211337_change_auctions_status.rb new file mode 100644 index 0000000000..af9178a2ef --- /dev/null +++ b/db/migrate/20181226211337_change_auctions_status.rb @@ -0,0 +1,9 @@ +class ChangeAuctionsStatus < ActiveRecord::Migration + disable_ddl_transaction! + + def change + execute <<-SQL + ALTER TYPE auction_status ADD VALUE 'domain_registered' AFTER 'payment_received'; + SQL + end +end diff --git a/db/migrate/20181227155537_add_payment_not_received_to_auction_status.rb b/db/migrate/20181227155537_add_payment_not_received_to_auction_status.rb new file mode 100644 index 0000000000..df8d48a9d7 --- /dev/null +++ b/db/migrate/20181227155537_add_payment_not_received_to_auction_status.rb @@ -0,0 +1,9 @@ +class AddPaymentNotReceivedToAuctionStatus < ActiveRecord::Migration + disable_ddl_transaction! + + def change + execute <<-SQL + ALTER TYPE auction_status ADD VALUE 'payment_not_received' AFTER 'payment_received'; + SQL + end +end diff --git a/db/migrate/20181227172042_change_auctions_status_to_string.rb b/db/migrate/20181227172042_change_auctions_status_to_string.rb new file mode 100644 index 0000000000..784f5f8f21 --- /dev/null +++ b/db/migrate/20181227172042_change_auctions_status_to_string.rb @@ -0,0 +1,9 @@ +class ChangeAuctionsStatusToString < ActiveRecord::Migration + def change + change_column :auctions, :status, :string + + execute <<-SQL + DROP type auction_status; + SQL + end +end diff --git a/db/migrate/20181230231015_add_auctions_registration_code_uniq_constraint.rb b/db/migrate/20181230231015_add_auctions_registration_code_uniq_constraint.rb new file mode 100644 index 0000000000..67384b29ee --- /dev/null +++ b/db/migrate/20181230231015_add_auctions_registration_code_uniq_constraint.rb @@ -0,0 +1,13 @@ +class AddAuctionsRegistrationCodeUniqConstraint < ActiveRecord::Migration + def up + execute <<-SQL + ALTER TABLE auctions ADD CONSTRAINT unique_registration_code UNIQUE (registration_code) + SQL + end + + def down + execute <<-SQL + ALTER TABLE auctions DROP CONSTRAINT unique_registration_code + SQL + end +end diff --git a/db/migrate/20190102114702_change_auctions_uuid.rb b/db/migrate/20190102114702_change_auctions_uuid.rb new file mode 100644 index 0000000000..891e044b85 --- /dev/null +++ b/db/migrate/20190102114702_change_auctions_uuid.rb @@ -0,0 +1,5 @@ +class ChangeAuctionsUuid < ActiveRecord::Migration + def change + change_column :auctions, :uuid, :uuid, null: false, default: 'gen_random_uuid()' + end +end diff --git a/db/migrate/20190102115333_add_auctions_uuid_uniq_constraint.rb b/db/migrate/20190102115333_add_auctions_uuid_uniq_constraint.rb new file mode 100644 index 0000000000..f2164ec7ec --- /dev/null +++ b/db/migrate/20190102115333_add_auctions_uuid_uniq_constraint.rb @@ -0,0 +1,13 @@ +class AddAuctionsUuidUniqConstraint < ActiveRecord::Migration + def up + execute <<-SQL + ALTER TABLE auctions ADD CONSTRAINT uniq_uuid UNIQUE (uuid) + SQL + end + + def down + execute <<-SQL + ALTER TABLE auctions DROP CONSTRAINT uniq_uuid + SQL + end +end diff --git a/db/structure.sql b/db/structure.sql index 6cca7b3615..db86621d83 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -402,6 +402,39 @@ CREATE SEQUENCE public.actions_id_seq ALTER SEQUENCE public.actions_id_seq OWNED BY public.actions.id; +-- +-- Name: auctions; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE public.auctions ( + id integer NOT NULL, + domain character varying NOT NULL, + status character varying NOT NULL, + uuid uuid DEFAULT public.gen_random_uuid() NOT NULL, + created_at timestamp without time zone NOT NULL, + registration_code character varying +); + + +-- +-- Name: auctions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.auctions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: auctions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.auctions_id_seq OWNED BY public.auctions.id; + + -- -- Name: bank_statements; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -2527,6 +2560,13 @@ ALTER TABLE ONLY public.accounts ALTER COLUMN id SET DEFAULT nextval('public.acc ALTER TABLE ONLY public.actions ALTER COLUMN id SET DEFAULT nextval('public.actions_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.auctions ALTER COLUMN id SET DEFAULT nextval('public.auctions_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2922,6 +2962,14 @@ ALTER TABLE ONLY public.actions ADD CONSTRAINT actions_pkey PRIMARY KEY (id); +-- +-- Name: auctions_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY public.auctions + ADD CONSTRAINT auctions_pkey PRIMARY KEY (id); + + -- -- Name: bank_statements_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -3322,6 +3370,14 @@ ALTER TABLE ONLY public.domains ADD CONSTRAINT uniq_domain_uuid UNIQUE (uuid); +-- +-- Name: uniq_uuid; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY public.auctions + ADD CONSTRAINT uniq_uuid UNIQUE (uuid); + + -- -- Name: unique_code; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -3354,6 +3410,14 @@ ALTER TABLE ONLY public.registrars ADD CONSTRAINT unique_reference_no UNIQUE (reference_no); +-- +-- Name: unique_registration_code; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY public.auctions + ADD CONSTRAINT unique_registration_code UNIQUE (registration_code); + + -- -- Name: unique_session_id; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -4864,5 +4928,37 @@ INSERT INTO schema_migrations (version) VALUES ('20181002090319'); INSERT INTO schema_migrations (version) VALUES ('20181108154921'); +INSERT INTO schema_migrations (version) VALUES ('20181129150515'); + +INSERT INTO schema_migrations (version) VALUES ('20181212105100'); + +INSERT INTO schema_migrations (version) VALUES ('20181212145456'); + +INSERT INTO schema_migrations (version) VALUES ('20181212145914'); + +INSERT INTO schema_migrations (version) VALUES ('20181213113115'); + +INSERT INTO schema_migrations (version) VALUES ('20181217144701'); + +INSERT INTO schema_migrations (version) VALUES ('20181217144845'); + +INSERT INTO schema_migrations (version) VALUES ('20181220094738'); + +INSERT INTO schema_migrations (version) VALUES ('20181220095053'); + +INSERT INTO schema_migrations (version) VALUES ('20181223153407'); + +INSERT INTO schema_migrations (version) VALUES ('20181226211337'); + +INSERT INTO schema_migrations (version) VALUES ('20181227155537'); + +INSERT INTO schema_migrations (version) VALUES ('20181227172042'); + +INSERT INTO schema_migrations (version) VALUES ('20181230231015'); + +INSERT INTO schema_migrations (version) VALUES ('20190102114702'); + +INSERT INTO schema_migrations (version) VALUES ('20190102115333'); + INSERT INTO schema_migrations (version) VALUES ('20190102144032'); diff --git a/doc/api/v1/auctions.md b/doc/api/v1/auctions.md new file mode 100644 index 0000000000..2a18019b32 --- /dev/null +++ b/doc/api/v1/auctions.md @@ -0,0 +1,72 @@ +## GET /api/v1/auctions +Returns started auctions. + +### Request +``` +GET /api/v1/auctions HTTP/1.1 +``` + +### Response +``` +HTTP/1.1 200 +Content-Type: application/json + +[ + { + "id": "1b3ee442-e8fe-4922-9492-8fcb9dccc69c", + "domain": "shop.test", + "status": "domain_registered" # https://github.com/internetee/registry/blob/0392984314f55640c8aae93f3b75b488d84ba73b/app/models/auction.rb#L2 + } +] +``` + +## GET /api/v1/auctions/$UUID +Returns auction details. + +### Request +``` +GET /api/v1/auctions/1b3ee442-e8fe-4922-9492-8fcb9dccc69c HTTP/1.1 +``` + +### Response +``` +HTTP/1.1 200 +Content-Type: application/json + +{ + "id": "1b3ee442-e8fe-4922-9492-8fcb9dccc69c", + "domain": "shop.test", + "status": "domain_registered" # https://github.com/internetee/registry/blob/0392984314f55640c8aae93f3b75b488d84ba73b/app/models/auction.rb#L2 +} +``` + +## PATCH /api/v1/auctions/$UUID +Updates auction. + +### Parameters +| Field name | Required | Type | Allowed values | Description | +| ---------- | -------- | ---- | -------------- | ----------- | +| status | no | String | "awaiting_payment", "no_bids", "payment_received", "payment_not_received" + +## Request +``` +PATCH /api/v1/auctions/954cdccb-af43-4765-ac8d-d40600040ab9 HTTP/1.1 +Content-type: application/json + +{ + "status": "no_bids" +} +``` + +## Response +``` +HTTP/1.1 200 +Content-Type: application/json + +{ + "id": "1b3ee442-e8fe-4922-9492-8fcb9dccc69c", + "domain": "shop.test", + "status": "domain_registered", # https://github.com/internetee/registry/blob/0392984314f55640c8aae93f3b75b488d84ba73b/app/models/auction.rb#L2 + "registration_code": "auction-001" # Revealed only if status is "payment_received", otherwise null is returned +} +``` diff --git a/doc/epp/domain.md b/doc/epp/domain.md index 2e51be1248..df76117091 100644 --- a/doc/epp/domain.md +++ b/doc/epp/domain.md @@ -41,7 +41,8 @@ Domain name mapping protocol short version: 1 Base64 encoded document. Attribute: type="pdf/asice/sce/adoc/asics/scs/edoc/adoc/bdoc/ddoc/zip/rar/gz/tar/7z" 0-1 - 0-1 Required if registering a reserved domain + 0-1 Required if registering a reserved or won domain + 0-1 Client transaction id [EXAMPLE REQUEST AND RESPONSE](/doc/epp-examples.md#epp-domain-with-citizen-as-a-registrant-creates-a-domain) diff --git a/lib/tasks/domain.rake b/lib/tasks/domain.rake deleted file mode 100644 index a5cc6d557b..0000000000 --- a/lib/tasks/domain.rake +++ /dev/null @@ -1,13 +0,0 @@ -namespace :domain do - desc 'Discard domains' - task discard: :environment do - domain_count = 0 - - Domain.discard_domains do |domain| - puts "#{domain} is discarded" - domain_count = domain_count + 1 - end - - puts "Discarded total: #{domain_count}" - end -end \ No newline at end of file diff --git a/lib/tasks/domains/release.rake b/lib/tasks/domains/release.rake new file mode 100644 index 0000000000..34b71ec348 --- /dev/null +++ b/lib/tasks/domains/release.rake @@ -0,0 +1,17 @@ +namespace :domains do + desc <<~TEXT.gsub("\n", "\s") + Releases domains with past `delete_at` by either sending them to the auction or discarding, + depending on `Rails.configuration.domains.release_to_auction` setting + TEXT + + task :release do + released_domain_count = 0 + + Domain.release_domains do |domain| + puts "#{domain} is released" + released_domain_count += 1 + end + + puts "Released total: #{released_domain_count}" + end +end diff --git a/test/fixtures/auctions.yml b/test/fixtures/auctions.yml new file mode 100644 index 0000000000..e25ae8ff56 --- /dev/null +++ b/test/fixtures/auctions.yml @@ -0,0 +1,4 @@ +one: + domain: auction.test + status: <%= Auction.statuses[:no_bids] %> + uuid: 1b3ee442-e8fe-4922-9492-8fcb9dccc69c diff --git a/test/integration/api/v1/auctions/details_test.rb b/test/integration/api/v1/auctions/details_test.rb new file mode 100644 index 0000000000..095f9e2eb8 --- /dev/null +++ b/test/integration/api/v1/auctions/details_test.rb @@ -0,0 +1,22 @@ +require 'test_helper' + +class ApiV1AuctionDetailsTest < ActionDispatch::IntegrationTest + setup do + @auction = auctions(:one) + Rails.configuration.auction.api_allowed_ips = '127.0.0.1' + end + + def test_returns_auction_details + assert_equal '1b3ee442-e8fe-4922-9492-8fcb9dccc69c', @auction.uuid + assert_equal 'auction.test', @auction.domain + assert_equal Auction.statuses[:no_bids], @auction.status + + get api_v1_auction_path(@auction.uuid), nil, 'Content-Type' => Mime::JSON.to_s + + assert_response :ok + assert_equal ({ 'id' => '1b3ee442-e8fe-4922-9492-8fcb9dccc69c', + 'domain' => 'auction.test', + 'status' => Auction.statuses[:no_bids] }), ActiveSupport::JSON + .decode(response.body) + end +end diff --git a/test/integration/api/v1/auctions/list_test.rb b/test/integration/api/v1/auctions/list_test.rb new file mode 100644 index 0000000000..3dfaff39ea --- /dev/null +++ b/test/integration/api/v1/auctions/list_test.rb @@ -0,0 +1,30 @@ +require 'test_helper' + +class ApiV1AuctionListTest < ActionDispatch::IntegrationTest + setup do + @auction = auctions(:one) + end + + def test_returns_started_auctions_without_authentication + @auction.update!(uuid: '1b3ee442-e8fe-4922-9492-8fcb9dccc69c', + domain: 'auction.test', + status: Auction.statuses[:started]) + + get api_v1_auctions_path, nil, 'Content-Type' => Mime::JSON.to_s + + assert_response :ok + assert_equal ([{ 'id' => '1b3ee442-e8fe-4922-9492-8fcb9dccc69c', + 'domain' => 'auction.test', + 'status' => Auction.statuses[:started] }]), ActiveSupport::JSON + .decode(response.body) + end + + def test_does_not_return_finished_auctions + @auction.update!(domain: 'auction.test', status: Auction.statuses[:awaiting_payment]) + + get api_v1_auctions_path, nil, 'Content-Type' => Mime::JSON.to_s + + assert_response :ok + assert_empty ActiveSupport::JSON.decode(response.body) + end +end diff --git a/test/integration/api/v1/auctions/update_test.rb b/test/integration/api/v1/auctions/update_test.rb new file mode 100644 index 0000000000..e460bfd062 --- /dev/null +++ b/test/integration/api/v1/auctions/update_test.rb @@ -0,0 +1,83 @@ +require 'test_helper' + +class ApiV1AuctionUpdateTest < ActionDispatch::IntegrationTest + setup do + @auction = auctions(:one) + Rails.configuration.auction.api_allowed_ips = '127.0.0.1' + end + + def test_returns_auction_details + assert_equal '1b3ee442-e8fe-4922-9492-8fcb9dccc69c', @auction.uuid + assert_equal 'auction.test', @auction.domain + assert_equal Auction.statuses[:no_bids], @auction.status + + patch api_v1_auction_path(@auction.uuid), { status: Auction.statuses[:no_bids] } + .to_json, 'Content-Type' => Mime::JSON.to_s + + assert_response :ok + assert_equal ({ 'id' => '1b3ee442-e8fe-4922-9492-8fcb9dccc69c', + 'domain' => 'auction.test', + 'status' => Auction.statuses[:no_bids] }), ActiveSupport::JSON + .decode(response.body) + end + + def test_updates_auction_status + patch api_v1_auction_path(@auction.uuid), { status: Auction.statuses[:awaiting_payment] } + .to_json, 'Content-Type' => Mime::JSON.to_s + @auction.reload + + assert_response :ok + assert_equal Auction.statuses[:awaiting_payment], @auction.status + end + + def test_updates_whois_when_marking_as_no_bids + @auction.update!(domain: 'auction.test', status: Auction.statuses[:started]) + Whois::Record.create!(name: 'auction.test') + + patch api_v1_auction_path(@auction.uuid), { status: Auction.statuses[:no_bids] } + .to_json, 'Content-Type' => Mime::JSON.to_s + + assert_nil Whois::Record.find_by(name: 'auction.test') + end + + def test_reveals_registration_code_when_payment_is_received + @auction.update!(registration_code: 'auction-001', + status: Auction.statuses[:awaiting_payment]) + + patch api_v1_auction_path(@auction.uuid), { status: Auction.statuses[:payment_received] } + .to_json, 'Content-Type' => Mime::JSON.to_s + @auction.reload + + response_json = ActiveSupport::JSON.decode(response.body) + assert_not_nil response_json['registration_code'] + end + + def test_conceals_registration_code_when_payment_is_not_received + @auction.update!(status: Auction.statuses[:awaiting_payment]) + + patch api_v1_auction_path(@auction.uuid), { status: Auction.statuses[:payment_not_received] } + .to_json, 'Content-Type' => Mime::JSON.to_s + @auction.reload + + response_json = ActiveSupport::JSON.decode(response.body) + assert_nil response_json['registration_code'] + end + + def test_restarts_an_auction_when_the_payment_is_not_received + @auction.update!(domain: 'auction.test', status: Auction.statuses[:awaiting_payment]) + + patch api_v1_auction_path(@auction.uuid), { status: Auction.statuses[:payment_not_received] } + .to_json, 'Content-Type' => Mime::JSON.to_s + + assert DNS::DomainName.new('auction.test').at_auction? + end + + def test_inaccessible_when_ip_address_is_not_allowed + Rails.configuration.auction.api_allowed_ips = '' + + patch api_v1_auction_path(@auction.uuid), { status: 'any' }.to_json, + 'Content-Type' => Mime::JSON.to_s + + assert_response :unauthorized + end +end diff --git a/test/integration/epp/domain/check/auction_test.rb b/test/integration/epp/domain/check/auction_test.rb new file mode 100644 index 0000000000..efc048f85c --- /dev/null +++ b/test/integration/epp/domain/check/auction_test.rb @@ -0,0 +1,87 @@ +require 'test_helper' + +class EppDomainCheckAuctionTest < ApplicationIntegrationTest + setup do + @auction = auctions(:one) + Domain.release_to_auction = true + end + + teardown do + Domain.release_to_auction = false + end + + def test_domain_is_unavailable_when_at_auction + @auction.update!(status: Auction.statuses[:started]) + + request_xml = <<-XML + + + + + + auction.test + + + + + XML + + post '/epp/command/check', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames' + + response_xml = Nokogiri::XML(response.body) + assert_equal '1000', response_xml.at_css('result')[:code] + assert_equal 1, response_xml.css('result').size + assert_equal '0', response_xml.at_xpath('//domain:name', 'domain' => 'https://epp.tld.ee/schema/domain-eis-1.0.xsd')['avail'] + assert_equal 'Domain is at auction', response_xml.at_xpath('//domain:reason', 'domain' => 'https://epp.tld.ee/schema/domain-eis-1.0.xsd').text + end + + def test_domain_is_unavailable_when_awaiting_payment + @auction.update!(status: Auction.statuses[:awaiting_payment]) + + request_xml = <<-XML + + + + + + auction.test + + + + + XML + + post '/epp/command/check', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames' + + response_xml = Nokogiri::XML(response.body) + assert_equal '1000', response_xml.at_css('result')[:code] + assert_equal 1, response_xml.css('result').size + assert_equal '0', response_xml.at_xpath('//domain:name', 'domain' => 'https://epp.tld.ee/schema/domain-eis-1.0.xsd')['avail'] + assert_equal 'Awaiting payment', response_xml.at_xpath('//domain:reason', 'domain' => 'https://epp.tld.ee/schema/domain-eis-1.0.xsd').text + end + + def test_domain_is_available_when_payment_received + @auction.update!(status: Auction.statuses[:payment_received]) + + request_xml = <<-XML + + + + + + auction.test + + + + + XML + + post '/epp/command/check', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames' + + response_xml = Nokogiri::XML(response.body) + assert_equal '1000', response_xml.at_css('result')[:code] + assert_equal 1, response_xml.css('result').size + assert_equal '1', response_xml.at_xpath('//domain:name', 'domain' => 'https://epp.tld.ee/schema/domain-eis-1.0.xsd')['avail'] + assert_nil response_xml.at_xpath('//domain:reason', 'domain' => 'https://epp.tld.ee/schema/domain-eis-1.0.xsd') + end +end diff --git a/test/integration/epp/domain/create/auction_test.rb b/test/integration/epp/domain/create/auction_test.rb new file mode 100644 index 0000000000..cb3a4f3c30 --- /dev/null +++ b/test/integration/epp/domain/create/auction_test.rb @@ -0,0 +1,248 @@ +require 'test_helper' + +class EppDomainCreateAuctionTest < ApplicationIntegrationTest + setup do + @auction = auctions(:one) + Domain.release_to_auction = true + end + + teardown do + Domain.release_to_auction = false + end + + def test_registers_domain_without_registration_code_when_not_at_auction + request_xml = <<-XML + + + + + + not-at-auction.test + #{contacts(:john).code} + + + + + #{'test' * 2000} + + + + + XML + + assert_difference 'Domain.count' do + post '/epp/command/create', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames' + end + response_xml = Nokogiri::XML(response.body) + assert_equal '1000', response_xml.at_css('result')[:code] + assert_equal 1, Nokogiri::XML(response.body).css('result').size + end + + def test_registers_domain_with_correct_registration_code_after_another_auction_when_payment_is_received + @auction.update!(status: Auction.statuses[:domain_registered], registration_code: 'some') + + another_auction = @auction.dup + another_auction.uuid = nil + another_auction.status = Auction.statuses[:payment_received] + another_auction.registration_code = 'auction002' + another_auction.save! + + request_xml = <<-XML + + + + + + auction.test + #{contacts(:john).code} + + + + + #{'test' * 2000} + + auction002 + + + + + + XML + + assert_difference 'Domain.count' do + post '/epp/command/create', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames' + end + response_xml = Nokogiri::XML(response.body) + assert_equal '1000', response_xml.at_css('result')[:code] + assert_equal 1, Nokogiri::XML(response.body).css('result').size + end + + def test_registers_domain_with_correct_registration_code_when_payment_is_received + @auction.update!(status: Auction.statuses[:payment_received], + registration_code: 'auction001') + + request_xml = <<-XML + + + + + + auction.test + #{contacts(:john).code} + + + + + #{'test' * 2000} + + auction001 + + + + + + XML + + assert_difference 'Domain.count' do + post '/epp/command/create', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames' + end + + @auction.reload + assert @auction.domain_registered? + + response_xml = Nokogiri::XML(response.body) + assert_equal '1000', response_xml.at_css('result')[:code] + assert_equal 1, Nokogiri::XML(response.body).css('result').size + end + + def test_domain_cannot_be_registered_without_registration_code + @auction.update!(status: Auction.statuses[:payment_received], + registration_code: 'auction001') + + request_xml = <<-XML + + + + + + auction.test + #{contacts(:john).code} + + + + + #{'test' * 2000} + + + + + XML + + assert_no_difference 'Domain.count' do + post '/epp/command/create', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames' + end + response_xml = Nokogiri::XML(response.body) + assert_equal '2003', response_xml.at_css('result')[:code] + assert_equal 'Required parameter missing; reserved>pw element is required', + response_xml.at_css('result msg').text + end + + def test_domain_cannot_be_registered_with_wrong_registration_code + @auction.update!(status: Auction.statuses[:payment_received], + registration_code: 'auction001') + + request_xml = <<-XML + + + + + + auction.test + #{contacts(:john).code} + + + + + #{'test' * 2000} + + wrong + + + + + + XML + + assert_no_difference 'Domain.count' do + post '/epp/command/create', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames' + end + response_xml = Nokogiri::XML(response.body) + assert_equal '2202', response_xml.at_css('result')[:code] + assert_equal 'Invalid authorization information; invalid reserved>pw value', + response_xml.at_css('result msg').text + end + + def test_domain_cannot_be_registered_when_payment_is_not_received + @auction.update!(status: Auction.statuses[:awaiting_payment]) + + request_xml = <<-XML + + + + + + auction.test + #{contacts(:john).code} + + + + + #{'test' * 2000} + + test + + + + + + XML + + assert_no_difference 'Domain.count' do + post '/epp/command/create', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames' + end + response_xml = Nokogiri::XML(response.body) + assert_equal '2003', response_xml.at_css('result')[:code] + assert_equal 'Required parameter missing; reserved>pw element required for reserved domains', + response_xml.at_css('result msg').text + end + + def test_domain_cannot_be_registered_when_at_auction + @auction.update!(status: Auction.statuses[:started]) + + request_xml = <<-XML + + + + + + auction.test + + + + + test + + + + + XML + + assert_no_difference 'Domain.count' do + post '/epp/command/create', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames' + end + response_xml = Nokogiri::XML(response.body) + assert_equal '2306', response_xml.at_css('result')[:code] + assert_equal 'Parameter value policy error: domain is at auction', + response_xml.at_css('result msg').text + end +end diff --git a/test/integration/epp/domain/info/auction_test.rb b/test/integration/epp/domain/info/auction_test.rb new file mode 100644 index 0000000000..3b67ab7fdb --- /dev/null +++ b/test/integration/epp/domain/info/auction_test.rb @@ -0,0 +1,87 @@ +require 'test_helper' + +class EppDomainInfoAuctionTest < ApplicationIntegrationTest + setup do + @auction = auctions(:one) + Domain.release_to_auction = true + end + + teardown do + Domain.release_to_auction = false + end + + def test_domain_is_unavailable_when_at_auction + @auction.update!(status: Auction.statuses[:started]) + + request_xml = <<-XML + + + + + + auction.test + + + + + XML + + post '/epp/command/info', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames' + + response_xml = Nokogiri::XML(response.body) + assert_equal '1000', response_xml.at_css('result')[:code] + assert_equal 1, response_xml.css('result').size + assert_equal 'auction.test', response_xml.at_xpath('//domain:name', 'domain' => 'https://epp.tld.ee/schema/domain-eis-1.0.xsd').text + assert_equal 'At auction', response_xml.at_xpath('//domain:status', 'domain' => 'https://epp.tld.ee/schema/domain-eis-1.0.xsd')['s'] + end + + def test_domain_is_reserved_when_awaiting_payment + @auction.update!(status: Auction.statuses[:awaiting_payment]) + + request_xml = <<-XML + + + + + + auction.test + + + + + XML + + post '/epp/command/info', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames' + + response_xml = Nokogiri::XML(response.body) + assert_equal '1000', response_xml.at_css('result')[:code] + assert_equal 1, response_xml.css('result').size + assert_equal 'auction.test', response_xml.at_xpath('//domain:name', 'domain' => 'https://epp.tld.ee/schema/domain-eis-1.0.xsd').text + assert_equal 'Awaiting payment', response_xml.at_xpath('//domain:status', 'domain' => 'https://epp.tld.ee/schema/domain-eis-1.0.xsd')['s'] + end + + def test_domain_is_reserved_when_payment_received + @auction.update!(status: Auction.statuses[:payment_received]) + + request_xml = <<-XML + + + + + + auction.test + + + + + XML + + post '/epp/command/info', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames' + + response_xml = Nokogiri::XML(response.body) + assert_equal '1000', response_xml.at_css('result')[:code] + assert_equal 1, response_xml.css('result').size + assert_equal 'auction.test', response_xml.at_xpath('//domain:name', 'domain' => 'https://epp.tld.ee/schema/domain-eis-1.0.xsd').text + assert_equal 'Reserved', response_xml.at_xpath('//domain:status', 'domain' => 'https://epp.tld.ee/schema/domain-eis-1.0.xsd')['s'] + end +end diff --git a/test/models/auction_test.rb b/test/models/auction_test.rb new file mode 100644 index 0000000000..481c5afbe3 --- /dev/null +++ b/test/models/auction_test.rb @@ -0,0 +1,112 @@ +require 'test_helper' + +class AuctionTest < ActiveSupport::TestCase + setup do + @auction = auctions(:one) + end + + def test_fixture_is_valid + assert @auction.valid? + end + + def test_statuses + assert_equal ({ 'started' => 'started', + 'no_bids' => 'no_bids', + 'awaiting_payment' => 'awaiting_payment', + 'payment_received' => 'payment_received', + 'payment_not_received' => 'payment_not_received', + 'domain_registered' => 'domain_registered' }), Auction.statuses + end + + def test_selling_domain_starts_new_auction + domain_name = DNS::DomainName.new('shop.test') + + assert_difference 'Auction.count' do + Auction.sell(domain_name) + end + auction = Auction.last + assert_equal domain_name.to_s, auction.domain + assert auction.started? + end + + def test_pending + domain_name = DNS::DomainName.new('auction.test') + assert_equal 'auction.test', @auction.domain + + assert @auction.no_bids? + assert_nil Auction.pending(domain_name) + + @auction.update!(status: Auction.statuses[:started]) + assert_equal @auction, Auction.pending(domain_name) + + @auction.update!(status: Auction.statuses[:awaiting_payment]) + assert_equal @auction, Auction.pending(domain_name) + + @auction.update!(status: Auction.statuses[:payment_received]) + assert_equal @auction, Auction.pending(domain_name) + end + + def test_record_with_invalid_status_cannot_be_saved + # ArgumentError is triggered by ActiveRecord::Base.enum + assert_raises ArgumentError do + @auction.status = 'invalid' + @auction.save! + end + end + + def test_payment_received + @auction.update!(status: Auction.statuses[:awaiting_payment], registration_code: nil) + + @auction.mark_as_payment_received + @auction.reload + + assert @auction.payment_received? + assert_not_nil @auction.registration_code + end + + def test_payment_not_received + @auction.update!(status: Auction.statuses[:awaiting_payment], registration_code: nil) + + @auction.mark_as_payment_not_received + @auction.reload + + assert @auction.payment_not_received? + assert_nil @auction.registration_code + end + + def test_restarts_an_auction_when_payment_is_not_received + @auction.update!(domain: 'auction.test', status: Auction.statuses[:awaiting_payment]) + + assert_difference 'Auction.count' do + @auction.mark_as_payment_not_received + end + + new_auction = Auction.last + assert_equal 'auction.test', new_auction.domain + assert new_auction.started? + end + + def test_domain_registrable + assert @auction.no_bids? + assert_not @auction.domain_registrable? + + @auction.status = Auction.statuses[:payment_received] + @auction.registration_code = 'auction001' + + assert @auction.domain_registrable?('auction001') + end + + def test_domain_unregistrable + @auction.status = Auction.statuses[:payment_not_received] + @auction.registration_code = 'auction001' + + assert_not @auction.domain_registrable?('auction001') + + @auction.status = Auction.statuses[:payment_received] + @auction.registration_code = 'auction001' + + assert_not @auction.domain_registrable?('wrong') + assert_not @auction.domain_registrable?(nil) + assert_not @auction.domain_registrable?('') + end +end diff --git a/test/models/dns/domain_name_test.rb b/test/models/dns/domain_name_test.rb new file mode 100644 index 0000000000..888ad0746d --- /dev/null +++ b/test/models/dns/domain_name_test.rb @@ -0,0 +1,90 @@ +require 'test_helper' + +class AuctionDouble + def domain_registrable?(_code) + true + end +end + +class AuctionDoubleTest < ActiveSupport::TestCase + def test_implements_the_domain_registrable_interface + assert_respond_to AuctionDouble.new, :domain_registrable? + end +end + +class DNS::DomainNameTest < ActiveSupport::TestCase + def test_available_when_not_at_auction + domain_name = DNS::DomainName.new('auction.test') + auctions(:one).update!(domain: 'auction.test', status: Auction.statuses[:domain_registered]) + + assert domain_name.available? + assert_not domain_name.unavailable? + end + + def test_available_with_correct_code + domain_name = DNS::DomainName.new('auction.test') + + Auction.stub(:pending, AuctionDouble.new) do + assert domain_name.available_with_code?('some') + end + end + + def test_unavailable_when_at_auction + domain_name = DNS::DomainName.new('auction.test') + auctions(:one).update!(domain: 'auction.test', status: Auction.statuses[:started]) + + assert domain_name.unavailable? + assert_not domain_name.available? + assert_equal :at_auction, domain_name.unavailability_reason + end + + def test_unavailable_when_awaiting_payment + domain_name = DNS::DomainName.new('auction.test') + auctions(:one).update!(domain: 'auction.test', status: Auction.statuses[:awaiting_payment]) + + assert domain_name.unavailable? + assert_not domain_name.available? + assert_equal :awaiting_payment, domain_name.unavailability_reason + end + + def test_sell_at_auction + domain_name = DNS::DomainName.new('new-auction.test') + assert_not domain_name.at_auction? + + domain_name.sell_at_auction + + assert domain_name.at_auction? + end + + def test_selling_at_auction_updates_whois + domain_name = DNS::DomainName.new('new-auction.test') + assert_not domain_name.at_auction? + + domain_name.sell_at_auction + + assert Whois::Record.find_by(name: 'new-auction.test') + end + + def test_at_auction + domain_name = DNS::DomainName.new('auction.test') + auctions(:one).update!(domain: 'auction.test', status: Auction.statuses[:started]) + assert domain_name.at_auction? + end + + def test_awaiting_payment + domain_name = DNS::DomainName.new('auction.test') + auctions(:one).update!(domain: 'auction.test', status: Auction.statuses[:awaiting_payment]) + assert domain_name.awaiting_payment? + end + + def test_pending_registration + domain_name = DNS::DomainName.new('auction.test') + auctions(:one).update!(domain: 'auction.test', status: Auction.statuses[:payment_received]) + assert domain_name.pending_registration? + end + + def test_to_s + domain_name = DNS::DomainName.new('shop.test') + assert_equal 'shop.test', domain_name.to_s + end +end diff --git a/test/models/domain/domain_test.rb b/test/models/domain/domain_test.rb index 9be97a8ece..5a5fe9ee43 100644 --- a/test/models/domain/domain_test.rb +++ b/test/models/domain/domain_test.rb @@ -12,4 +12,9 @@ def test_valid_fixture_is_valid def test_invalid_fixture_is_invalid assert domains(:invalid).invalid? end + + def test_domain_name + domain = Domain.new(name: 'shop.test') + assert_equal 'shop.test', domain.domain_name.to_s + end end diff --git a/test/models/domain/releasable/auctionable_test.rb b/test/models/domain/releasable/auctionable_test.rb new file mode 100644 index 0000000000..e158b468de --- /dev/null +++ b/test/models/domain/releasable/auctionable_test.rb @@ -0,0 +1,51 @@ +require 'test_helper' + +class DomainReleasableAuctionableTest < ActiveSupport::TestCase + setup do + @domain = domains(:shop) + Domain.release_to_auction = true + end + + teardown do + Domain.release_to_auction = false + end + + def test_sells_domain_at_auction + @domain.update!(delete_at: Time.zone.parse('2010-07-05 07:59')) + travel_to Time.zone.parse('2010-07-05 08:00') + + Domain.release_domains + + assert @domain.domain_name.at_auction? + end + + def test_deletes_registered_domain + @domain.update!(delete_at: Time.zone.parse('2010-07-05 07:59')) + travel_to Time.zone.parse('2010-07-05 08:00') + + assert_difference 'Domain.count', -1 do + Domain.release_domains + end + end + + def test_ignores_domains_with_delete_at_in_the_future_or_now + @domain.update!(delete_at: Time.zone.parse('2010-07-05 08:00')) + travel_to Time.zone.parse('2010-07-05 08:00') + + assert_no_difference 'Domain.count' do + Domain.release_domains + end + assert_not @domain.domain_name.at_auction? + end + + def test_ignores_domains_with_server_delete_prohibited_status + @domain.update!(delete_at: Time.zone.parse('2010-07-05 07:59'), + statuses: [DomainStatus::SERVER_DELETE_PROHIBITED]) + travel_to Time.zone.parse('2010-07-05 08:00') + + assert_no_difference 'Domain.count' do + Domain.release_domains + end + assert_not @domain.domain_name.at_auction? + end +end diff --git a/test/integration/tasks/discard_domain_test.rb b/test/models/domain/releasable/discardable_test.rb similarity index 51% rename from test/integration/tasks/discard_domain_test.rb rename to test/models/domain/releasable/discardable_test.rb index 0da7014c76..5351a8d775 100644 --- a/test/integration/tasks/discard_domain_test.rb +++ b/test/models/domain/releasable/discardable_test.rb @@ -1,51 +1,53 @@ require 'test_helper' -class DiscardDomainTaskTest < TaskTestCase +class DomainReleasableDiscardableTest < ActiveSupport::TestCase setup do - travel_to Time.zone.parse('2010-07-05 08:00') @domain = domains(:shop) end - def test_discard_domains_with_past_delete_at + def test_discards_domains_with_past_delete_at @domain.update!(delete_at: Time.zone.parse('2010-07-05 07:59')) - Rake::Task['domain:discard'].execute + travel_to Time.zone.parse('2010-07-05 08:00') + + Domain.release_domains @domain.reload + assert @domain.discarded? end - def test_ignore_domains_with_delete_at_in_the_future_or_now + def test_ignores_domains_with_delete_at_in_the_future_or_now @domain.update!(delete_at: Time.zone.parse('2010-07-05 08:00')) - Rake::Task['domain:discard'].execute + travel_to Time.zone.parse('2010-07-05 08:00') + + Domain.release_domains @domain.reload - refute @domain.discarded? + + assert_not @domain.discarded? end - def test_ignore_already_discarded_domains + def test_ignores_already_discarded_domains @domain.update!(delete_at: Time.zone.parse('2010-07-05 07:59')) - @domain.discard + travel_to Time.zone.parse('2010-07-05 08:00') + + Domain.release_domains job_count = lambda do QueJob.where("args->>0 = '#{@domain.id}'", job_class: DomainDeleteJob.name).count end assert_no_difference job_count, 'A domain should not be discarded again' do - Rake::Task['domain:discard'].execute + Domain.release_domains end end - def test_ignore_domains_with_server_delete_prohibited_status + def test_ignores_domains_with_server_delete_prohibited_status @domain.update!(delete_at: Time.zone.parse('2010-07-05 07:59'), statuses: [DomainStatus::SERVER_DELETE_PROHIBITED]) - Rake::Task['domain:discard'].execute - @domain.reload - refute @domain.discarded? - end + travel_to Time.zone.parse('2010-07-05 08:00') - def test_show_results - @domain.update!(delete_at: Time.zone.parse('2010-07-05 07:59')) - $stdout = StringIO.new + Domain.release_domains + @domain.reload - Rake::Task['domain:discard'].execute - assert_equal "shop.test is discarded\nDiscarded total: 1\n", $stdout.string + assert_not @domain.discarded? end -end \ No newline at end of file +end diff --git a/test/models/domain/releasable_test.rb b/test/models/domain/releasable_test.rb new file mode 100644 index 0000000000..f7051c2663 --- /dev/null +++ b/test/models/domain/releasable_test.rb @@ -0,0 +1,11 @@ +require 'test_helper' + +class DomainReleasableTest < ActiveSupport::TestCase + setup do + @domain = domains(:shop) + end + + def test_releasing_a_domain_discards_it_by_default + refute Domain.release_to_auction + end +end diff --git a/test/models/whois/record_test.rb b/test/models/whois/record_test.rb new file mode 100644 index 0000000000..2f3d4fc04b --- /dev/null +++ b/test/models/whois/record_test.rb @@ -0,0 +1,45 @@ +require 'test_helper' + +class Whois::RecordTest < ActiveSupport::TestCase + def test_creates_new_whois_record_when_domain_is_at_auction + domain_name = DNS::DomainName.new('some.test') + + domain_name.stub(:at_auction?, true) do + assert_difference 'Whois::Record.count' do + Whois::Record.refresh(domain_name) + + whois_record = Whois::Record.last + assert_equal 'some.test', whois_record.name + assert_equal ({ 'name' => 'some.test', 'status' => 'AtAuction' }), whois_record.json + end + end + end + + def test_refreshes_whois_record_when_domain_auction_reaches_awaiting_payment_state + domain_name = DNS::DomainName.new('some.test') + + domain_name.stub(:awaiting_payment?, true) do + assert_difference 'Whois::Record.count' do + Whois::Record.refresh(domain_name) + + whois_record = Whois::Record.last + assert_equal 'some.test', whois_record.name + assert_equal ({ 'name' => 'some.test', 'status' => 'PendingRegistration' }), whois_record.json + end + end + end + + def test_refreshes_whois_record_when_domain_auction_reaches_pending_registration_state + domain_name = DNS::DomainName.new('some.test') + + domain_name.stub(:pending_registration?, true) do + assert_difference 'Whois::Record.count' do + Whois::Record.refresh(domain_name) + + whois_record = Whois::Record.last + assert_equal 'some.test', whois_record.name + assert_equal ({ 'name' => 'some.test', 'status' => 'PendingRegistration' }), whois_record.json + end + end + end +end diff --git a/test/tasks/domains/release_test.rb b/test/tasks/domains/release_test.rb new file mode 100644 index 0000000000..69d0eae841 --- /dev/null +++ b/test/tasks/domains/release_test.rb @@ -0,0 +1,19 @@ +require 'test_helper' + +class ReleaseDomainsTaskTest < ActiveSupport::TestCase + setup do + @domain = domains(:shop) + end + + def test_output + @domain.update!(delete_at: Time.zone.parse('2010-07-05 07:59')) + travel_to Time.zone.parse('2010-07-05 08:00') + assert_output("shop.test is released\nReleased total: 1\n") { run_task } + end + + private + + def run_task + Rake::Task['domains:release'].execute + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index ce5666ece3..5a7d63f319 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -23,8 +23,13 @@ class ActiveSupport::TestCase ActiveRecord::Migration.check_pending! fixtures :all + setup do + @original_auction_config = Rails.configuration.auction + end + teardown do travel_back + Rails.configuration.auction = @original_auction_config end end