diff --git a/.codeclimate.yml b/.codeclimate.yml index 2bc90b200..d079d891f 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -20,6 +20,9 @@ plugins: channel: eslint-5 fixme: enabled: true + checks: + TODO: + enabled: false rubocop: enabled: true channel: rubocop-0-74 diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml new file mode 100644 index 000000000..eea0ccc03 --- /dev/null +++ b/.github/workflows/ruby.yml @@ -0,0 +1,117 @@ +name: Github Testing +on: [push] + + +jobs: + test: + services: + postgres: + image: postgres:12 + ports: ["5432:5432"] + env: + POSTGRES_PASSWORD: password + POSTGRES_USERNAME: postgres + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + strategy: + fail-fast: false + matrix: + os: [ubuntu-18.04] + ruby: [2.6, 2.7 ] + runs-on: ${{ matrix.os }} + continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }} + steps: + + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: config bundler + run: | + bundle config set without 'development staging production' + bundle config set deployment '[secure]' + bundle env + head -n1 $(which bundle) + + - name: Set ENV for codeclimate (pull_request) + run: | + git fetch --no-tags --prune --depth=1 origin +refs/heads/$GITHUB_HEAD_REF:refs/remotes/origin/$GITHUB_HEAD_REF + echo "GIT_BRANCH=$GITHUB_HEAD_REF" >> $GITHUB_ENV + echo "GIT_COMMIT_SHA=$(git rev-parse origin/$GITHUB_HEAD_REF)" >> $GITHUB_ENV + if: github.event_name == 'pull_request' + + - name: Set ENV for codeclimate (push) + run: | + echo "GIT_BRANCH=$GITHUB_REF" >> $GITHUB_ENV + echo "GIT_COMMIT_SHA=$GITHUB_SHA" >> $GITHUB_ENV + if: github.event_name == 'push' + + - name: Prepare CodeClimate + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + run: | + curl -LSs 'https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64' >./cc-test-reporter; + chmod +x ./cc-test-reporter + ./cc-test-reporter before-build + + - name: Run Tests + env: + PG_DATABASE: postgres + PG_HOST: localhost + PG_USER: postgres + PG_PASSWORD: password + PG_PORT: ${{ job.services.postgres.ports[5432] }} + RAILS_ENV: test + COVERAGE: true + DISABLE_SPRING: 1 + run: | + cp config/application.yml.sample config/application.yml + cp config/database_travis.yml config/database.yml + echo "openssl_config_path: 'test/fixtures/files/test_ca/openssl.cnf'" >> config/application.yml + echo "crl_dir: 'test/fixtures/files/test_ca/crl'" >> config/application.yml + echo "crl_path: 'test/fixtures/files/test_ca/crl/crl.pem'" >> config/application.yml + echo "ca_cert_path: 'test/fixtures/files/test_ca/certs/ca.crt.pem'" >> config/application.yml + echo "ca_key_path: 'test/fixtures/files/test_ca/private/ca.key.pem'" >> config/application.yml + echo "ca_key_password: 'password'" >> config/application.yml + bundle exec rake db:setup:all + bundle exec rails test test/* + - name: Save coverage + run: ./cc-test-reporter format-coverage --output coverage/codeclimate.${{ matrix.ruby }}.json + + - uses: actions/upload-artifact@v1 + with: + name: coverage-${{ matrix.ruby }} + path: coverage/codeclimate.${{ matrix.ruby }}.json + + upload_coverage: + runs-on: ubuntu-18.04 + + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + CC_TEST_REPORTER_URL: https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 + + needs: test + + steps: + - name: Download test coverage reporter + run: curl -L $CC_TEST_REPORTER_URL > cc-test-reporter + + - name: Give test coverage reporter executable permissions + run: chmod +x cc-test-reporter + + - uses: actions/download-artifact@v1 + with: + name: coverage-2.6 + path: coverage + + - uses: actions/download-artifact@v1 + with: + name: coverage-2.7 + path: coverage + + - name: Aggregate & upload results to Code Climate + run: | + ./cc-test-reporter sum-coverage coverage/codeclimate.*.json + ./cc-test-reporter upload-coverage + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 23d4ab6b1..000000000 --- a/.travis.yml +++ /dev/null @@ -1,35 +0,0 @@ -language: ruby -cache: bundler -env: - - DB=postgresql -before_install: - - "wget -N http://chromedriver.storage.googleapis.com/2.43/chromedriver_linux64.zip -P ~/" - - "unzip ~/chromedriver_linux64.zip -d ~/" - - "rm ~/chromedriver_linux64.zip" - - "sudo mv -f ~/chromedriver /usr/local/share/" - - "sudo chmod +x /usr/local/share/chromedriver" - - "sudo ln -s /usr/local/share/chromedriver /usr/local/bin/chromedriver" - - "bundle config set without 'development staging production'" - - "bundle config set deployment '[secure]'" -before_script: - - "cp config/application.yml.sample config/application.yml" - - "echo \"openssl_config_path: 'test/fixtures/files/test_ca/openssl.cnf'\" >> config/application.yml" - - "echo \"crl_dir: 'test/fixtures/files/test_ca/crl'\" >> config/application.yml" - - "echo \"crl_path: 'test/fixtures/files/test_ca/crl/crl.pem'\" >> config/application.yml" - - "echo \"ca_cert_path: 'test/fixtures/files/test_ca/certs/ca.crt.pem'\" >> config/application.yml" - - "echo \"ca_key_path: 'test/fixtures/files/test_ca/private/ca.key.pem'\" >> config/application.yml" - - "echo \"ca_key_password: 'password'\" >> config/application.yml" - - "cp config/database_travis.yml config/database.yml" - - "bundle exec rake db:setup:all" - - "curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter" - - "chmod +x ./cc-test-reporter" - - "./cc-test-reporter before-build" -after_script: - - "./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT" -script: - - "bundle exec rails test test/*" -services: - - postgresql -addons: - postgresql: "9.4" - chrome: stable diff --git a/CHANGELOG.md b/CHANGELOG.md index 58ae259b2..e9135d83f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,54 @@ +23.12.2020 +* fix for REPP logging and registrar portal communication [#1782](https://github.com/internetee/registry/pull/1782) + +22.12.2020 +* SSL CA verification fix for Bulk renew [#1778](https://github.com/internetee/registry/pull/1778) + +21.12.2020 +* Bulk renew for REPP and registrar [#1763](https://github.com/internetee/registry/issues/1763) + +17.12.2020 +* New API for registering bounced emails [#1687](https://github.com/internetee/registry/pull/1687) + +16.12.2020 +* Refactored domain delete confirmation for interactors [#1769](https://github.com/internetee/registry/issues/1769) + +15.12.2020 +* Improved logic for domain list request in registrant API [#1750](https://github.com/internetee/registry/pull/1750) +* Refactored Whois update job for interactors [#1771](https://github.com/internetee/registry/issues/1771) + +14.12.2020 +* Refactored domain cron jobs for interactors [#1767](https://github.com/internetee/registry/issues/1767) + +09.12.2020 +* Refactored domain update confirm for interactors [#1760](https://github.com/internetee/registry/issues/1760) + +08.12.2020 +* Replaced Travis-CI with GitHub Actions [#1746](https://github.com/internetee/registry/pull/1746) +* Refactored domain delete for interactors [#1755](https://github.com/internetee/registry/issues/1755) + +01.12.2020 +* Refactored clientHold for interactors [#1751](https://github.com/internetee/registry/issues/1751) +* Fixed internal error on removing clientHold status when not present [#1766](https://github.com/internetee/registry/issues/1766) + +30.11.2020 +* Refactor - interactors moved to domain space [#1762](https://github.com/internetee/registry/pull/1762) + +27.11.2020 +* Refactored delete confirmation for interactors [#1753](https://github.com/internetee/registry/issues/1753) + +24.11.2020 +* Added subnet support for list of allowed IPs [#983](https://github.com/internetee/registry/issues/983) +* Added contact endpoint to Restful EPP API [#1580](https://github.com/internetee/registry/issues/1580) + +20.11.2020 +* Registrant confirmation over Registrant API [#1742](https://github.com/internetee/registry/pull/1742) +* Refactored forceDelete cancellation for interactors [#1743](https://github.com/internetee/registry/issues/1743) + +19.11.2020 +* Only sponsoring registrar has access to private contact's details [#1745](https://github.com/internetee/registry/issues/1745) +* Refactor ForceDelete [#1740](https://github.com/internetee/registry/issues/1740) + 13.11.2020 * Fixed per registrar epp session limit [#729](https://github.com/internetee/registry/issues/729) * Correct error code is returned on reaching session limit [#587](https://github.com/internetee/registry/issues/587) diff --git a/Gemfile b/Gemfile index 25c3eafff..9bbcba254 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,7 @@ source 'https://rubygems.org' # core +gem 'active_interaction', '~> 3.8' gem 'bootsnap', '>= 1.1.0', require: false gem 'iso8601', '0.12.1' # for dates and times gem 'rails', '~> 6.0' @@ -35,8 +36,6 @@ gem 'select2-rails', '3.5.9.3' # for autocomplete gem 'cancancan' gem 'devise', '~> 4.7' -gem 'grape' - # registry specfic gem 'data_migrate', '~> 6.1' gem 'isikukood' # for EE-id validation @@ -74,15 +73,11 @@ gem 'e_invoice', github: 'internetee/e_invoice', branch: :master gem 'lhv', github: 'internetee/lhv', branch: 'master' gem 'domain_name' gem 'haml', '~> 5.0' +gem 'rexml' gem 'wkhtmltopdf-binary', '~> 0.12.5.1' gem 'directo', github: 'internetee/directo', branch: 'master' -group :development do - # deploy - gem 'listen', '3.2.1' - gem 'mina', '0.3.1' # for fast deployment -end group :development, :test do gem 'pry', '0.10.1' diff --git a/Gemfile.lock b/Gemfile.lock index c628257a2..2a0bb55b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -112,6 +112,8 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) + active_interaction (3.8.3) + activemodel (>= 4, < 7) activejob (6.0.3.3) activesupport (= 6.0.3.3) globalid (>= 0.3.6) @@ -196,28 +198,6 @@ GEM docile (1.3.2) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dry-configurable (0.11.6) - concurrent-ruby (~> 1.0) - dry-core (~> 0.4, >= 0.4.7) - dry-equalizer (~> 0.2) - dry-container (0.7.2) - concurrent-ruby (~> 1.0) - dry-configurable (~> 0.1, >= 0.1.3) - dry-core (0.4.9) - concurrent-ruby (~> 1.0) - dry-equalizer (0.3.0) - dry-inflector (0.2.0) - dry-logic (1.0.7) - concurrent-ruby (~> 1.0) - dry-core (~> 0.2) - dry-equalizer (~> 0.2) - dry-types (1.4.0) - concurrent-ruby (~> 1.0) - dry-container (~> 0.3) - dry-core (~> 0.4, >= 0.4.4) - dry-equalizer (~> 0.3) - dry-inflector (~> 0.1, >= 0.1.2) - dry-logic (~> 1.0, >= 1.0.2) erubi (1.9.0) erubis (2.7.0) execjs (2.7.0) @@ -226,13 +206,6 @@ GEM thor (~> 0.14) globalid (0.4.2) activesupport (>= 4.2.0) - grape (1.4.0) - activesupport - builder - dry-types (>= 1.1) - mustermann-grape (~> 1.0.0) - rack (>= 1.3.0) - rack-accept gyoku (1.3.1) builder (>= 2.1.2) haml (5.1.2) @@ -277,9 +250,6 @@ GEM kaminari-core (= 1.2.1) kaminari-core (1.2.1) libxml-ruby (3.2.0) - listen (3.2.1) - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) logger (1.4.2) loofah (2.7.0) crass (~> 1.0.2) @@ -293,9 +263,6 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2020.0512) mimemagic (0.3.5) - mina (0.3.1) - open4 (~> 1.3.4) - rake mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.14.2) @@ -312,8 +279,6 @@ GEM multi_json (1.15.0) mustermann (1.1.1) ruby2_keywords (~> 0.0.1) - mustermann-grape (1.0.1) - mustermann (>= 1.0.0) netrc (0.11.0) nio4r (2.5.4) nokogiri (1.10.10) @@ -325,7 +290,6 @@ GEM omniauth-rails_csrf_protection (0.1.2) actionpack (>= 4.2) omniauth (>= 1.3.1) - open4 (1.3.4) openid_connect (1.2.0) activemodel attr_required (>= 1.0.0) @@ -357,8 +321,6 @@ GEM que (~> 0.8) sinatra rack (2.2.3) - rack-accept (0.4.5) - rack (>= 0.4) rack-oauth2 (1.16.0) activesupport attr_required @@ -401,9 +363,6 @@ GEM activesupport (>= 5.2.1) i18n polyamorous (= 2.3.2) - rb-fsevent (0.10.4) - rb-inotify (0.10.1) - ffi (~> 1.0) rbtree3 (0.6.0) regexp_parser (1.8.0) request_store (1.5.0) @@ -416,6 +375,7 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) + rexml (3.2.4) ruby2_keywords (0.0.2) rubyzip (2.3.0) sass-rails (6.0.0) @@ -521,6 +481,7 @@ PLATFORMS ruby DEPENDENCIES + active_interaction (~> 3.8) activerecord-import airbrake bootsnap (>= 1.1.0) @@ -542,7 +503,6 @@ DEPENDENCIES epp! epp-xml (= 1.1.0)! figaro (= 1.1.1) - grape haml (~> 5.0) isikukood iso8601 (= 0.12.1) @@ -550,8 +510,6 @@ DEPENDENCIES jquery-ui-rails (= 5.0.5) kaminari lhv! - listen (= 3.2.1) - mina (= 0.3.1) minitest (~> 5.14) money-rails nokogiri @@ -568,6 +526,7 @@ DEPENDENCIES rails (~> 6.0) ransack (~> 2.3) rest-client + rexml sass-rails select2-rails (= 3.5.9.3) selectize-rails (= 0.12.1) diff --git a/app/api/repp/account_v1.rb b/app/api/repp/account_v1.rb deleted file mode 100644 index fe3acd387..000000000 --- a/app/api/repp/account_v1.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Repp - class AccountV1 < Grape::API - version 'v1', using: :path - - resource :accounts do - desc 'Return current cash account balance' - - get 'balance' do - @response = { - balance: current_user.registrar.cash_account.balance, - currency: current_user.registrar.cash_account.currency - } - end - end - end -end diff --git a/app/api/repp/api.rb b/app/api/repp/api.rb deleted file mode 100644 index af6864cfa..000000000 --- a/app/api/repp/api.rb +++ /dev/null @@ -1,65 +0,0 @@ -module Repp - class API < Grape::API - format :json - prefix :repp - - http_basic do |username, password| - @current_user ||= ApiUser.find_by(username: username, plain_text_password: password) - if @current_user - true - else - error! I18n.t('api_user_not_found'), 401 - end - end - - before do - webclient_request = ENV['webclient_ips'].split(',').map(&:strip).include?(request.ip) - unless webclient_request - error! I18n.t('api.authorization.ip_not_allowed', ip: request.ip), 401 unless @current_user.registrar.api_ip_white?(request.ip) - end - - if @current_user.cannot?(:view, :repp) - error! I18n.t('no_permission'), 401 unless @current_user.registrar.api_ip_white?(request.ip) - end - - next if Rails.env.test? || Rails.env.development? - message = 'Certificate mismatch! Cert common name should be:' - request_name = env['HTTP_SSL_CLIENT_S_DN_CN'] - - if webclient_request - webclient_cert_name = ENV['webclient_cert_common_name'] || 'webclient' - error! "Webclient #{message} #{webclient_cert_name}", 401 if webclient_cert_name != request_name - else - unless @current_user.pki_ok?(request.env['HTTP_SSL_CLIENT_CERT'], - request.env['HTTP_SSL_CLIENT_S_DN_CN']) - error! "#{message} #{@current_user.username}", 401 - end - end - end - - helpers do - attr_reader :current_user - end - - after do - ApiLog::ReppLog.create({ - request_path: request.path, - request_method: request.request_method, - request_params: request.params.except('route_info').to_json, - response: @response.to_json, - response_code: status, - api_user_name: current_user.try(:username), - api_user_registrar: current_user.try(:registrar).try(:to_s), - ip: request.ip, - uuid: request.try(:uuid) - }) - end - - mount Repp::DomainV1 - mount Repp::ContactV1 - mount Repp::AccountV1 - mount Repp::DomainTransfersV1 - mount Repp::NameserversV1 - mount Repp::DomainContactsV1 - end -end diff --git a/app/api/repp/contact_v1.rb b/app/api/repp/contact_v1.rb deleted file mode 100644 index 810829ef7..000000000 --- a/app/api/repp/contact_v1.rb +++ /dev/null @@ -1,35 +0,0 @@ -module Repp - class ContactV1 < Grape::API - version 'v1', using: :path - - resource :contacts do - desc 'Return list of contact' - params do - optional :limit, type: Integer, values: (1..200).to_a, desc: 'How many contacts to show' - optional :offset, type: Integer, desc: 'Contact number to start at' - optional :details, type: String, values: %w(true false), desc: 'Whether to include details' - end - - get '/' do - limit = params[:limit] || 200 - offset = params[:offset] || 0 - - if params[:details] == 'true' - contacts = current_user.registrar.contacts.limit(limit).offset(offset) - - unless Contact.address_processing? - attributes = Contact.attribute_names - Contact.address_attribute_names - contacts = contacts.select(attributes) - end - else - contacts = current_user.registrar.contacts.limit(limit).offset(offset).pluck(:code) - end - - @response = { - contacts: contacts, - total_number_of_records: current_user.registrar.contacts.count - } - end - end - end -end diff --git a/app/api/repp/domain_contacts_v1.rb b/app/api/repp/domain_contacts_v1.rb deleted file mode 100644 index 7f3e323ac..000000000 --- a/app/api/repp/domain_contacts_v1.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Repp - class DomainContactsV1 < Grape::API - version 'v1', using: :path - - resource :domains do - resource :contacts do - patch '/' do - current_contact = current_user.registrar.contacts - .find_by(code: params[:current_contact_id]) - new_contact = current_user.registrar.contacts.find_by(code: params[:new_contact_id]) - - unless current_contact - error!({ error: { type: 'invalid_request_error', - param: 'current_contact_id', - message: "No such contact: #{params[:current_contact_id]}"} }, - :bad_request) - end - - unless new_contact - error!({ error: { type: 'invalid_request_error', - param: 'new_contact_id', - message: "No such contact: #{params[:new_contact_id]}" } }, - :bad_request) - end - - if new_contact.invalid? - error!({ error: { type: 'invalid_request_error', - param: 'new_contact_id', - message: 'New contact must be valid' } }, - :bad_request) - end - - if current_contact == new_contact - error!({ error: { type: 'invalid_request_error', - message: 'New contact ID must be different from current' \ - ' contact ID' } }, - :bad_request) - end - - affected_domains, skipped_domains = TechDomainContact - .replace(current_contact, new_contact) - @response = { affected_domains: affected_domains, skipped_domains: skipped_domains } - end - end - end - end -end diff --git a/app/api/repp/domain_transfers_v1.rb b/app/api/repp/domain_transfers_v1.rb deleted file mode 100644 index c6a48df6d..000000000 --- a/app/api/repp/domain_transfers_v1.rb +++ /dev/null @@ -1,48 +0,0 @@ -module Repp - class DomainTransfersV1 < Grape::API - version 'v1', using: :path - - resource :domain_transfers do - post '/' do - params do - requires :data, type: Hash do - requires :domainTransfers, type: Array do - requires :domainName, type: String, allow_blank: false - requires :transferCode, type: String, allow_blank: false - end - end - end - - new_registrar = current_user.registrar - domain_transfers = params['data']['domainTransfers'] - successful_domain_transfers = [] - errors = [] - - domain_transfers.each do |domain_transfer| - domain_name = domain_transfer['domainName'] - transfer_code = domain_transfer['transferCode'] - domain = Domain.find_by(name: domain_name) - - if domain - if domain.transfer_code == transfer_code - DomainTransfer.request(domain, new_registrar) - successful_domain_transfers << { type: 'domain_transfer', attributes: { domain_name: domain.name } } - else - errors << { title: "#{domain_name} transfer code is wrong" } - end - else - errors << { title: "#{domain_name} does not exist" } - end - end - - if errors.none? - status 200 - @response = { data: successful_domain_transfers } - else - status 400 - @response = { errors: errors } - end - end - end - end -end diff --git a/app/api/repp/domain_v1.rb b/app/api/repp/domain_v1.rb deleted file mode 100644 index cf45bfc6f..000000000 --- a/app/api/repp/domain_v1.rb +++ /dev/null @@ -1,50 +0,0 @@ -module Repp - class DomainV1 < Grape::API - version 'v1', using: :path - - resource :domains do - desc 'Return list of domains' - params do - optional :limit, type: Integer, values: (1..200).to_a, desc: 'How many domains to show' - optional :offset, type: Integer, desc: 'Domain number to start at' - optional :details, type: String, values: %w(true false), desc: 'Whether to include details' - end - - get '/' do - limit = params[:limit] || 200 - offset = params[:offset] || 0 - - if params[:details] == 'true' - domains = current_user.registrar.domains.limit(limit).offset(offset) - else - domains = current_user.registrar.domains.limit(limit).offset(offset).pluck(:name) - end - - @response = { - domains: domains, - total_number_of_records: current_user.registrar.domains.count - } - end - - # example: curl -u registrar1:password localhost:3000/repp/v1/domains/1/transfer_info -H "Auth-Code: authinfopw1" - get '/:id/transfer_info', requirements: { id: /.*/ } do - ident = params[:id] - domain = ident.match?(/\A[0-9]+\z/) ? Domain.find_by(id: ident) : Domain.find_by_idn(ident) - - error! I18n.t('errors.messages.epp_domain_not_found'), 404 unless domain - error! I18n.t('errors.messages.epp_authorization_error'), 401 unless domain.transfer_code.eql? request.headers['Auth-Code'] - - contact_repp_json = proc{|contact| - contact.as_json.slice("code", "name", "ident", "ident_type", "ident_country_code", "phone", "email", "street", "city", "zip","country_code", "statuses") - } - - @response = { - domain: domain.name, - registrant: contact_repp_json.call(domain.registrant), - admin_contacts: domain.admin_contacts.map{|e| contact_repp_json.call(e)}, - tech_contacts: domain.tech_contacts.map{|e| contact_repp_json.call(e)} - } - end - end - end -end diff --git a/app/api/repp/nameservers_v1.rb b/app/api/repp/nameservers_v1.rb deleted file mode 100644 index df3bb64eb..000000000 --- a/app/api/repp/nameservers_v1.rb +++ /dev/null @@ -1,49 +0,0 @@ -module Repp - class NameserversV1 < Grape::API - version 'v1', using: :path - - resource 'registrar/nameservers' do - put '/' do - params do - requires :data, type: Hash, allow_blank: false do - requires :type, type: String, allow_blank: false - requires :id, type: String, allow_blank: false - optional :domains, type: Array - requires :attributes, type: Hash, allow_blank: false do - requires :hostname, type: String, allow_blank: false - requires :ipv4, type: Array - requires :ipv6, type: Array - end - end - end - - hostname = params[:data][:id] - - unless current_user.registrar.nameservers.exists?(hostname: hostname) - error!({ errors: [{ title: "Hostname #{hostname} does not exist" }] }, 404) - end - - new_attributes = { - hostname: params[:data][:attributes][:hostname], - ipv4: params[:data][:attributes][:ipv4], - ipv6: params[:data][:attributes][:ipv6], - } - - domains = params[:data][:domains].map(&:downcase) || [] - - begin - affected_domains = current_user.registrar.replace_nameservers(hostname, new_attributes, - domains: domains) - rescue ActiveRecord::RecordInvalid => e - error!({ errors: e.record.errors.full_messages.map { |error| { title: error } } }, 400) - end - - status 200 - @response = { data: { type: 'nameserver', - id: params[:data][:attributes][:hostname], - attributes: params[:data][:attributes] }, - affected_domains: affected_domains } - end - end - end -end diff --git a/app/controllers/admin/bounced_mail_addresses_controller.rb b/app/controllers/admin/bounced_mail_addresses_controller.rb new file mode 100644 index 000000000..1c59acaa4 --- /dev/null +++ b/app/controllers/admin/bounced_mail_addresses_controller.rb @@ -0,0 +1,30 @@ +module Admin + class BouncedMailAddressesController < BaseController + before_action :set_bounced_mail_address, only: %i[show destroy] + load_and_authorize_resource + + # GET /bounced_mail_addresses + def index + @bounced_mail_addresses = BouncedMailAddress.all.order(created_at: :desc) + end + + # GET /bounced_mail_addresses/1 + def show; end + + # DELETE /bounced_mail_addresses/1 + def destroy + @bounced_mail_address.destroy + redirect_to( + admin_bounced_mail_addresses_url, + notice: 'Bounced mail address was successfully destroyed.' + ) + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_bounced_mail_address + @bounced_mail_address = BouncedMailAddress.find(params[:id]) + end + end +end diff --git a/app/controllers/admin/domains/force_delete_controller.rb b/app/controllers/admin/domains/force_delete_controller.rb index 6a111926f..4fe85fa3b 100644 --- a/app/controllers/admin/domains/force_delete_controller.rb +++ b/app/controllers/admin/domains/force_delete_controller.rb @@ -4,26 +4,15 @@ module Admin def create authorize! :manage, domain + notice = t('.scheduled') + domain.transaction do - domain.schedule_force_delete(type: force_delete_type) - domain.registrar.notifications.create!(text: t('force_delete_set_on_domain', - domain_name: domain.name, - outzone_date: domain.outzone_date, - purge_date: domain.purge_date)) - - notify_by_email if notify_by_email? + result = domain.schedule_force_delete(type: force_delete_type, + notify_by_email: notify_by_email?) + notice = result.errors.messages[:domain].first unless result.valid? end - redirect_to edit_admin_domain_url(domain), notice: t('.scheduled') - end - - def notify_by_email - if force_delete_type == :fast_track - send_email - domain.update(contact_notification_sent_date: Time.zone.today) - else - domain.update(template_name: domain.notification_template) - end + redirect_to edit_admin_domain_url(domain), notice: notice end def destroy @@ -42,13 +31,6 @@ module Admin ActiveRecord::Type::Boolean.new.cast(params[:notify_by_email]) end - def send_email - DomainDeleteMailer.forced(domain: domain, - registrar: domain.registrar, - registrant: domain.registrant, - template_name: domain.notification_template).deliver_now - end - def force_delete_type soft_delete? ? :soft : :fast_track end diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 54930edf9..b62b3e063 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -10,6 +10,11 @@ module Api head :unauthorized unless ip_allowed end + def authenticate_shared_key + api_key = "Basic #{ENV['api_shared_key']}" + head(:unauthorized) unless api_key == request.authorization + end + def not_found_error uuid = params['uuid'] json = { error: 'Not Found', uuid: uuid, message: 'Record not found' } diff --git a/app/controllers/api/v1/bounces_controller.rb b/app/controllers/api/v1/bounces_controller.rb new file mode 100644 index 000000000..fd10a3398 --- /dev/null +++ b/app/controllers/api/v1/bounces_controller.rb @@ -0,0 +1,25 @@ +module Api + module V1 + class BouncesController < BaseController + before_action :authenticate_shared_key + + # POST api/v1/bounces/ + def create + return head(:bad_request) unless bounce_params[:bounce][:bouncedRecipients].any? + + BouncedMailAddress.record(bounce_params) + head(:created) + rescue ActionController::ParameterMissing + head(:bad_request) + end + + def bounce_params + params.require(:data).require(:bounce).require(:bouncedRecipients).each do |r| + r.require(:emailAddress) + end + + params.require(:data) + end + end + end +end diff --git a/app/controllers/api/v1/registrant/confirms_controller.rb b/app/controllers/api/v1/registrant/confirms_controller.rb new file mode 100644 index 000000000..057400c8e --- /dev/null +++ b/app/controllers/api/v1/registrant/confirms_controller.rb @@ -0,0 +1,119 @@ +require 'serializers/registrant_api/domain' + +module Api + module V1 + module Registrant + class ConfirmsController < ::Api::V1::Registrant::BaseController + skip_before_action :authenticate, :set_paper_trail_whodunnit + before_action :set_domain, only: %i[index update] + before_action :verify_action, only: %i[index update] + before_action :verify_decision, only: %i[update] + + def index + res = { + domain_name: @domain.name, + current_registrant: serialized_registrant(@domain.registrant), + } + + unless delete_action? + res[:new_registrant] = serialized_registrant(@domain.pending_registrant) + end + + render json: res, status: :ok + end + + def update + verification = RegistrantVerification.new(domain_id: @domain.id, + verification_token: verify_params[:token]) + + unless delete_action? ? delete_action(verification) : change_action(verification) + head :bad_request + return + end + + render json: { domain_name: @domain.name, + current_registrant: serialized_registrant(current_registrant), + status: params[:decision] }, status: :ok + end + + private + + def initiator + "email link, #{I18n.t(:user_not_authenticated)}" + end + + def current_registrant + confirmed? && !delete_action? ? @domain.pending_registrant : @domain.registrant + end + + def confirmed? + verify_params[:decision] == 'confirmed' + end + + def change_action(verification) + if confirmed? + verification.domain_registrant_change_confirm!(initiator) + else + verification.domain_registrant_change_reject!(initiator) + end + end + + def delete_action(verification) + if confirmed? + verification.domain_registrant_delete_confirm!(initiator) + else + verification.domain_registrant_delete_reject!(initiator) + end + end + + def serialized_registrant(registrant) + { + name: registrant.try(:name), + ident: registrant.try(:ident), + country: registrant.try(:ident_country_code), + } + end + + def verify_params + params do |p| + p.require(:name) + p.require(:token) + p.permit(:decision) + end + end + + def delete_action? + return true if params[:template] == 'delete' + + false + end + + def verify_decision + return if %w[confirmed rejected].include?(params[:decision]) + + head :not_found + end + + def set_domain + @domain = Domain.find_by(name: verify_params[:name]) + @domain ||= Domain.find_by(name_puny: verify_params[:name]) + return if @domain + + render json: { error: 'Domain not found' }, status: :not_found + end + + def verify_action + action = if params[:template] == 'change' + @domain.registrant_update_confirmable?(verify_params[:token]) + elsif params[:template] == 'delete' + @domain.registrant_delete_confirmable?(verify_params[:token]) + end + + return if action + + render json: { error: 'Application expired or not found' }, status: :unauthorized + end + end + end + end +end diff --git a/app/controllers/api/v1/registrant/contacts_controller.rb b/app/controllers/api/v1/registrant/contacts_controller.rb index 10f9abacf..e11458ff8 100644 --- a/app/controllers/api/v1/registrant/contacts_controller.rb +++ b/app/controllers/api/v1/registrant/contacts_controller.rb @@ -19,15 +19,16 @@ module Api end contacts = current_user_contacts.limit(limit).offset(offset) - serialized_contacts = contacts.collect { |contact| serialize_contact(contact) } + serialized_contacts = contacts.collect { |contact| serialize_contact(contact, false) } render json: serialized_contacts end def show contact = current_user_contacts.find_by(uuid: params[:uuid]) + links = params[:links] == 'true' if contact - render json: serialize_contact(contact) + render json: serialize_contact(contact, links) else render json: { errors: [{ base: ['Contact not found'] }] }, status: :not_found end @@ -85,7 +86,7 @@ module Api contact.registrar.notify(action) end - render json: serialize_contact(contact) + render json: serialize_contact(contact, false) end private @@ -96,8 +97,8 @@ module Api current_registrant_user.direct_contacts end - def serialize_contact(contact) - Serializers::RegistrantApi::Contact.new(contact).to_json + def serialize_contact(contact, links) + Serializers::RegistrantApi::Contact.new(contact, links).to_json end end end diff --git a/app/controllers/api/v1/registrant/domains_controller.rb b/app/controllers/api/v1/registrant/domains_controller.rb index b985ef390..73b534598 100644 --- a/app/controllers/api/v1/registrant/domains_controller.rb +++ b/app/controllers/api/v1/registrant/domains_controller.rb @@ -7,6 +7,7 @@ module Api def index limit = params[:limit] || 200 offset = params[:offset] || 0 + simple = params[:simple] == 'true' || false if limit.to_i > 200 || limit.to_i < 1 render(json: { errors: [{ limit: ['parameter is out of range'] }] }, @@ -18,21 +19,20 @@ module Api status: :bad_request) && return end - @domains = current_user_domains.limit(limit).offset(offset) - - serialized_domains = @domains.map do |item| - serializer = Serializers::RegistrantApi::Domain.new(item) + domains = current_user_domains + serialized_domains = domains.limit(limit).offset(offset).map do |item| + serializer = Serializers::RegistrantApi::Domain.new(item, simplify: simple) serializer.to_json end - render json: serialized_domains + render json: { count: domains.count, domains: serialized_domains } end def show @domain = current_user_domains.find_by(uuid: params[:uuid]) if @domain - serializer = Serializers::RegistrantApi::Domain.new(@domain) + serializer = Serializers::RegistrantApi::Domain.new(@domain, simplify: false) render json: serializer.to_json else render json: { errors: [{ base: ['Domain not found'] }] }, status: :not_found diff --git a/app/controllers/epp/contacts_controller.rb b/app/controllers/epp/contacts_controller.rb index df9755af6..85305213b 100644 --- a/app/controllers/epp/contacts_controller.rb +++ b/app/controllers/epp/contacts_controller.rb @@ -1,10 +1,9 @@ require 'deserializers/xml/contact_update' - +require 'deserializers/xml/contact_create' module Epp class ContactsController < BaseController before_action :find_contact, only: [:info, :update, :delete] before_action :find_password, only: [:info, :update, :delete] - helper_method :address_processing? def info authorize! :info, @contact, @password @@ -21,25 +20,13 @@ module Epp def create authorize! :create, Epp::Contact - frame = params[:parsed_frame] - @contact = Epp::Contact.new(frame, current_user.registrar) - @contact.add_legal_file_to_new(frame) - @contact.generate_code + @contact = Epp::Contact.new(params[:parsed_frame], current_user.registrar) + collected_data = ::Deserializers::Xml::ContactCreate.new(params[:parsed_frame]) + action = Actions::ContactCreate.new(@contact, collected_data.legal_document, + collected_data.ident) - if @contact.save - if !address_processing? && address_given? - @response_code = 1100 - @response_description = t('epp.contacts.completed_without_address') - else - @response_code = 1000 - @response_description = t('epp.contacts.completed') - end - - render_epp_response '/epp/contacts/save' - else - handle_errors(@contact) - end + action_call_response(action: action) end def update @@ -52,29 +39,18 @@ module Epp collected_data.ident, current_user) - if action.call - if !address_processing? && address_given? - @response_code = 1100 - @response_description = t('epp.contacts.completed_without_address') - else - @response_code = 1000 - @response_description = t('epp.contacts.completed') - end - - render_epp_response 'epp/contacts/save' - else - handle_errors(@contact) - end + action_call_response(action: action) end def delete authorize! :delete, @contact, @password - - if @contact.destroy_and_clean(params[:parsed_frame]) - render_epp_response '/epp/contacts/delete' - else + action = Actions::ContactDelete.new(@contact, params[:legal_document]) + unless action.call handle_errors(@contact) + return end + + render_epp_response '/epp/contacts/delete' end def renew @@ -91,6 +67,26 @@ module Epp private + def opt_addr? + !Contact.address_processing? && address_given? + end + + def action_call_response(action:) + # rubocop:disable Style/AndOr + (handle_errors(@contact) and return) unless action.call + # rubocop:enable Style/AndOr + + if opt_addr? + @response_code = 1100 + @response_description = t('epp.contacts.completed_without_address') + else + @response_code = 1000 + @response_description = t('epp.contacts.completed') + end + + render_epp_response('epp/contacts/save') + end + def find_password @password = params[:parsed_frame].css('authInfo pw').text end @@ -129,8 +125,7 @@ module Epp 'postalInfo > addr > cc', ] - required_attributes.concat(address_attributes) if address_processing? - + required_attributes.concat(address_attributes) if Contact.address_processing? requires(*required_attributes) ident = params[:parsed_frame].css('ident') @@ -206,9 +201,5 @@ module Epp def address_given? params[:parsed_frame].css('postalInfo addr').size != 0 end - - def address_processing? - Contact.address_processing? - end end end diff --git a/app/controllers/registrar/bulk_change_controller.rb b/app/controllers/registrar/bulk_change_controller.rb index 441127f6c..801ab0516 100644 --- a/app/controllers/registrar/bulk_change_controller.rb +++ b/app/controllers/registrar/bulk_change_controller.rb @@ -4,11 +4,38 @@ class Registrar def new authorize! :manage, :repp + @expire_date = Time.zone.now.to_date render file: 'registrar/bulk_change/new', locals: { active_tab: default_tab } end + def bulk_renew + authorize! :manage, :repp + set_form_data + + if ready_to_renew? + res = ReppApi.bulk_renew(domain_ids_for_bulk_renew, params[:period], + current_registrar_user) + + flash_message(JSON.parse(res)) + else + flash[:notice] = nil + end + + render file: 'registrar/bulk_change/new', locals: { active_tab: :bulk_renew } + end + private + def ready_to_renew? + domain_ids_for_bulk_renew.present? && params[:renew].present? + end + + def set_form_data + @expire_date = params[:expire_date].to_date + @domains = domains_by_date(@expire_date) + @period = params[:period] + end + def available_contacts current_registrar_user.registrar.contacts.order(:name).pluck(:name, :code) end @@ -16,5 +43,27 @@ class Registrar def default_tab :technical_contact end + + def domains_scope + current_registrar_user.registrar.domains + end + + def domains_by_date(date) + domains_scope.where('valid_to <= ?', date) + end + + def domain_ids_for_bulk_renew + params.dig('domain_ids')&.reject { |id| id.blank? } + end + + def renew_task(domains) + Domains::BulkRenew::Start.run(domains: domains, + period_element: @period, + registrar: current_registrar_user.registrar) + end + + def flash_message(res) + flash[:notice] = res['code'] == 1000 ? t(:bulk_renew_completed) : res['message'] + end end end diff --git a/app/controllers/registrar/domain_transfers_controller.rb b/app/controllers/registrar/domain_transfers_controller.rb index acacc3ef4..584a50d33 100644 --- a/app/controllers/registrar/domain_transfers_controller.rb +++ b/app/controllers/registrar/domain_transfers_controller.rb @@ -15,12 +15,12 @@ class Registrar csv.each do |row| domain_name = row['Domain'] transfer_code = row['Transfer code'] - domain_transfers << { 'domainName' => domain_name, 'transferCode' => transfer_code } + domain_transfers << { 'domain_name' => domain_name, 'transfer_code' => transfer_code } end - uri = URI.parse("#{ENV['repp_url']}domain_transfers") + uri = URI.parse("#{ENV['repp_url']}domains/transfer") request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') - request.body = { data: { domainTransfers: domain_transfers } }.to_json + request.body = { data: { domain_transfers: domain_transfers } }.to_json request.basic_auth(current_registrar_user.username, current_registrar_user.plain_text_password) @@ -55,10 +55,12 @@ class Registrar parsed_response = JSON.parse(response.body, symbolize_names: true) if response.code == '200' - flash[:notice] = t '.transferred', count: parsed_response[:data].size + failed = parsed_response[:data][:failed].each(&:domain_name).join(', ') + flash[:notice] = t('.transferred', count: parsed_response[:data][:success].size, + failed: failed) redirect_to registrar_domains_url else - @api_errors = parsed_response[:errors] + @api_errors = parsed_response[:message] render file: 'registrar/bulk_change/new', locals: { active_tab: :bulk_transfer } end else diff --git a/app/controllers/registrar/nameservers_controller.rb b/app/controllers/registrar/nameservers_controller.rb index c8c88c8ca..2a22476be 100644 --- a/app/controllers/registrar/nameservers_controller.rb +++ b/app/controllers/registrar/nameservers_controller.rb @@ -49,12 +49,13 @@ class Registrar if response.code == '200' notices = [t('.replaced')] - notices << "#{t('.affected_domains')}: #{parsed_response[:affected_domains].join(', ')}" + notices << "#{t('.affected_domains')}: " \ + "#{parsed_response[:data][:affected_domains].join(', ')}" - flash[:notice] = notices + flash[:notice] = notices.join(', ') redirect_to registrar_domains_url else - @api_errors = parsed_response[:errors] + @api_errors = parsed_response[:message] render file: 'registrar/bulk_change/new', locals: { active_tab: :nameserver } end end diff --git a/app/controllers/registrar/sessions_controller.rb b/app/controllers/registrar/sessions_controller.rb index 2fa69ca90..e413c4feb 100644 --- a/app/controllers/registrar/sessions_controller.rb +++ b/app/controllers/registrar/sessions_controller.rb @@ -62,6 +62,7 @@ class Registrar def find_user_by_idc_and_allowed(idc) return User.new unless idc + possible_users = ApiUser.where(identity_code: idc) || User.new possible_users.each do |selected_user| if selected_user.registrar.white_ips.registrar_area.include_ip?(request.ip) diff --git a/app/controllers/registrar/tech_contacts_controller.rb b/app/controllers/registrar/tech_contacts_controller.rb index 1d459ef0f..001651250 100644 --- a/app/controllers/registrar/tech_contacts_controller.rb +++ b/app/controllers/registrar/tech_contacts_controller.rb @@ -43,16 +43,18 @@ class Registrar if response.code == '200' notices = [t('.replaced')] - notices << "#{t('.affected_domains')}: #{parsed_response[:affected_domains].join(', ')}" + notices << "#{t('.affected_domains')}: " \ + "#{parsed_response[:data][:affected_domains].join(', ')}" - if parsed_response[:skipped_domains] - notices << "#{t('.skipped_domains')}: #{parsed_response[:skipped_domains].join(', ')}" + if parsed_response[:data][:skipped_domains] + notices << "#{t('.skipped_domains')}: " \ + "#{parsed_response[:data][:skipped_domains].join(', ')}" end - flash[:notice] = notices + flash[:notice] = notices.join(', ') redirect_to registrar_domains_url else - @error = parsed_response[:error] + @error = response.code == '404' ? 'Contact(s) not found' : parsed_response[:message] render file: 'registrar/bulk_change/new', locals: { active_tab: :technical_contact } end end diff --git a/app/controllers/repp/v1/accounts_controller.rb b/app/controllers/repp/v1/accounts_controller.rb new file mode 100644 index 000000000..89c14808f --- /dev/null +++ b/app/controllers/repp/v1/accounts_controller.rb @@ -0,0 +1,11 @@ +module Repp + module V1 + class AccountsController < BaseController + def balance + resp = { balance: current_user.registrar.cash_account.balance, + currency: current_user.registrar.cash_account.currency } + render_success(data: resp) + end + end + end +end diff --git a/app/controllers/repp/v1/auctions_controller.rb b/app/controllers/repp/v1/auctions_controller.rb index 4a5265d13..676dac266 100644 --- a/app/controllers/repp/v1/auctions_controller.rb +++ b/app/controllers/repp/v1/auctions_controller.rb @@ -3,9 +3,9 @@ module Repp class AuctionsController < ActionController::API def index auctions = Auction.started + @response = { count: auctions.count, auctions: auctions_to_json(auctions) } - render json: { count: auctions.count, - auctions: auctions_to_json(auctions) } + render json: @response end private diff --git a/app/controllers/repp/v1/base_controller.rb b/app/controllers/repp/v1/base_controller.rb new file mode 100644 index 000000000..2814ce2da --- /dev/null +++ b/app/controllers/repp/v1/base_controller.rb @@ -0,0 +1,130 @@ +module Repp + module V1 + class BaseController < ActionController::API + rescue_from ActiveRecord::RecordNotFound, with: :not_found_error + before_action :authenticate_user + before_action :validate_webclient_ca + before_action :check_ip_restriction + attr_reader :current_user + + before_action :set_paper_trail_whodunnit + + rescue_from ActionController::ParameterMissing do |exception| + render json: { code: 2003, message: exception }, status: :bad_request + end + + after_action do + ApiLog::ReppLog.create( + request_path: request.path, request_method: request.request_method, + request_params: request.params.except('route_info').to_json, uuid: request.try(:uuid), + response: @response.to_json, response_code: status, ip: request.ip, + api_user_name: current_user.try(:username), + api_user_registrar: current_user.try(:registrar).try(:to_s) + ) + end + + private + + def set_paper_trail_whodunnit + ::PaperTrail.request.whodunnit = current_user + end + + def render_success(code: nil, message: nil, data: nil) + @response = { code: code || 1000, message: message || 'Command completed successfully', + data: data || {} } + + render(json: @response, status: :ok) + end + + def epp_errors + @epp_errors ||= [] + end + + def handle_errors(obj = nil, update: false) + @epp_errors ||= [] + + obj&.construct_epp_errors + @epp_errors += obj.errors[:epp_errors] if obj + + format_epp_errors if update + @epp_errors.uniq! + + render_epp_error + end + + def format_epp_errors + @epp_errors.each_with_index do |error, index| + blocked_by_delete_prohibited?(error, index) + end + end + + def blocked_by_delete_prohibited?(error, index) + if error[:code] == 2304 && error[:value][:val] == DomainStatus::SERVER_DELETE_PROHIBITED && + error[:value][:obj] == 'status' + + @epp_errors[index][:value][:val] = DomainStatus::PENDING_UPDATE + end + end + + def render_epp_error(status = :bad_request, data = {}) + @epp_errors ||= [] + @epp_errors << { code: 2304, msg: 'Command failed' } if data != {} + + @response = { code: @epp_errors[0][:code].to_i, message: @epp_errors[0][:msg], data: data } + render(json: @response, status: status) + end + + def basic_token + pattern = /^Basic / + header = request.headers['Authorization'] + header = header.gsub(pattern, '') if header&.match(pattern) + header.strip + end + + def authenticate_user + username, password = Base64.urlsafe_decode64(basic_token).split(':') + @current_user ||= ApiUser.find_by(username: username, plain_text_password: password) + + return if @current_user + + raise(ArgumentError) + rescue NoMethodError, ArgumentError + @response = { code: 2202, message: 'Invalid authorization information' } + render(json: @response, status: :unauthorized) + end + + def check_ip_restriction + return if webclient_request? + return if @current_user.registrar.api_ip_white?(request.ip) + + @response = { code: 2202, + message: I18n.t('registrar.authorization.ip_not_allowed', ip: request.ip) } + render(json: @response, status: :unauthorized) + end + + def webclient_request? + return if Rails.env.test? + + ENV['webclient_ips'].split(',').map(&:strip).include?(request.ip) + end + + def validate_webclient_ca + return unless webclient_request? + + request_name = request.env['HTTP_SSL_CLIENT_S_DN_CN'] + webclient_cn = ENV['webclient_cert_common_name'] || 'webclient' + return if request_name == webclient_cn + + @response = { code: 2202, + message: I18n.t('registrar.authorization.ip_not_allowed', ip: request.ip) } + + render(json: @response, status: :unauthorized) + end + + def not_found_error + @response = { code: 2303, message: 'Object does not exist' } + render(json: @response, status: :not_found) + end + end + end +end diff --git a/app/controllers/repp/v1/contacts_controller.rb b/app/controllers/repp/v1/contacts_controller.rb new file mode 100644 index 000000000..acf275c47 --- /dev/null +++ b/app/controllers/repp/v1/contacts_controller.rb @@ -0,0 +1,137 @@ +require 'serializers/repp/contact' +module Repp + module V1 + class ContactsController < BaseController + before_action :find_contact, only: %i[show update destroy] + + ## GET /repp/v1/contacts + def index + record_count = current_user.registrar.contacts.count + contacts = showable_contacts(params[:details], params[:limit] || 200, + params[:offset] || 0) + @response = { contacts: contacts, total_number_of_records: record_count } + render(json: @response, status: :ok) + end + + ## GET /repp/v1/contacts/1 + def show + serializer = ::Serializers::Repp::Contact.new(@contact, + show_address: Contact.address_processing?) + render_success(data: serializer.to_json) + end + + ## GET /repp/v1/contacts/check/1 + def check + contact = Epp::Contact.find_by(code: params[:id]) + data = { contact: { id: params[:id], available: contact.nil? } } + + render_success(data: data) + end + + ## POST /repp/v1/contacts + def create + @contact = Epp::Contact.new(contact_params_with_address, current_user.registrar, epp: false) + action = Actions::ContactCreate.new(@contact, params[:legal_document], + contact_ident_params) + + unless action.call + handle_errors(@contact) + return + end + + render_success(create_update_success_body) + end + + ## PUT /repp/v1/contacts/1 + def update + action = Actions::ContactUpdate.new(@contact, contact_params_with_address(required: false), + params[:legal_document], + contact_ident_params(required: false), current_user) + + unless action.call + handle_errors(@contact) + return + end + + render_success(create_update_success_body) + end + + def destroy + action = Actions::ContactDelete.new(@contact, params[:legal_document]) + unless action.call + handle_errors(@contact) + return + end + + render_success + end + + def contact_addr_present? + return false unless contact_addr_params.key?(:addr) + + contact_addr_params[:addr].keys.any? + end + + def create_update_success_body + { code: opt_addr? ? 1100 : nil, data: { contact: { id: @contact.code } }, + message: opt_addr? ? I18n.t('epp.contacts.completed_without_address') : nil } + end + + def showable_contacts(details, limit, offset) + contacts = current_user.registrar.contacts.limit(limit).offset(offset) + + return contacts.pluck(:code) unless details + + contacts = contacts.map do |contact| + serializer = ::Serializers::Repp::Contact.new(contact, + show_address: Contact.address_processing?) + serializer.to_json + end + + contacts + end + + def opt_addr? + !Contact.address_processing? && contact_addr_present? + end + + def find_contact + code = params[:id] + @contact = Epp::Contact.find_by!(code: code, registrar: current_user.registrar) + end + + def contact_params_with_address(required: true) + return contact_create_params(required: required) unless contact_addr_params.key?(:addr) + + addr = {} + contact_addr_params[:addr].each_key { |k| addr[k] = contact_addr_params[:addr][k] } + contact_create_params(required: required).merge(addr) + end + + def contact_create_params(required: true) + params.require(:contact).require(%i[name email phone]) if required + params.require(:contact).permit(:name, :email, :phone, :id) + end + + def contact_ident_params(required: true) + if required + params.require(:contact).require(:ident).require(%i[ident ident_type ident_country_code]) + params.require(:contact).require(:ident).permit(:ident, :ident_type, :ident_country_code) + else + params.permit(contact: { ident: %i[ident ident_type ident_country_code] }) + end + + params[:contact][:ident] + end + + def contact_addr_params + if Contact.address_processing? + params.require(:contact).require(:addr).require(%i[country_code city street zip]) + params.require(:contact).require(:addr).permit(:country_code, :city, :street, :zip) + else + params.require(:contact).permit(addr: %i[country_code city street zip]) + end + end + end + end +end diff --git a/app/controllers/repp/v1/domains/contacts_controller.rb b/app/controllers/repp/v1/domains/contacts_controller.rb new file mode 100644 index 000000000..75404e0c6 --- /dev/null +++ b/app/controllers/repp/v1/domains/contacts_controller.rb @@ -0,0 +1,42 @@ +module Repp + module V1 + module Domains + class ContactsController < BaseController + before_action :set_current_contact, only: [:update] + before_action :set_new_contact, only: [:update] + + def set_current_contact + @current_contact = current_user.registrar.contacts.find_by!( + code: contact_params[:current_contact_id] + ) + end + + def set_new_contact + @new_contact = current_user.registrar.contacts.find_by!(code: params[:new_contact_id]) + end + + def update + @epp_errors ||= [] + @epp_errors << { code: 2304, msg: 'New contact must be valid' } if @new_contact.invalid? + + if @new_contact == @current_contact + @epp_errors << { code: 2304, msg: 'New contact must be different from current' } + end + + return handle_errors if @epp_errors.any? + + affected, skipped = TechDomainContact.replace(@current_contact, @new_contact) + @response = { affected_domains: affected, skipped_domains: skipped } + render_success(data: @response) + end + + private + + def contact_params + params.require(%i[current_contact_id new_contact_id]) + params.permit(:current_contact_id, :new_contact_id) + end + end + end + end +end diff --git a/app/controllers/repp/v1/domains/renews_controller.rb b/app/controllers/repp/v1/domains/renews_controller.rb new file mode 100644 index 000000000..6b016dd86 --- /dev/null +++ b/app/controllers/repp/v1/domains/renews_controller.rb @@ -0,0 +1,65 @@ +module Repp + module V1 + module Domains + class RenewsController < BaseController + before_action :validate_renew_period, only: [:bulk_renew] + before_action :select_renewable_domains, only: [:bulk_renew] + + def bulk_renew + renew = run_bulk_renew_task(@domains, bulk_renew_params[:renew_period]) + return render_success(data: { updated_domains: @domains.map(&:name) }) if renew.valid? + + @epp_errors << { code: 2002, + msg: renew.errors.keys.map { |k, _v| renew.errors[k] }.join(', ') } + handle_errors + end + + private + + def validate_renew_period + @epp_errors ||= [] + periods = Depp::Domain::PERIODS.map { |p| p[1] } + return if periods.include? bulk_renew_params[:renew_period] + + @epp_errors << { code: 2005, msg: 'Invalid renew period' } + end + + def select_renewable_domains + @epp_errors ||= [] + + if bulk_renew_params[:domains].instance_of?(Array) + @domains = bulk_renew_domains + else + @epp_errors << { code: 2005, msg: 'Domains attribute must be an array' } + end + + return handle_errors if @epp_errors.any? + end + + def run_bulk_renew_task(domains, period) + ::Domains::BulkRenew::Start.run(domains: domains, period_element: period, + registrar: current_user.registrar) + end + + def bulk_renew_params + params do + params.require(%i[domains renew_period]) + params.permit(:domains, :renew_period) + end + end + + def bulk_renew_domains + @epp_errors ||= [] + domains = [] + bulk_renew_params[:domains].each do |idn| + domain = Epp::Domain.find_by(name: idn) + domains << domain if domain + @epp_errors << { code: 2304, msg: "Object does not exist: #{idn}" } unless domain + end + + domains + end + end + end + end +end diff --git a/app/controllers/repp/v1/domains_controller.rb b/app/controllers/repp/v1/domains_controller.rb new file mode 100644 index 000000000..ba90f23f2 --- /dev/null +++ b/app/controllers/repp/v1/domains_controller.rb @@ -0,0 +1,94 @@ +module Repp + module V1 + class DomainsController < BaseController + before_action :set_authorized_domain, only: [:transfer_info] + + def index + records = current_user.registrar.domains + domains = records.limit(limit).offset(offset) + domains = domains.pluck(:name) unless index_params[:details] == 'true' + + render_success(data: { domains: domains, total_number_of_records: records.count }) + end + + def transfer_info + contact_fields = %i[code name ident ident_type ident_country_code phone email street city + zip country_code statuses] + + data = { + domain: @domain.name, + registrant: @domain.registrant.as_json(only: contact_fields), + admin_contacts: @domain.admin_contacts.map { |c| c.as_json(only: contact_fields) }, + tech_contacts: @domain.tech_contacts.map { |c| c.as_json(only: contact_fields) }, + } + + render_success(data: data) + end + + def transfer + @errors ||= [] + @successful = [] + + transfer_params[:domain_transfers].each do |transfer| + initiate_transfer(transfer) + end + + render_success(data: { success: @successful, failed: @errors }) + end + + def initiate_transfer(transfer) + domain = Epp::Domain.find_or_initialize_by(name: transfer[:domain_name]) + action = Actions::DomainTransfer.new(domain, transfer[:transfer_code], + current_user.registrar) + + if action.call + @successful << { type: 'domain_transfer', domain_name: domain.name } + else + @errors << { type: 'domain_transfer', domain_name: domain.name, + errors: domain.errors[:epp_errors] } + end + end + + private + + def transfer_params + params.require(:data).require(:domain_transfers).each do |t| + t.require(:domain_name) + t.permit(:domain_name) + t.require(:transfer_code) + t.permit(:transfer_code) + end + params.require(:data).permit(domain_transfers: %i[domain_name transfer_code]) + end + + def transfer_info_params + params.require(:id) + params.permit(:id) + end + + def set_authorized_domain + @epp_errors ||= [] + h = {} + h[transfer_info_params[:id].match?(/\A[0-9]+\z/) ? :id : :name] = transfer_info_params[:id] + @domain = Domain.find_by!(h) + + return if @domain.transfer_code.eql?(request.headers['Auth-Code']) + + @epp_errors << { code: 2202, msg: I18n.t('errors.messages.epp_authorization_error') } + handle_errors + end + + def limit + index_params[:limit] || 200 + end + + def offset + index_params[:offset] || 0 + end + + def index_params + params.permit(:limit, :offset, :details) + end + end + end +end diff --git a/app/controllers/repp/v1/registrar/nameservers_controller.rb b/app/controllers/repp/v1/registrar/nameservers_controller.rb new file mode 100644 index 000000000..1df781ad7 --- /dev/null +++ b/app/controllers/repp/v1/registrar/nameservers_controller.rb @@ -0,0 +1,56 @@ +module Repp + module V1 + module Registrar + class NameserversController < BaseController + before_action :verify_nameserver_existance, only: %i[update] + + def update + affected = current_user.registrar + .replace_nameservers(hostname, + hostname_params[:data][:attributes], + domains: domains_from_params) + + render_success(data: data_format_for_success(affected)) + rescue ActiveRecord::RecordInvalid => e + handle_errors(e.record) + end + + private + + def domains_from_params + return [] unless params[:data][:domains] + + params[:data][:domains].map(&:downcase) + end + + def data_format_for_success(affected_domains) + { + type: 'nameserver', + id: params[:data][:attributes][:hostname], + attributes: params[:data][:attributes], + affected_domains: affected_domains, + } + end + + def hostname_params + params.require(:data).require(%i[type id]) + params.require(:data).require(:attributes).require([:hostname]) + + params.permit(data: [ + :type, :id, + { domains: [], + attributes: [:hostname, { ipv4: [], ipv6: [] }] } + ]) + end + + def hostname + hostname_params[:data][:id] + end + + def verify_nameserver_existance + current_user.registrar.nameservers.find_by!(hostname: hostname) + end + end + end + end +end diff --git a/app/controllers/repp/v1/retained_domains_controller.rb b/app/controllers/repp/v1/retained_domains_controller.rb index c1bb458e9..8edc32f5b 100644 --- a/app/controllers/repp/v1/retained_domains_controller.rb +++ b/app/controllers/repp/v1/retained_domains_controller.rb @@ -3,8 +3,9 @@ module Repp class RetainedDomainsController < ActionController::API def index domains = RetainedDomains.new(query_params) + @response = { count: domains.count, domains: domains.to_jsonable } - render json: { count: domains.count, domains: domains.to_jsonable } + render json: @response end def query_params diff --git a/app/helpers/object_versions_helper.rb b/app/helpers/object_versions_helper.rb index d8e00abbe..be8ef1217 100644 --- a/app/helpers/object_versions_helper.rb +++ b/app/helpers/object_versions_helper.rb @@ -3,7 +3,7 @@ module ObjectVersionsHelper version.object_changes.to_h.each do |key, value| method_name = "#{key}=".to_sym if new_object.respond_to?(method_name) - new_object.public_send(method_name, value.last) + new_object.public_send(method_name, event_value(version, value)) end end end @@ -12,4 +12,10 @@ module ObjectVersionsHelper field_names = model.column_names version.object.to_h.select { |key, _value| field_names.include?(key) } end + + private + + def event_value(version, val) + version.event == 'destroy' ? val.first : val.last + end end diff --git a/app/interactions/domains/bulk_renew/single_domain_renew.rb b/app/interactions/domains/bulk_renew/single_domain_renew.rb new file mode 100644 index 000000000..66c6244b3 --- /dev/null +++ b/app/interactions/domains/bulk_renew/single_domain_renew.rb @@ -0,0 +1,62 @@ +module Domains + module BulkRenew + class SingleDomainRenew < ActiveInteraction::Base + object :domain, + class: Epp::Domain + integer :period + string :unit + object :registrar + + def execute + in_transaction_with_retries do + success = domain.renew(domain.valid_to, period, unit) + if success + check_balance + reduce_balance + else + add_error + end + end + end + + def check_balance + compose(Domains::CheckBalance::SingleDomain, + domain: domain, + operation: 'renew', + period: period, + unit: unit) + end + + def reduce_balance + domain_pricelist = domain.pricelist('renew', period, unit) + registrar.debit!(sum: domain_pricelist.price.amount, + description: "#{I18n.t('renew')} #{domain.name}", + activity_type: AccountActivity::RENEW, + price: domain_pricelist) + end + + def in_transaction_with_retries + if Rails.env.test? + yield + else + transaction_wrapper { yield } + end + rescue ActiveRecord::StatementInvalid + sleep rand / 100 + retry + end + + def transaction_wrapper + ActiveRecord::Base.transaction(isolation: :serializable) do + yield if block_given? + end + end + + private + + def add_error + errors.add(:domain, I18n.t('domain_renew_error_for_domain', domain: domain.name)) + end + end + end +end diff --git a/app/interactions/domains/bulk_renew/start.rb b/app/interactions/domains/bulk_renew/start.rb new file mode 100644 index 000000000..f758b52cd --- /dev/null +++ b/app/interactions/domains/bulk_renew/start.rb @@ -0,0 +1,56 @@ +module Domains + module BulkRenew + class Start < ActiveInteraction::Base + array :domains do + object class: Epp::Domain + end + string :period_element + object :registrar + + def execute + if renewable? + domains.each do |domain| + task = run_task(domain) + manage_errors(task) + end + else + manage_errors(mass_check_balance) + end + end + + private + + def renewable? + mass_check_balance.valid? && mass_check_balance.result + end + + def period + period_element.to_i.zero? ? 1 : period_element.to_i + end + + def unit + period_element[-1] || 'y' + end + + def mass_check_balance + Domains::CheckBalance::Mass.run(domains: domains, + operation: 'renew', + period: period, + unit: unit, + balance: registrar.balance) + end + + def manage_errors(task) + task.errors.each { |k, v| errors.add(k, v) } unless task.valid? + errors.add(:domain, I18n.t('not_enough_funds')) unless task.result + end + + def run_task(domain) + Domains::BulkRenew::SingleDomainRenew.run(domain: domain, + period: period, + unit: unit, + registrar: registrar) + end + end + end +end diff --git a/app/interactions/domains/cancel_force_delete/base.rb b/app/interactions/domains/cancel_force_delete/base.rb new file mode 100644 index 000000000..b15a3ae77 --- /dev/null +++ b/app/interactions/domains/cancel_force_delete/base.rb @@ -0,0 +1,9 @@ +module Domains + module CancelForceDelete + class Base < ActiveInteraction::Base + object :domain, + class: Domain, + description: 'Domain to cancel ForceDelete on' + end + end +end diff --git a/app/interactions/domains/cancel_force_delete/cancel_force_delete.rb b/app/interactions/domains/cancel_force_delete/cancel_force_delete.rb new file mode 100644 index 000000000..7c4ca90e1 --- /dev/null +++ b/app/interactions/domains/cancel_force_delete/cancel_force_delete.rb @@ -0,0 +1,12 @@ +module Domains + module CancelForceDelete + class CancelForceDelete < Base + def execute + compose(RemoveForceDeleteStatuses, inputs) + compose(RestoreStatusesBeforeForceDelete, inputs) + compose(ClearForceDeleteData, inputs) + compose(NotifyRegistrar, inputs) + end + end + end +end diff --git a/app/interactions/domains/cancel_force_delete/clear_force_delete_data.rb b/app/interactions/domains/cancel_force_delete/clear_force_delete_data.rb new file mode 100644 index 000000000..066df04fd --- /dev/null +++ b/app/interactions/domains/cancel_force_delete/clear_force_delete_data.rb @@ -0,0 +1,12 @@ +module Domains + module CancelForceDelete + class ClearForceDeleteData < Base + def execute + domain.force_delete_data = nil + domain.force_delete_date = nil + domain.force_delete_start = nil + domain.save(validate: false) + end + end + end +end diff --git a/app/interactions/domains/cancel_force_delete/notify_registrar.rb b/app/interactions/domains/cancel_force_delete/notify_registrar.rb new file mode 100644 index 000000000..0de69e13e --- /dev/null +++ b/app/interactions/domains/cancel_force_delete/notify_registrar.rb @@ -0,0 +1,10 @@ +module Domains + module CancelForceDelete + class NotifyRegistrar < Base + def execute + domain.registrar.notifications.create!(text: I18n.t('force_delete_cancelled', + domain_name: domain.name)) + end + end + end +end diff --git a/app/interactions/domains/cancel_force_delete/remove_force_delete_statuses.rb b/app/interactions/domains/cancel_force_delete/remove_force_delete_statuses.rb new file mode 100644 index 000000000..c77820ecf --- /dev/null +++ b/app/interactions/domains/cancel_force_delete/remove_force_delete_statuses.rb @@ -0,0 +1,13 @@ +module Domains + module CancelForceDelete + class RemoveForceDeleteStatuses < Base + def execute + domain.statuses.delete(DomainStatus::FORCE_DELETE) + domain.statuses.delete(DomainStatus::SERVER_RENEW_PROHIBITED) + domain.statuses.delete(DomainStatus::SERVER_TRANSFER_PROHIBITED) + domain.statuses.delete(DomainStatus::CLIENT_HOLD) + domain.save(validate: false) + end + end + end +end diff --git a/app/interactions/domains/cancel_force_delete/restore_statuses_before_force_delete.rb b/app/interactions/domains/cancel_force_delete/restore_statuses_before_force_delete.rb new file mode 100644 index 000000000..c55c06764 --- /dev/null +++ b/app/interactions/domains/cancel_force_delete/restore_statuses_before_force_delete.rb @@ -0,0 +1,11 @@ +module Domains + module CancelForceDelete + class RestoreStatusesBeforeForceDelete < Base + def execute + domain.statuses = domain.statuses_before_force_delete + domain.statuses_before_force_delete = nil + domain.save(validate: false) + end + end + end +end diff --git a/app/interactions/domains/check_balance/mass.rb b/app/interactions/domains/check_balance/mass.rb new file mode 100644 index 000000000..58af25a9f --- /dev/null +++ b/app/interactions/domains/check_balance/mass.rb @@ -0,0 +1,33 @@ +module Domains + module CheckBalance + class Mass < ActiveInteraction::Base + array :domains do + object class: Epp::Domain + end + string :operation + integer :period + string :unit + float :balance + + attr_accessor :total_price + + def execute + calculate_total_price + + balance >= @total_price + end + + def calculate_total_price + @total_price = 0 + domains.each do |domain| + task = Domains::CheckBalance::SingleDomain.run(domain: domain, + operation: 'renew', + period: period, + unit: unit) + + task.valid? ? @total_price += task.result : errors.merge!(task.errors) + end + end + end + end +end diff --git a/app/interactions/domains/check_balance/single_domain.rb b/app/interactions/domains/check_balance/single_domain.rb new file mode 100644 index 000000000..166edc4e3 --- /dev/null +++ b/app/interactions/domains/check_balance/single_domain.rb @@ -0,0 +1,26 @@ +module Domains + module CheckBalance + class SingleDomain < ActiveInteraction::Base + object :domain, + class: Epp::Domain + + string :operation + integer :period + string :unit + + def execute + return domain_pricelist.price.amount if domain_pricelist.try(:price) + + errors.add(:domain, I18n.t(:active_price_missing_for_operation_with_domain, + domain: domain.name)) + false + end + + private + + def domain_pricelist + domain.pricelist(operation, period.try(:to_i), unit) + end + end + end +end diff --git a/app/interactions/domains/client_hold/base.rb b/app/interactions/domains/client_hold/base.rb new file mode 100644 index 000000000..c3c626b79 --- /dev/null +++ b/app/interactions/domains/client_hold/base.rb @@ -0,0 +1,10 @@ +module Domains + module ClientHold + class Base < ActiveInteraction::Base + def to_stdout(message) + time = Time.zone.now.utc + STDOUT << "#{time} - #{message}\n" unless Rails.env.test? + end + end + end +end diff --git a/app/interactions/domains/client_hold/process_client_hold.rb b/app/interactions/domains/client_hold/process_client_hold.rb new file mode 100644 index 000000000..8b8d60403 --- /dev/null +++ b/app/interactions/domains/client_hold/process_client_hold.rb @@ -0,0 +1,69 @@ +module Domains + module ClientHold + class ProcessClientHold < Base + object :domain, + class: Domain, + description: 'Domain to set ClientHold on' + + # rubocop:disable Metrics/AbcSize + def execute + notify_on_grace_period if should_notify_on_soft_force_delete? + + return unless client_holdable? + + domain.statuses << DomainStatus::CLIENT_HOLD + to_stdout("DomainCron.start_client_hold: #{domain.id} (#{domain.name}) #{domain.changes}\n") + + domain.save(validate: false) + notify_client_hold + + to_stdout("Successfully set client_hold on (#{domain.name})") + end + + def notify_on_grace_period + domain.registrar.notifications.create!(text: I18n.t('grace_period_started_domain', + domain_name: domain.name, + date: domain.force_delete_start)) + send_mail if domain.template_name.present? + domain.update(contact_notification_sent_date: Time.zone.today) + end + + def notify_client_hold + domain.registrar.notifications.create!(text: I18n.t('force_delete_set_on_domain', + domain_name: domain.name, + outzone_date: domain.outzone_date, + purge_date: domain.purge_date)) + end + + def send_mail + DomainDeleteMailer.forced(domain: domain, + registrar: domain.registrar, + registrant: domain.registrant, + template_name: domain.template_name).deliver_now + end + + def should_notify_on_soft_force_delete? + domain.force_delete_scheduled? && domain.contact_notification_sent_date.blank? && + domain.force_delete_start.to_date <= Time.zone.now.to_date && + domain.force_delete_type.to_sym == :soft && + !domain.statuses.include?(DomainStatus::CLIENT_HOLD) + end + # rubocop:enable Metrics/AbcSize + + def client_holdable? + domain.force_delete_scheduled? && + !domain.statuses.include?(DomainStatus::CLIENT_HOLD) && + domain.force_delete_start.present? && + force_delete_lte_today && force_delete_lte_valid_date + end + + def force_delete_lte_today + domain.force_delete_start + Setting.expire_warning_period.days <= Time.zone.now + end + + def force_delete_lte_valid_date + domain.force_delete_start + Setting.expire_warning_period.days <= domain.valid_to + end + end + end +end diff --git a/app/interactions/domains/client_hold/set_client_hold.rb b/app/interactions/domains/client_hold/set_client_hold.rb new file mode 100644 index 000000000..0e54b531f --- /dev/null +++ b/app/interactions/domains/client_hold/set_client_hold.rb @@ -0,0 +1,17 @@ +module Domains + module ClientHold + class SetClientHold < Base + def execute + to_stdout('Setting client_hold to domains\n') + + ::PaperTrail.request.whodunnit = "cron - #{self.class.name}" + + ::Domain.force_delete_scheduled.each do |domain| + Domains::ClientHold::ProcessClientHold.run(domain: domain) + end + + to_stdout('All client_hold setting are done\n') + end + end + end +end diff --git a/app/interactions/domains/delete/base.rb b/app/interactions/domains/delete/base.rb new file mode 100644 index 000000000..434712f72 --- /dev/null +++ b/app/interactions/domains/delete/base.rb @@ -0,0 +1,9 @@ +module Domains + module Delete + class Base < ActiveInteraction::Base + object :domain, + class: Domain, + description: 'Domain to delete' + end + end +end diff --git a/app/interactions/domains/delete/do_delete.rb b/app/interactions/domains/delete/do_delete.rb new file mode 100644 index 000000000..202c36938 --- /dev/null +++ b/app/interactions/domains/delete/do_delete.rb @@ -0,0 +1,13 @@ +module Domains + module Delete + class DoDelete < Base + def execute + ::PaperTrail.request.whodunnit = "interaction - #{self.class.name}" + WhoisRecord.where(domain_id: domain.id).destroy_all + + domain.destroy + compose(Domains::Delete::NotifyRegistrar, inputs) + end + end + end +end diff --git a/app/interactions/domains/delete/notify_registrar.rb b/app/interactions/domains/delete/notify_registrar.rb new file mode 100644 index 000000000..d6af00fd8 --- /dev/null +++ b/app/interactions/domains/delete/notify_registrar.rb @@ -0,0 +1,14 @@ +module Domains + module Delete + class NotifyRegistrar < Base + def execute + bye_bye = domain.versions.last + domain.registrar.notifications.create!( + text: "#{I18n.t(:domain_deleted)}: #{domain.name}", + attached_obj_id: bye_bye.id, + attached_obj_type: bye_bye.class.to_s + ) + end + end + end +end diff --git a/app/interactions/domains/delete_confirm/base.rb b/app/interactions/domains/delete_confirm/base.rb new file mode 100644 index 000000000..5bd07f40f --- /dev/null +++ b/app/interactions/domains/delete_confirm/base.rb @@ -0,0 +1,53 @@ +module Domains + module DeleteConfirm + class Base < ActiveInteraction::Base + object :domain, + class: Domain, + description: 'Domain to confirm release' + string :action + string :initiator, + default: nil + + validates :domain, :action, presence: true + validates :action, inclusion: { in: [RegistrantVerification::CONFIRMED, + RegistrantVerification::REJECTED] } + + def raise_errors!(domain) + return unless domain.errors.any? + + message = "domain #{domain.name} failed with errors #{domain.errors.full_messages}" + throw message + end + + def notify_registrar(message_key) + domain.registrar.notifications.create!( + text: "#{I18n.t(message_key)}: #{domain.name}", + attached_obj_id: domain.id, + attached_obj_type: domain.class.to_s + ) + end + + def preclean_pendings + domain.registrant_verification_token = nil + domain.registrant_verification_asked_at = nil + end + + def clean_pendings! + domain.is_admin = true + domain.registrant_verification_token = nil + domain.registrant_verification_asked_at = nil + domain.pending_json = {} + clear_statuses + domain.save + end + + def clear_statuses + domain.statuses.delete(DomainStatus::PENDING_DELETE_CONFIRMATION) + domain.statuses.delete(DomainStatus::PENDING_UPDATE) + domain.statuses.delete(DomainStatus::PENDING_DELETE) + domain.status_notes[DomainStatus::PENDING_UPDATE] = '' + domain.status_notes[DomainStatus::PENDING_DELETE] = '' + end + end + end +end diff --git a/app/interactions/domains/delete_confirm/process_action.rb b/app/interactions/domains/delete_confirm/process_action.rb new file mode 100644 index 000000000..59f23de67 --- /dev/null +++ b/app/interactions/domains/delete_confirm/process_action.rb @@ -0,0 +1,17 @@ +module Domains + module DeleteConfirm + class ProcessAction < Base + def execute + ::PaperTrail.request.whodunnit = "interaction - #{self.class.name} - #{action} by"\ + " #{initiator}" + + case action + when RegistrantVerification::CONFIRMED + compose(ProcessDeleteConfirmed, inputs) + when RegistrantVerification::REJECTED + compose(ProcessDeleteRejected, inputs) + end + end + end + end +end diff --git a/app/interactions/domains/delete_confirm/process_delete_confirmed.rb b/app/interactions/domains/delete_confirm/process_delete_confirmed.rb new file mode 100644 index 000000000..1f3068107 --- /dev/null +++ b/app/interactions/domains/delete_confirm/process_delete_confirmed.rb @@ -0,0 +1,50 @@ +module Domains + module DeleteConfirm + class ProcessDeleteConfirmed < Base + def execute + notify_registrar(:poll_pending_delete_confirmed_by_registrant) + domain.apply_pending_delete! + raise_errors!(domain) + end + + def apply_pending_delete! + preclean_pendings + clean_pendings! + DomainDeleteMailer.accepted(domain).deliver_now + domain.set_pending_delete! + end + + def set_pending_delete! + unless domain.pending_deletable? + add_epp_error + return + end + + domain.delete_date = delete_date + domain.statuses << DomainStatus::PENDING_DELETE + set_server_hold if server_holdable? + domain.save(validate: false) + end + + def set_server_hold + domain.statuses << DomainStatus::SERVER_HOLD + domain.outzone_at = Time.current + end + + def server_holdable? + return false if domain.statuses.include?(DomainStatus::SERVER_HOLD) + return false if domain.statuses.include?(DomainStatus::SERVER_MANUAL_INZONE) + + true + end + + def delete_date + Time.zone.today + Setting.redemption_grace_period.days + 1.day + end + + def add_epp_error + domain.add_epp_error('2304', nil, nil, I18n.t(:object_status_prohibits_operation)) + end + end + end +end diff --git a/app/interactions/domains/delete_confirm/process_delete_rejected.rb b/app/interactions/domains/delete_confirm/process_delete_rejected.rb new file mode 100644 index 000000000..25b491660 --- /dev/null +++ b/app/interactions/domains/delete_confirm/process_delete_rejected.rb @@ -0,0 +1,32 @@ +module Domains + module DeleteConfirm + class ProcessDeleteRejected < Base + def execute + domain.cancel_pending_delete + notify_registrar(:poll_pending_delete_rejected_by_registrant) + domain.save(validate: false) + raise_errors!(domain) + + send_domain_delete_rejected_email + end + + def send_domain_delete_rejected_email + if domain.registrant_verification_token.blank? + warn "EMAIL NOT DELIVERED: registrant_verification_token is missing for #{domain.name}" + elsif domain.registrant_verification_asked_at.blank? + warn "EMAIL NOT DELIVERED: registrant_verification_asked_at is missing for #{domain.name}" + else + send_email + end + end + + def warn(message) + Rails.logger.warn(message) + end + + def send_email + DomainDeleteMailer.rejected(domain).deliver_now + end + end + end +end diff --git a/app/interactions/domains/delete_confirm_email/send_request.rb b/app/interactions/domains/delete_confirm_email/send_request.rb new file mode 100644 index 000000000..7070423a6 --- /dev/null +++ b/app/interactions/domains/delete_confirm_email/send_request.rb @@ -0,0 +1,24 @@ +module Domains + module DeleteConfirmEmail + class SendRequest < ActiveInteraction::Base + object :domain, + class: Domain, + description: 'Domain to send delete confirmation' + + def execute + log + DomainDeleteMailer.confirmation_request(domain: domain, + registrar: domain.registrar, + registrant: domain.registrant).deliver_later + end + + private + + def log + message = "Send DomainDeleteMailer#confirm email for domain #{domain.name} (##{domain.id})"\ + " to #{domain.registrant.email}" + Rails.logger.info(message) + end + end + end +end diff --git a/app/interactions/domains/expire_period/base.rb b/app/interactions/domains/expire_period/base.rb new file mode 100644 index 000000000..eab8171bf --- /dev/null +++ b/app/interactions/domains/expire_period/base.rb @@ -0,0 +1,10 @@ +module Domains + module ExpirePeriod + class Base < ActiveInteraction::Base + def to_stdout(message) + time = Time.zone.now.utc + STDOUT << "#{time} - #{message}\n" unless Rails.env.test? + end + end + end +end diff --git a/app/interactions/domains/expire_period/process_expired.rb b/app/interactions/domains/expire_period/process_expired.rb new file mode 100644 index 000000000..595de1228 --- /dev/null +++ b/app/interactions/domains/expire_period/process_expired.rb @@ -0,0 +1,28 @@ +module Domains + module ExpirePeriod + class ProcessExpired < Base + object :domain, + class: Domain, + description: 'Domain to set expiration' + + def execute + set_graceful_expired + to_stdout("start_expire_period: ##{domain.id} (#{domain.name}) #{domain.changes}") + + saved = domain.save(validate: false) + + DomainExpireEmailJob.enqueue(domain.id, run_at: send_time) if saved + end + + def set_graceful_expired + domain.outzone_at = domain.expire_time + Domain.expire_warning_period + domain.delete_date = domain.outzone_at + Domain.redemption_grace_period + domain.statuses |= [DomainStatus::EXPIRED] + end + + def send_time + domain.valid_to + Setting.expiration_reminder_mail.to_i.days + end + end + end +end diff --git a/app/interactions/domains/expire_period/start.rb b/app/interactions/domains/expire_period/start.rb new file mode 100644 index 000000000..1ed2342f1 --- /dev/null +++ b/app/interactions/domains/expire_period/start.rb @@ -0,0 +1,19 @@ +module Domains + module ExpirePeriod + class Start < Base + def execute + ::PaperTrail.request.whodunnit = "cron - #{self.class.name}" + count = 0 + + Domain.expired.each do |domain| + next unless domain.expirable? + + count += 1 + Domains::ExpirePeriod::ProcessExpired.run(domain: domain) + end + + to_stdout("Successfully expired #{count}") + end + end + end +end diff --git a/app/interactions/domains/expired_pendings/base.rb b/app/interactions/domains/expired_pendings/base.rb new file mode 100644 index 000000000..7faa32050 --- /dev/null +++ b/app/interactions/domains/expired_pendings/base.rb @@ -0,0 +1,10 @@ +module Domains + module ExpiredPendings + class Base < ActiveInteraction::Base + def to_stdout(message) + time = Time.zone.now.utc + STDOUT << "#{time} - #{message}\n" unless Rails.env.test? + end + end + end +end diff --git a/app/interactions/domains/expired_pendings/clean_all.rb b/app/interactions/domains/expired_pendings/clean_all.rb new file mode 100644 index 000000000..1dbab266c --- /dev/null +++ b/app/interactions/domains/expired_pendings/clean_all.rb @@ -0,0 +1,35 @@ +module Domains + module ExpiredPendings + class CleanAll < Base + def execute + to_stdout('Clean expired domain pendings') + + ::PaperTrail.request.whodunnit = "cron - #{self.class.name}" + + count = 0 + expired_pending_domains.each do |domain| + log_error(domain) && next unless need_to_be_cleared?(domain) + count += 1 + Domains::ExpiredPendings::ProcessClean.run(domain: domain) + end + to_stdout("Successfully cancelled #{count} domain pendings") + end + + private + + def need_to_be_cleared?(domain) + domain.pending_update? || domain.pending_delete? || domain.pending_delete_confirmation? + end + + def log_error(domain) + to_stdout("ISSUE: DOMAIN #{domain.id}: #{domain.name} IS IN EXPIRED PENDING LIST, "\ + 'but no pendingDelete/pendingUpdate state present!') + end + + def expired_pending_domains + expire_at = Setting.expire_pending_confirmation.hours.ago + Domain.where('registrant_verification_asked_at <= ?', expire_at) + end + end + end +end diff --git a/app/interactions/domains/expired_pendings/process_clean.rb b/app/interactions/domains/expired_pendings/process_clean.rb new file mode 100644 index 000000000..c6277c3c5 --- /dev/null +++ b/app/interactions/domains/expired_pendings/process_clean.rb @@ -0,0 +1,60 @@ +module Domains + module ExpiredPendings + class ProcessClean < Base + object :domain, + class: Domain + + def execute + check_notify + clean_pendings + + to_stdout("DomainCron.clean_expired_pendings: ##{domain.id} (#{domain.name})") + UpdateWhoisRecordJob.enqueue domain.name, 'domain' + end + + private + + def notify_pending_update + RegistrantChangeMailer.expired(domain: domain, + registrar: domain.registrar, + registrant: domain.registrant).deliver_later + end + + def notify_pending_delete + DomainDeleteMailer.expired(domain).deliver_later + end + + def clean_pendings + clean_verification_data + domain.pending_json = {} + clean_statuses + domain.save + end + + def statuses_to_clean + [DomainStatus::PENDING_DELETE_CONFIRMATION, + DomainStatus::PENDING_UPDATE, + DomainStatus::PENDING_DELETE] + end + + def clean_statuses + domain.statuses = domain.statuses - statuses_to_clean + domain.status_notes[DomainStatus::PENDING_UPDATE] = '' + domain.status_notes[DomainStatus::PENDING_DELETE] = '' + end + + def clean_verification_data + domain.registrant_verification_token = nil + domain.registrant_verification_asked_at = nil + end + + def check_notify + notify_pending_update if domain.pending_update? + + return unless domain.pending_delete? || domain.pending_delete_confirmation? + + notify_pending_delete + end + end + end +end diff --git a/app/interactions/domains/force_delete/base.rb b/app/interactions/domains/force_delete/base.rb new file mode 100644 index 000000000..27601c1d2 --- /dev/null +++ b/app/interactions/domains/force_delete/base.rb @@ -0,0 +1,17 @@ +module Domains + module ForceDelete + class Base < ActiveInteraction::Base + object :domain, + class: Domain, + description: 'Domain to set ForceDelete on' + symbol :type, + default: :fast_track, + description: 'Force delete type, might be :fast_track or :soft' + boolean :notify_by_email, + default: false, + description: 'Do we need to send email notification' + + validates :type, inclusion: { in: %i[fast_track soft] } + end + end +end diff --git a/app/interactions/domains/force_delete/check_discarded.rb b/app/interactions/domains/force_delete/check_discarded.rb new file mode 100644 index 000000000..cbb9c1dc6 --- /dev/null +++ b/app/interactions/domains/force_delete/check_discarded.rb @@ -0,0 +1,12 @@ +module Domains + module ForceDelete + class CheckDiscarded < Base + def execute + return true unless domain.discarded? + + message = 'Force delete procedure cannot be scheduled while a domain is discarded' + errors.add(:domain, message) + end + end + end +end diff --git a/app/interactions/domains/force_delete/notify_by_email.rb b/app/interactions/domains/force_delete/notify_by_email.rb new file mode 100644 index 000000000..b60f54a5e --- /dev/null +++ b/app/interactions/domains/force_delete/notify_by_email.rb @@ -0,0 +1,23 @@ +module Domains + module ForceDelete + class NotifyByEmail < Base + def execute + return unless notify_by_email + + if type == :fast_track + send_email + domain.update(contact_notification_sent_date: Time.zone.today) + else + domain.update(template_name: domain.notification_template) + end + end + + def send_email + DomainDeleteMailer.forced(domain: domain, + registrar: domain.registrar, + registrant: domain.registrant, + template_name: domain.notification_template).deliver_now + end + end + end +end diff --git a/app/interactions/domains/force_delete/notify_registrar.rb b/app/interactions/domains/force_delete/notify_registrar.rb new file mode 100644 index 000000000..522502640 --- /dev/null +++ b/app/interactions/domains/force_delete/notify_registrar.rb @@ -0,0 +1,12 @@ +module Domains + module ForceDelete + class NotifyRegistrar < Base + def execute + domain.registrar.notifications.create!(text: I18n.t('force_delete_set_on_domain', + domain_name: domain.name, + outzone_date: domain.outzone_date, + purge_date: domain.purge_date)) + end + end + end +end diff --git a/app/interactions/domains/force_delete/post_set_process.rb b/app/interactions/domains/force_delete/post_set_process.rb new file mode 100644 index 000000000..904149f93 --- /dev/null +++ b/app/interactions/domains/force_delete/post_set_process.rb @@ -0,0 +1,19 @@ +module Domains + module ForceDelete + class PostSetProcess < Base + def execute + statuses = domain.statuses + # Stop all pending actions + statuses.delete(DomainStatus::PENDING_UPDATE) + statuses.delete(DomainStatus::PENDING_TRANSFER) + statuses.delete(DomainStatus::PENDING_RENEW) + statuses.delete(DomainStatus::PENDING_CREATE) + + # Allow deletion + statuses.delete(DomainStatus::CLIENT_DELETE_PROHIBITED) + statuses.delete(DomainStatus::SERVER_DELETE_PROHIBITED) + domain.save(validate: false) + end + end + end +end diff --git a/app/interactions/domains/force_delete/prepare_domain.rb b/app/interactions/domains/force_delete/prepare_domain.rb new file mode 100644 index 000000000..74eea21ed --- /dev/null +++ b/app/interactions/domains/force_delete/prepare_domain.rb @@ -0,0 +1,15 @@ +module Domains + module ForceDelete + class PrepareDomain < Base + STATUSES_TO_SET = [DomainStatus::FORCE_DELETE, + DomainStatus::SERVER_RENEW_PROHIBITED, + DomainStatus::SERVER_TRANSFER_PROHIBITED].freeze + + def execute + domain.statuses_before_force_delete = domain.statuses + domain.statuses |= STATUSES_TO_SET + domain.save(validate: false) + end + end + end +end diff --git a/app/interactions/domains/force_delete/set_force_delete.rb b/app/interactions/domains/force_delete/set_force_delete.rb new file mode 100644 index 000000000..16a0b09fa --- /dev/null +++ b/app/interactions/domains/force_delete/set_force_delete.rb @@ -0,0 +1,14 @@ +module Domains + module ForceDelete + class SetForceDelete < Base + def execute + compose(CheckDiscarded, inputs) + compose(PrepareDomain, inputs) + compose(SetStatus, inputs) + compose(PostSetProcess, inputs) + compose(NotifyRegistrar, inputs) + compose(NotifyByEmail, inputs) + end + end + end +end diff --git a/app/interactions/domains/force_delete/set_status.rb b/app/interactions/domains/force_delete/set_status.rb new file mode 100644 index 000000000..b0a53ad82 --- /dev/null +++ b/app/interactions/domains/force_delete/set_status.rb @@ -0,0 +1,40 @@ +module Domains + module ForceDelete + class SetStatus < Base + def execute + domain.force_delete_type = type + type == :fast_track ? force_delete_fast_track : force_delete_soft + domain.save(validate: false) + end + + def force_delete_fast_track + domain.force_delete_date = Time.zone.today + + expire_warning_period_days + + redemption_grace_period_days + domain.force_delete_start = Time.zone.today + 1.day + end + + def force_delete_soft + years = (domain.valid_to.to_date - Time.zone.today).to_i / 365 + soft_forcedelete_dates(years) if years.positive? + end + + private + + def soft_forcedelete_dates(years) + domain.force_delete_start = domain.valid_to - years.years + domain.force_delete_date = domain.force_delete_start + + Setting.expire_warning_period.days + + Setting.redemption_grace_period.days + end + + def redemption_grace_period_days + Setting.redemption_grace_period.days + 1.day + end + + def expire_warning_period_days + Setting.expire_warning_period.days + end + end + end +end diff --git a/app/interactions/domains/redemption_grace_period/base.rb b/app/interactions/domains/redemption_grace_period/base.rb new file mode 100644 index 000000000..5d1ede289 --- /dev/null +++ b/app/interactions/domains/redemption_grace_period/base.rb @@ -0,0 +1,10 @@ +module Domains + module RedemptionGracePeriod + class Base < ActiveInteraction::Base + def to_stdout(message) + time = Time.zone.now.utc + STDOUT << "#{time} - #{message}\n" unless Rails.env.test? + end + end + end +end diff --git a/app/interactions/domains/redemption_grace_period/process_grace_period.rb b/app/interactions/domains/redemption_grace_period/process_grace_period.rb new file mode 100644 index 000000000..0f120a996 --- /dev/null +++ b/app/interactions/domains/redemption_grace_period/process_grace_period.rb @@ -0,0 +1,20 @@ +module Domains + module RedemptionGracePeriod + class ProcessGracePeriod < Base + object :domain, + class: Domain + + def execute + domain.statuses << DomainStatus::SERVER_HOLD + to_stdout(process_msg) + domain.save(validate: false) + end + + private + + def process_msg + "start_redemption_grace_period: #{domain.id} (#{domain.name}) #{domain.changes}" + end + end + end +end diff --git a/app/interactions/domains/redemption_grace_period/start.rb b/app/interactions/domains/redemption_grace_period/start.rb new file mode 100644 index 000000000..ef7f42e58 --- /dev/null +++ b/app/interactions/domains/redemption_grace_period/start.rb @@ -0,0 +1,20 @@ +module Domains + module RedemptionGracePeriod + class Start < Base + def execute + to_stdout('Setting server_hold to domains') + + ::PaperTrail.request.whodunnit = "cron - #{self.class.name}" + count = 0 + + Domain.outzone_candidates.each do |domain| + next unless domain.server_holdable? + + count += 1 + Domains::RedemptionGracePeriod::ProcessGracePeriod.run(domain: domain) + end + to_stdout("Successfully set server_hold to #{count} of domains") + end + end + end +end diff --git a/app/interactions/domains/update_confirm/base.rb b/app/interactions/domains/update_confirm/base.rb new file mode 100644 index 000000000..0e1fa81d3 --- /dev/null +++ b/app/interactions/domains/update_confirm/base.rb @@ -0,0 +1,53 @@ +module Domains + module UpdateConfirm + class Base < ActiveInteraction::Base + object :domain, + class: Domain, + description: 'Domain to confirm update' + string :action + string :initiator, + default: nil + + validates :domain, :action, presence: true + validates :action, inclusion: { in: [RegistrantVerification::CONFIRMED, + RegistrantVerification::REJECTED] } + + def raise_errors!(domain) + return unless domain.errors.any? + + message = "domain #{domain.name} failed with errors #{domain.errors.full_messages}" + throw message + end + + def notify_registrar(message_key) + domain.registrar.notifications.create!( + text: "#{I18n.t(message_key)}: #{domain.name}", + attached_obj_id: domain.id, + attached_obj_type: domain.class.to_s + ) + end + + def preclean_pendings + domain.registrant_verification_token = nil + domain.registrant_verification_asked_at = nil + end + + def clean_pendings! + domain.is_admin = true + domain.registrant_verification_token = nil + domain.registrant_verification_asked_at = nil + domain.pending_json = {} + clear_statuses + domain.save + end + + def clear_statuses + domain.statuses.delete(DomainStatus::PENDING_DELETE_CONFIRMATION) + domain.statuses.delete(DomainStatus::PENDING_UPDATE) + domain.statuses.delete(DomainStatus::PENDING_DELETE) + domain.status_notes[DomainStatus::PENDING_UPDATE] = '' + domain.status_notes[DomainStatus::PENDING_DELETE] = '' + end + end + end +end diff --git a/app/interactions/domains/update_confirm/process_action.rb b/app/interactions/domains/update_confirm/process_action.rb new file mode 100644 index 000000000..6ef8d0fe6 --- /dev/null +++ b/app/interactions/domains/update_confirm/process_action.rb @@ -0,0 +1,17 @@ +module Domains + module UpdateConfirm + class ProcessAction < Base + def execute + ::PaperTrail.request.whodunnit = "interaction - #{self.class.name} - #{action} by"\ + " #{initiator}" + + case action + when RegistrantVerification::CONFIRMED + compose(ProcessUpdateConfirmed, inputs) + when RegistrantVerification::REJECTED + compose(ProcessUpdateRejected, inputs) + end + end + end + end +end diff --git a/app/interactions/domains/update_confirm/process_update_confirmed.rb b/app/interactions/domains/update_confirm/process_update_confirmed.rb new file mode 100644 index 000000000..cb69d042e --- /dev/null +++ b/app/interactions/domains/update_confirm/process_update_confirmed.rb @@ -0,0 +1,34 @@ +module Domains + module UpdateConfirm + class ProcessUpdateConfirmed < Base + def execute + ActiveRecord::Base.transaction do + old_registrant = domain.registrant + notify_registrar(:poll_pending_update_confirmed_by_registrant) + + apply_pending_update! + raise_errors!(domain) + RegistrantChange.new(domain: domain, old_registrant: old_registrant).confirm + end + end + + def apply_pending_update! + preclean_pendings + update_domain + clean_pendings! + + WhoisRecord.find_by(domain_id: domain.id).save # need to reload model + end + + # rubocop:disable Metrics/AbcSize + def update_domain + user = ApiUser.find(domain.pending_json['current_user_id']) + frame = Nokogiri::XML(domain.pending_json['frame']) + domain.upid = user.registrar.id if user.registrar + domain.up_date = Time.zone.now + domain.update(frame, user, false) + end + # rubocop:enable Metrics/AbcSize + end + end +end diff --git a/app/interactions/domains/update_confirm/process_update_rejected.rb b/app/interactions/domains/update_confirm/process_update_rejected.rb new file mode 100644 index 000000000..1d7b75b0e --- /dev/null +++ b/app/interactions/domains/update_confirm/process_update_rejected.rb @@ -0,0 +1,18 @@ +module Domains + module UpdateConfirm + class ProcessUpdateRejected < Base + def execute + ActiveRecord::Base.transaction do + RegistrantChangeMailer.rejected(domain: domain, + registrar: domain.registrar, + registrant: domain.registrant).deliver_now + + notify_registrar(:poll_pending_update_rejected_by_registrant) + + preclean_pendings + clean_pendings! + end + end + end + end +end diff --git a/app/interactions/whois/delete_record.rb b/app/interactions/whois/delete_record.rb new file mode 100644 index 000000000..62e416d2f --- /dev/null +++ b/app/interactions/whois/delete_record.rb @@ -0,0 +1,48 @@ +module Whois + class DeleteRecord < ActiveInteraction::Base + string :name + string :type + + validates :type, inclusion: { in: %w[reserved blocked domain disputed zone] } + + def execute + send "delete_#{type}", name + end + + # 1. deleting own + # 2. trying to regenerate reserved in order domain is still in the list + def delete_domain(name) + WhoisRecord.where(name: name).destroy_all + + BlockedDomain.find_by(name: name).try(:generate_data) + ReservedDomain.find_by(name: name).try(:generate_data) + Dispute.active.find_by(domain_name: name).try(:generate_data) + end + + def delete_reserved(name) + remove_status_from_whois(domain_name: name, domain_status: 'Reserved') + end + + def delete_blocked(name) + delete_reserved(name) + end + + def delete_disputed(name) + return if Dispute.active.find_by(domain_name: name).present? + + remove_status_from_whois(domain_name: name, domain_status: 'disputed') + end + + def delete_zone(name) + WhoisRecord.where(name: name).destroy_all + Whois::Record.where(name: name).destroy_all + end + + def remove_status_from_whois(domain_name:, domain_status:) + Whois::Record.where(name: domain_name).each do |r| + r.json['status'] = r.json['status'].delete_if { |status| status == domain_status } + r.json['status'].blank? ? r.destroy : r.save + end + end + end +end diff --git a/app/interactions/whois/update.rb b/app/interactions/whois/update.rb new file mode 100644 index 000000000..bd37b4576 --- /dev/null +++ b/app/interactions/whois/update.rb @@ -0,0 +1,39 @@ +module Whois + class Update < ActiveInteraction::Base + array :names + string :type + + validates :type, inclusion: { in: %w[reserved blocked domain disputed zone] } + + def execute + ::PaperTrail.request.whodunnit = "job - #{self.class.name} - #{type}" + + klass = determine_class + + Array(names).each do |name| + record = find_record(klass, name) + if record + Whois::UpdateRecord.run(record: record, type: type) + else + Whois::DeleteRecord.run(name: name, type: type) + end + end + end + + private + + def determine_class + case type + when 'reserved' then ReservedDomain + when 'blocked' then BlockedDomain + when 'domain' then Domain + when 'disputed' then Dispute.active + else DNS::Zone + end + end + + def find_record(klass, name) + klass == DNS::Zone ? klass.find_by(origin: name) : klass.find_by(name: name) + end + end +end diff --git a/app/interactions/whois/update_record.rb b/app/interactions/whois/update_record.rb new file mode 100644 index 000000000..6fc838968 --- /dev/null +++ b/app/interactions/whois/update_record.rb @@ -0,0 +1,32 @@ +module Whois + class UpdateRecord < ActiveInteraction::Base + interface :record + string :type + + validates :type, inclusion: { in: %w[reserved blocked domain disputed zone] } + + def execute + send "update_#{type}", record + end + + def update_domain(domain) + domain.whois_record ? domain.whois_record.save : domain.create_whois_record + end + + def update_reserved(record) + record.generate_data + end + + def update_blocked(record) + update_reserved(record) + end + + def update_disputed(record) + update_reserved(record) + end + + def update_zone(record) + update_reserved(record) + end + end +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 000000000..a009ace51 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/app/jobs/domain_delete_confirm_email_job.rb b/app/jobs/domain_delete_confirm_email_job.rb deleted file mode 100644 index ea5a9bf49..000000000 --- a/app/jobs/domain_delete_confirm_email_job.rb +++ /dev/null @@ -1,22 +0,0 @@ -class DomainDeleteConfirmEmailJob < Que::Job - def run(domain_id) - domain = Domain.find(domain_id) - - log(domain) - DomainDeleteMailer.confirmation_request(domain: domain, - registrar: domain.registrar, - registrant: domain.registrant).deliver_now - end - - private - - def log(domain) - message = "Send DomainDeleteMailer#confirm email for domain #{domain.name} (##{domain.id})" \ - " to #{domain.registrant.email}" - logger.info(message) - end - - def logger - Rails.logger - end -end diff --git a/app/jobs/domain_delete_confirm_job.rb b/app/jobs/domain_delete_confirm_job.rb index e94f2432c..afea3da59 100644 --- a/app/jobs/domain_delete_confirm_job.rb +++ b/app/jobs/domain_delete_confirm_job.rb @@ -1,39 +1,11 @@ -class DomainDeleteConfirmJob < Que::Job - def run(domain_id, action, initiator = nil) - ::PaperTrail.request.whodunnit = "job - #{self.class.name} - #{action} by #{initiator}" - # it's recommended to keep transaction against job table as short as possible. - ActiveRecord::Base.transaction do - domain = Epp::Domain.find(domain_id) +class DomainDeleteConfirmJob < ApplicationJob + queue_as :default - case action - when RegistrantVerification::CONFIRMED - domain.notify_registrar(:poll_pending_delete_confirmed_by_registrant) - domain.apply_pending_delete! - raise_errors!(domain) + def perform(domain_id, action, initiator = nil) + domain = Epp::Domain.find(domain_id) - when RegistrantVerification::REJECTED - domain.statuses.delete(DomainStatus::PENDING_DELETE_CONFIRMATION) - domain.notify_registrar(:poll_pending_delete_rejected_by_registrant) - - domain.cancel_pending_delete - domain.save(validate: false) - raise_errors!(domain) - - if domain.registrant_verification_token.blank? - Rails.logger.warn "EMAIL NOT DELIVERED: registrant_verification_token is missing for #{domain.name}" - elsif domain.registrant_verification_asked_at.blank? - Rails.logger.warn "EMAIL NOT DELIVERED: registrant_verification_asked_at is missing for #{domain.name}" - else - DomainDeleteMailer.rejected(domain).deliver_now - end - end - - destroy # it's best to destroy the job in the same transaction - end - end - - - def raise_errors!(domain) - throw "domain #{domain.name} failed with errors #{domain.errors.full_messages}" if domain.errors.any? + Domains::DeleteConfirm::ProcessAction.run(domain: domain, + action: action, + initiator: initiator) end end diff --git a/app/jobs/domain_delete_job.rb b/app/jobs/domain_delete_job.rb index 43e0bb844..5577908c6 100644 --- a/app/jobs/domain_delete_job.rb +++ b/app/jobs/domain_delete_job.rb @@ -3,15 +3,6 @@ class DomainDeleteJob < Que::Job def run(domain_id) domain = Domain.find(domain_id) - ::PaperTrail.request.whodunnit = "job - #{self.class.name}" - WhoisRecord.where(domain_id: domain.id).destroy_all - - domain.destroy - bye_bye = domain.versions.last - domain.registrar.notifications.create!( - text: "#{I18n.t(:domain_deleted)}: #{domain.name}", - attached_obj_id: bye_bye.id, - attached_obj_type: bye_bye.class.to_s - ) + Domains::Delete::DoDelete.run(domain: domain) end end diff --git a/app/jobs/domain_update_confirm_job.rb b/app/jobs/domain_update_confirm_job.rb index f3665f1e8..403318ca6 100644 --- a/app/jobs/domain_update_confirm_job.rb +++ b/app/jobs/domain_update_confirm_job.rb @@ -1,37 +1,10 @@ -class DomainUpdateConfirmJob < Que::Job - def run(domain_id, action, initiator = nil) - ::PaperTrail.request.whodunnit = "job - #{self.class.name} - #{action} by #{initiator}" - # it's recommended to keep transaction against job table as short as possible. - ActiveRecord::Base.transaction do - domain = Epp::Domain.find(domain_id) - domain.is_admin = true - case action - when RegistrantVerification::CONFIRMED - old_registrant = domain.registrant - domain.notify_registrar(:poll_pending_update_confirmed_by_registrant) - raise_errors!(domain) +class DomainUpdateConfirmJob < ApplicationJob + queue_as :default - domain.apply_pending_update! - raise_errors!(domain) - - domain.clean_pendings! - raise_errors!(domain) - RegistrantChange.new(domain: domain, old_registrant: old_registrant).confirm - when RegistrantVerification::REJECTED - RegistrantChangeMailer.rejected(domain: domain, - registrar: domain.registrar, - registrant: domain.registrant).deliver_now - - domain.notify_registrar(:poll_pending_update_rejected_by_registrant) - - domain.preclean_pendings - domain.clean_pendings! - end - destroy # it's best to destroy the job in the same transaction - end - end - - def raise_errors!(domain) - throw "domain #{domain.name} failed with errors #{domain.errors.full_messages}" if domain.errors.any? + def perform(domain_id, action, initiator = nil) + domain = Epp::Domain.find(domain_id) + Domains::UpdateConfirm::ProcessAction.run(domain: domain, + action: action, + initiator: initiator) end end diff --git a/app/jobs/update_whois_record_job.rb b/app/jobs/update_whois_record_job.rb index 2973d5a6b..d067807a0 100644 --- a/app/jobs/update_whois_record_job.rb +++ b/app/jobs/update_whois_record_job.rb @@ -1,87 +1,5 @@ class UpdateWhoisRecordJob < Que::Job - def run(names, type) - ::PaperTrail.request.whodunnit = "job - #{self.class.name} - #{type}" - - klass = determine_class(type) - - Array(names).each do |name| - record = find_record(klass, name) - if record - send "update_#{type}", record - else - send "delete_#{type}", name - end - end - end - - def find_record(klass, name) - klass == DNS::Zone ? klass.find_by(origin: name) : klass.find_by(name: name) - end - - def determine_class(type) - case type - when 'reserved' then ReservedDomain - when 'blocked' then BlockedDomain - when 'domain' then Domain - when 'disputed' then Dispute.active - when 'zone' then DNS::Zone - end - end - - def update_domain(domain) - domain.whois_record ? domain.whois_record.save : domain.create_whois_record - end - - def update_reserved(record) - record.generate_data - end - - def update_blocked(record) - update_reserved(record) - end - - def update_disputed(record) - update_reserved(record) - end - - def update_zone(record) - update_reserved(record) - end - - # 1. deleting own - # 2. trying to regenerate reserved in order domain is still in the list - def delete_domain(name) - WhoisRecord.where(name: name).destroy_all - - BlockedDomain.find_by(name: name).try(:generate_data) - ReservedDomain.find_by(name: name).try(:generate_data) - Dispute.active.find_by(domain_name: name).try(:generate_data) - end - - def delete_reserved(name) - remove_status_from_whois(domain_name: name, domain_status: 'Reserved') - end - - def delete_blocked(name) - delete_reserved(name) - end - - def delete_disputed(name) - return if Dispute.active.find_by(domain_name: name).present? - - remove_status_from_whois(domain_name: name, domain_status: 'disputed') - end - - def delete_zone(name) - WhoisRecord.where(name: name).destroy_all - Whois::Record.where(name: name).destroy_all - end - - def remove_status_from_whois(domain_name:, domain_status:) - Whois::Record.where(name: domain_name).each do |r| - r.json['status'] = r.json['status'].delete_if { |status| status == domain_status } - r.json['status'].blank? ? r.destroy : r.save - end + Whois::Update.run(names: [names].flatten, type: type) end end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 9269a1102..af5d59d63 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,15 @@ class ApplicationMailer < ActionMailer::Base append_view_path Rails.root.join('app', 'views', 'mailers') layout 'mailer' -end \ No newline at end of file + + def registrant_confirm_url(domain:, method:) + token = domain.registrant_verification_token + base_url = ENV['registrant_portal_verifications_base_url'] + + url = registrant_domain_delete_confirm_url(domain, token: token) if method == 'delete' + url ||= registrant_domain_update_confirm_url(domain, token: token) + return url if base_url.blank? + + "#{base_url}/confirmation/#{domain.name_puny}/#{method}/#{token}" + end +end diff --git a/app/mailers/domain_delete_mailer.rb b/app/mailers/domain_delete_mailer.rb index dbacab68d..3e4507194 100644 --- a/app/mailers/domain_delete_mailer.rb +++ b/app/mailers/domain_delete_mailer.rb @@ -2,7 +2,7 @@ class DomainDeleteMailer < ApplicationMailer def confirmation_request(domain:, registrar:, registrant:) @domain = DomainPresenter.new(domain: domain, view: view_context) @registrar = RegistrarPresenter.new(registrar: registrar, view: view_context) - @confirmation_url = confirmation_url(domain) + @confirmation_url = registrant_confirm_url(domain: domain, method: 'delete') subject = default_i18n_subject(domain_name: domain.name) mail(to: registrant.email, subject: subject) @@ -48,10 +48,6 @@ class DomainDeleteMailer < ApplicationMailer private - def confirmation_url(domain) - registrant_domain_delete_confirm_url(domain, token: domain.registrant_verification_token) - end - def forced_email_from ENV['action_mailer_force_delete_from'] || self.class.default[:from] end diff --git a/app/mailers/registrant_change_mailer.rb b/app/mailers/registrant_change_mailer.rb index ff3cfa18e..8f43f4ab5 100644 --- a/app/mailers/registrant_change_mailer.rb +++ b/app/mailers/registrant_change_mailer.rb @@ -5,7 +5,7 @@ class RegistrantChangeMailer < ApplicationMailer @domain = DomainPresenter.new(domain: domain, view: view_context) @registrar = RegistrarPresenter.new(registrar: registrar, view: view_context) @new_registrant = RegistrantPresenter.new(registrant: new_registrant, view: view_context) - @confirmation_url = confirmation_url(domain) + @confirmation_url = registrant_confirm_url(domain: domain, method: 'change') subject = default_i18n_subject(domain_name: domain.name) mail(to: current_registrant.email, subject: subject) @@ -49,10 +49,6 @@ class RegistrantChangeMailer < ApplicationMailer private - def confirmation_url(domain) - registrant_domain_update_confirm_url(domain, token: domain.registrant_verification_token) - end - def address_processing Contact.address_processing? end diff --git a/app/models/ability.rb b/app/models/ability.rb index dce8a515b..31637b8ea 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -109,6 +109,7 @@ class Ability can :destroy, :pending can :create, :zonefile can :access, :settings_menu + can :manage, BouncedMailAddress end def static_registrant diff --git a/app/models/actions/contact_create.rb b/app/models/actions/contact_create.rb new file mode 100644 index 000000000..22fabc7b9 --- /dev/null +++ b/app/models/actions/contact_create.rb @@ -0,0 +1,81 @@ +module Actions + class ContactCreate + attr_reader :contact, :legal_document, :ident + + def initialize(contact, legal_document, ident) + @contact = contact + @legal_document = legal_document + @ident = ident + end + + def call + maybe_remove_address + maybe_attach_legal_doc + validate_ident + commit + end + + def maybe_remove_address + return if Contact.address_processing? + + contact.city = nil + contact.zip = nil + contact.street = nil + contact.state = nil + contact.country_code = nil + end + + def validate_ident + validate_ident_integrity + validate_ident_birthday + + identifier = ::Contact::Ident.new(code: ident[:ident], type: ident[:ident_type], + country_code: ident[:ident_country_code]) + + identifier.validate + contact.identifier = identifier + end + + def validate_ident_integrity + return if ident.blank? + + if ident[:ident_type].blank? + contact.add_epp_error('2003', nil, 'ident_type', + I18n.t('errors.messages.required_ident_attribute_missing')) + @error = true + elsif !%w[priv org birthday].include?(ident[:ident_type]) + contact.add_epp_error('2003', nil, 'ident_type', 'Invalid ident type') + @error = true + end + end + + def validate_ident_birthday + return if ident.blank? + return unless ident[:ident_type] != 'birthday' && ident[:ident_country_code].blank? + + contact.add_epp_error('2003', nil, 'ident_country_code', + I18n.t('errors.messages.required_ident_attribute_missing')) + @error = true + end + + def maybe_attach_legal_doc + return unless legal_document + + doc = LegalDocument.create( + documentable_type: Contact, + document_type: legal_document[:type], body: legal_document[:body] + ) + + contact.legal_documents = [doc] + contact.legal_document_id = doc.id + end + + def commit + contact.id = nil # new record + return false if @error + + contact.generate_code + contact.save + end + end +end diff --git a/app/models/actions/contact_delete.rb b/app/models/actions/contact_delete.rb new file mode 100644 index 000000000..59032d566 --- /dev/null +++ b/app/models/actions/contact_delete.rb @@ -0,0 +1,41 @@ +module Actions + class ContactDelete + attr_reader :contact + attr_reader :new_attributes + attr_reader :legal_document + attr_reader :ident + attr_reader :user + + def initialize(contact, legal_document = nil) + @legal_document = legal_document + @contact = contact + end + + def call + maybe_attach_legal_doc + + if contact.linked? + contact.errors.add(:domains, :exist) + return + end + + commit + end + + def maybe_attach_legal_doc + return unless legal_document + + document = contact.legal_documents.create( + document_type: legal_document[:type], + body: legal_document[:body] + ) + + contact.legal_document_id = document.id + contact.save + end + + def commit + contact.destroy + end + end +end diff --git a/app/models/actions/contact_update.rb b/app/models/actions/contact_update.rb index f8b39ecb4..7ca7b6b04 100644 --- a/app/models/actions/contact_update.rb +++ b/app/models/actions/contact_update.rb @@ -17,7 +17,7 @@ module Actions def call maybe_remove_address maybe_update_statuses - maybe_update_ident + maybe_update_ident if ident.present? maybe_attach_legal_doc commit end @@ -53,7 +53,11 @@ module Actions end def maybe_update_ident - return unless ident[:ident] + unless ident.is_a?(Hash) + contact.add_epp_error('2308', nil, nil, I18n.t('epp.contacts.errors.valid_ident')) + @error = true + return + end if contact.identifier.valid? submitted_ident = ::Contact::Ident.new(code: ident[:ident], diff --git a/app/models/actions/domain_transfer.rb b/app/models/actions/domain_transfer.rb new file mode 100644 index 000000000..1ff9aafe9 --- /dev/null +++ b/app/models/actions/domain_transfer.rb @@ -0,0 +1,74 @@ +module Actions + class DomainTransfer + attr_reader :domain + attr_reader :transfer_code + attr_reader :legal_document + attr_reader :ident + attr_reader :user + + def initialize(domain, transfer_code, user) + @domain = domain + @transfer_code = transfer_code + @user = user + end + + def call + return unless domain_exists? + return unless valid_transfer_code? + + run_validations + + # return domain.pending_transfer if domain.pending_transfer + # attach_legal_document(::Deserializers::Xml::LegalDocument.new(frame).call) + + return if domain.errors[:epp_errors].any? + + commit + end + + def domain_exists? + return true if domain.persisted? + + domain.add_epp_error('2303', nil, nil, 'Object does not exist') + + false + end + + def run_validations + validate_registrar + validate_eligilibty + validate_not_discarded + end + + def valid_transfer_code? + return true if transfer_code == domain.transfer_code + + domain.add_epp_error('2202', nil, nil, 'Invalid authorization information') + false + end + + def validate_registrar + return unless user == domain.registrar + + domain.add_epp_error('2002', nil, nil, + I18n.t(:domain_already_belongs_to_the_querying_registrar)) + end + + def validate_eligilibty + return unless domain.non_transferable? + + domain.add_epp_error('2304', nil, nil, 'Object status prohibits operation') + end + + def validate_not_discarded + return unless domain.discarded? + + domain.add_epp_error('2106', nil, nil, 'Object is not eligible for transfer') + end + + def commit + bare_domain = Domain.find(domain.id) + ::DomainTransfer.request(bare_domain, user) + end + end +end diff --git a/app/models/bounced_mail_address.rb b/app/models/bounced_mail_address.rb new file mode 100644 index 000000000..73c6a0941 --- /dev/null +++ b/app/models/bounced_mail_address.rb @@ -0,0 +1,28 @@ +class BouncedMailAddress < ApplicationRecord + validates :email, :message_id, :bounce_type, :bounce_subtype, :action, :status, presence: true + + def bounce_reason + "#{action} (#{status} #{diagnostic})" + end + + def self.record(json) + bounced_records = json['bounce']['bouncedRecipients'] + bounced_records.each do |record| + bounce_record = BouncedMailAddress.new(params_from_json(json, record)) + + bounce_record.save + end + end + + def self.params_from_json(json, bounced_record) + { + email: bounced_record['emailAddress'], + message_id: json['mail']['messageId'], + bounce_type: json['bounce']['bounceType'], + bounce_subtype: json['bounce']['bounceSubType'], + action: bounced_record['action'], + status: bounced_record['status'], + diagnostic: bounced_record['diagnosticCode'], + } + end +end diff --git a/app/models/concerns/domain/deletable.rb b/app/models/concerns/domain/deletable.rb index bd01ab1c9..81518c739 100644 --- a/app/models/concerns/domain/deletable.rb +++ b/app/models/concerns/domain/deletable.rb @@ -20,6 +20,6 @@ module Concerns::Domain::Deletable end def deletion_deadline - delete_date + 24.hours + (delete_date || Time.zone.now) + 24.hours end -end \ No newline at end of file +end diff --git a/app/models/concerns/domain/force_delete.rb b/app/models/concerns/domain/force_delete.rb index f3bf96975..e06da25cc 100644 --- a/app/models/concerns/domain/force_delete.rb +++ b/app/models/concerns/domain/force_delete.rb @@ -33,70 +33,14 @@ module Concerns::Domain::ForceDelete # rubocop:disable Metrics/ModuleLength statuses.include?(DomainStatus::FORCE_DELETE) end - def should_notify_on_soft_force_delete? - force_delete_scheduled? && contact_notification_sent_date.blank? && - force_delete_start.to_date <= Time.zone.now.to_date && force_delete_type.to_sym == :soft && - !statuses.include?(DomainStatus::CLIENT_HOLD) - end - - def client_holdable? - force_delete_scheduled? && !statuses.include?(DomainStatus::CLIENT_HOLD) && - force_delete_start.present? && force_delete_lte_today && force_delete_lte_valid_date - end - - def force_delete_lte_today - force_delete_start + Setting.expire_warning_period.days <= Time.zone.now - end - - def force_delete_lte_valid_date - force_delete_start + Setting.expire_warning_period.days <= valid_to - end - - def schedule_force_delete(type: :fast_track) - if discarded? - raise StandardError, 'Force delete procedure cannot be scheduled while a domain is discarded' - end - - type == :fast_track ? force_delete_fast_track : force_delete_soft - end - - def add_force_delete_type(force_delete_type) - self.force_delete_type = force_delete_type - end - - def force_delete_fast_track - preserve_current_statuses_for_force_delete - add_force_delete_statuses - add_force_delete_type(:fast) - self.force_delete_date = force_delete_fast_track_start_date + 1.day - self.force_delete_start = Time.zone.today + 1.day - stop_all_pending_actions - allow_deletion - save(validate: false) - end - - def force_delete_soft - preserve_current_statuses_for_force_delete - add_force_delete_statuses - add_force_delete_type(:soft) - calculate_soft_delete_date - stop_all_pending_actions - allow_deletion - save(validate: false) - end - - def clear_force_delete_data - self.force_delete_data = nil + def schedule_force_delete(type: :fast_track, notify_by_email: false) + Domains::ForceDelete::SetForceDelete.run(domain: self, + type: type, + notify_by_email: notify_by_email) end def cancel_force_delete - remove_force_delete_statuses - restore_statuses_before_force_delete - clear_force_delete_data - self.force_delete_date = nil - self.force_delete_start = nil - save(validate: false) - registrar.notifications.create!(text: I18n.t('force_delete_cancelled', domain_name: name)) + Domains::CancelForceDelete::CancelForceDelete.run(domain: self) end def outzone_date @@ -107,55 +51,4 @@ module Concerns::Domain::ForceDelete # rubocop:disable Metrics/ModuleLength (force_delete_date&.beginning_of_day || valid_to + Setting.expire_warning_period.days + Setting.redemption_grace_period.days) end - - private - - def calculate_soft_delete_date - years = (valid_to.to_date - Time.zone.today).to_i / 365 - soft_delete_dates(years) if years.positive? - end - - def soft_delete_dates(years) - self.force_delete_start = valid_to - years.years - self.force_delete_date = force_delete_start + Setting.expire_warning_period.days + - Setting.redemption_grace_period.days - end - - def stop_all_pending_actions - statuses.delete(DomainStatus::PENDING_UPDATE) - statuses.delete(DomainStatus::PENDING_TRANSFER) - statuses.delete(DomainStatus::PENDING_RENEW) - statuses.delete(DomainStatus::PENDING_CREATE) - end - - def preserve_current_statuses_for_force_delete - update(statuses_before_force_delete: statuses) - end - - def restore_statuses_before_force_delete - self.statuses = statuses_before_force_delete - self.statuses_before_force_delete = nil - end - - def add_force_delete_statuses - self.statuses |= [DomainStatus::FORCE_DELETE, - DomainStatus::SERVER_RENEW_PROHIBITED, - DomainStatus::SERVER_TRANSFER_PROHIBITED] - end - - def remove_force_delete_statuses - statuses.delete(DomainStatus::FORCE_DELETE) - statuses.delete(DomainStatus::SERVER_RENEW_PROHIBITED) - statuses.delete(DomainStatus::SERVER_TRANSFER_PROHIBITED) - statuses.delete(DomainStatus::CLIENT_HOLD) - end - - def allow_deletion - statuses.delete(DomainStatus::CLIENT_DELETE_PROHIBITED) - statuses.delete(DomainStatus::SERVER_DELETE_PROHIBITED) - end - - def force_delete_fast_track_start_date - Time.zone.today + Setting.expire_warning_period.days + Setting.redemption_grace_period.days - end end diff --git a/app/models/concerns/job/force_delete.rb b/app/models/concerns/job/force_delete.rb deleted file mode 100644 index 316612d1e..000000000 --- a/app/models/concerns/job/force_delete.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Concerns - module Job - module ForceDelete - extend ActiveSupport::Concern - - class_methods do - def start_client_hold - log_prepare_client_hold - - ::PaperTrail.request.whodunnit = "cron - #{__method__}" - - ::Domain.force_delete_scheduled.each do |domain| - proceed_client_hold(domain: domain) - end - - log_end_end_force_delete_job - end - - def proceed_client_hold(domain:) - notify_on_grace_period(domain) if domain.should_notify_on_soft_force_delete? - return unless domain.client_holdable? - - domain.statuses << DomainStatus::CLIENT_HOLD - log_start_client_hold(domain) - - domain.save(validate: false) - notify_client_hold(domain) - - log_end_end_client_hold(domain) - end - end - end - end -end diff --git a/app/models/concerns/job/force_delete_logging.rb b/app/models/concerns/job/force_delete_logging.rb deleted file mode 100644 index 8f6ee227c..000000000 --- a/app/models/concerns/job/force_delete_logging.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Concerns - module Job - module ForceDeleteLogging - extend ActiveSupport::Concern - - class_methods do - def log_prepare_client_hold - return if Rails.env.test? - - STDOUT << "#{Time.zone.now.utc} - Setting client_hold to domains\n" - end - - def log_start_client_hold(domain) - return if Rails.env.test? - - STDOUT << "#{Time.zone.now.utc} DomainCron.start_client_hold: ##{domain.id} "\ - "(#{domain.name}) #{domain.changes}\n" - end - - def log_end_end_client_hold(domain) - return if Rails.env.test? - - STDOUT << "#{Time.zone.now.utc} - Successfully set client_hold on (#{domain.name})" - end - - def log_end_end_force_delete_job - return if Rails.env.test? - - STDOUT << "#{Time.zone.now.utc} - All client_hold setting are done\n" - end - end - end - end -end diff --git a/app/models/concerns/job/force_delete_notify.rb b/app/models/concerns/job/force_delete_notify.rb deleted file mode 100644 index bc291354e..000000000 --- a/app/models/concerns/job/force_delete_notify.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Concerns - module Job - module ForceDeleteNotify - extend ActiveSupport::Concern - - class_methods do - def notify_client_hold(domain) - domain.registrar.notifications.create!(text: I18n.t('force_delete_set_on_domain', - domain_name: domain.name, - outzone_date: domain.outzone_date, - purge_date: domain.purge_date)) - end - - def notify_on_grace_period(domain) - domain.registrar.notifications.create!(text: I18n.t('grace_period_started_domain', - domain_name: domain.name, - date: domain.force_delete_start)) - send_mail(domain) if domain.template_name.present? - domain.update(contact_notification_sent_date: Time.zone.today) - end - - def send_mail(domain) - DomainDeleteMailer.forced(domain: domain, - registrar: domain.registrar, - registrant: domain.registrant, - template_name: domain.template_name).deliver_now - end - end - end - end -end diff --git a/app/models/contact.rb b/app/models/contact.rb index 9dc1e34a2..e30312b4a 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -333,31 +333,6 @@ class Contact < ApplicationRecord Country.new(country_code) end - # TODO: refactor, it should not allow to destroy with normal destroy, - # no need separate method - # should use only in transaction - def destroy_and_clean frame - if linked? - errors.add(:domains, :exist) - return false - end - - legal_document_data = ::Deserializers::Xml::LegalDocument.new(frame).call - - if legal_document_data - - doc = LegalDocument.create( - documentable_type: Contact, - document_type: legal_document_data[:type], - body: legal_document_data[:body] - ) - self.legal_documents = [doc] - self.legal_document_id = doc.id - self.save - end - destroy - end - def to_upcase_country_code self.ident_country_code = ident_country_code.upcase if ident_country_code self.country_code = country_code.upcase if country_code @@ -372,19 +347,24 @@ class Contact < ApplicationRecord @desc = {} registrant_domains.each do |dom| - @desc[dom.name] ||= [] - @desc[dom.name] << :registrant + @desc[dom.name] ||= { id: dom.uuid, roles: [] } + @desc[dom.name][:roles] << :registrant end domain_contacts.each do |dc| - @desc[dc.domain.name] ||= [] - @desc[dc.domain.name] << dc.name.downcase.to_sym + @desc[dc.domain.name] ||= { id: dc.domain.uuid, roles: [] } + @desc[dc.domain.name][:roles] << dc.name.downcase.to_sym @desc[dc.domain.name] = @desc[dc.domain.name].compact end @desc end + def related_domains + a = related_domain_descriptions + a.keys.map { |d| { name: d, id: a[d][:id], roles: a[d][:roles] } } + end + def status_notes_array=(notes) self.status_notes = {} notes ||= [] diff --git a/app/models/domain.rb b/app/models/domain.rb index b15bb7c55..49f18d9db 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -327,6 +327,7 @@ class Domain < ApplicationRecord end def notify_registrar(message_key) + # TODO: To be deleted with DomainDeleteConfirm refactoring registrar.notifications.create!( text: "#{I18n.t(message_key)}: #{name}", attached_obj_id: id, @@ -335,11 +336,13 @@ class Domain < ApplicationRecord end def preclean_pendings + # TODO: To be deleted with refactoring self.registrant_verification_token = nil self.registrant_verification_asked_at = nil end def clean_pendings! + # TODO: To be deleted with refactoring preclean_pendings self.pending_json = {} statuses.delete(DomainStatus::PENDING_DELETE_CONFIRMATION) @@ -418,7 +421,7 @@ class Domain < ApplicationRecord pending_delete_confirmation! save(validate: false) # should check if this did succeed - DomainDeleteConfirmEmailJob.enqueue(id) + Domains::DeleteConfirmEmail::SendRequest.run(domain: self) end def cancel_pending_delete @@ -482,12 +485,6 @@ class Domain < ApplicationRecord Registrant.find_by(id: pending_json['new_registrant_id']) end - def set_graceful_expired - self.outzone_at = expire_time + self.class.expire_warning_period - self.delete_date = outzone_at + self.class.redemption_grace_period - self.statuses |= [DomainStatus::EXPIRED] - end - def pending_update? statuses.include?(DomainStatus::PENDING_UPDATE) end diff --git a/app/models/domain_cron.rb b/app/models/domain_cron.rb index ad64456ca..e936c29e6 100644 --- a/app/models/domain_cron.rb +++ b/app/models/domain_cron.rb @@ -1,84 +1,17 @@ class DomainCron - include Concerns::Job::ForceDelete - include Concerns::Job::ForceDeleteLogging - include Concerns::Job::ForceDeleteNotify - def self.clean_expired_pendings - STDOUT << "#{Time.zone.now.utc} - Clean expired domain pendings\n" unless Rails.env.test? - - ::PaperTrail.request.whodunnit = "cron - #{__method__}" - expire_at = Setting.expire_pending_confirmation.hours.ago - count = 0 - expired_pending_domains = Domain.where('registrant_verification_asked_at <= ?', expire_at) - expired_pending_domains.each do |domain| - unless domain.pending_update? || domain.pending_delete? || domain.pending_delete_confirmation? - msg = "#{Time.zone.now.utc} - ISSUE: DOMAIN #{domain.id}: #{domain.name} IS IN EXPIRED PENDING LIST, " \ - "but no pendingDelete/pendingUpdate state present!\n" - STDOUT << msg unless Rails.env.test? - next - end - count += 1 - if domain.pending_update? - RegistrantChangeExpiredEmailJob.enqueue(domain.id) - end - if domain.pending_delete? || domain.pending_delete_confirmation? - DomainDeleteMailer.expired(domain).deliver_now - end - - domain.preclean_pendings - domain.clean_pendings! - - unless Rails.env.test? - STDOUT << "#{Time.zone.now.utc} DomainCron.clean_expired_pendings: ##{domain.id} (#{domain.name})\n" - end - UpdateWhoisRecordJob.enqueue domain.name, 'domain' - end - STDOUT << "#{Time.zone.now.utc} - Successfully cancelled #{count} domain pendings\n" unless Rails.env.test? - count + Domains::ExpiredPendings::CleanAll.run! end def self.start_expire_period - ::PaperTrail.request.whodunnit = "cron - #{__method__}" - domains = Domain.expired - marked = 0 - real = 0 - - domains.each do |domain| - next unless domain.expirable? - real += 1 - domain.set_graceful_expired - STDOUT << "#{Time.zone.now.utc} DomainCron.start_expire_period: ##{domain.id} (#{domain.name}) #{domain.changes}\n" unless Rails.env.test? - - send_time = domain.valid_to + Setting.expiration_reminder_mail.to_i.days - saved = domain.save(validate: false) - - if saved - DomainExpireEmailJob.enqueue(domain.id, run_at: send_time) - marked += 1 - end - end - - STDOUT << "#{Time.zone.now.utc} - Successfully expired #{marked} of #{real} domains\n" unless Rails.env.test? + Domains::ExpirePeriod::Start.run! end def self.start_redemption_grace_period - STDOUT << "#{Time.zone.now.utc} - Setting server_hold to domains\n" unless Rails.env.test? + Domains::RedemptionGracePeriod::Start.run! + end - ::PaperTrail.request.whodunnit = "cron - #{__method__}" - - domains = Domain.outzone_candidates - marked = 0 - real = 0 - - domains.each do |domain| - next unless domain.server_holdable? - real += 1 - domain.statuses << DomainStatus::SERVER_HOLD - STDOUT << "#{Time.zone.now.utc} DomainCron.start_redemption_grace_period: ##{domain.id} (#{domain.name}) #{domain.changes}\n" unless Rails.env.test? - domain.save(validate: false) and marked += 1 - end - - STDOUT << "#{Time.zone.now.utc} - Successfully set server_hold to #{marked} of #{real} domains\n" unless Rails.env.test? - marked + def self.start_client_hold + Domains::ClientHold::SetClientHold.run! end end diff --git a/app/models/epp/contact.rb b/app/models/epp/contact.rb index 6867b037d..50ebac065 100644 --- a/app/models/epp/contact.rb +++ b/app/models/epp/contact.rb @@ -30,12 +30,13 @@ class Epp::Contact < Contact at end - def new(frame, registrar) + def new(frame, registrar, epp: true) return super if frame.blank? + attrs = epp ? attrs_from(frame, new_record: true) : frame super( - attrs_from(frame, new_record: true).merge( - code: frame.css('id').text, + attrs.merge( + code: epp ? frame.css('id').text : frame[:id], registrar: registrar ) ) diff --git a/app/models/epp/domain.rb b/app/models/epp/domain.rb index c46732712..d8f5f2bb9 100644 --- a/app/models/epp/domain.rb +++ b/app/models/epp/domain.rb @@ -417,7 +417,7 @@ class Epp::Domain < Domain if statuses.include?(x) to_destroy << x else - add_epp_error('2303', 'status', x, [:domain_statuses, :not_found]) + add_epp_error('2303', 'status', x, %i[statuses not_found]) end end @@ -432,7 +432,7 @@ class Epp::Domain < Domain frame.css('status').each do |x| unless DomainStatus::CLIENT_STATUSES.include?(x['s']) - add_epp_error('2303', 'status', x['s'], [:domain_statuses, :not_found]) + add_epp_error('2303', 'status', x['s'], %i[statuses not_found]) next end @@ -508,25 +508,6 @@ class Epp::Domain < Domain errors.empty? && super(at) end - def apply_pending_update! - preclean_pendings - user = ApiUser.find(pending_json['current_user_id']) - frame = Nokogiri::XML(pending_json['frame']) - - self.statuses.delete(DomainStatus::PENDING_UPDATE) - self.upid = user.registrar.id if user.registrar - self.up_date = Time.zone.now - - return unless update(frame, user, false) - clean_pendings! - - save! - - WhoisRecord.find_by(domain_id: id).save # need to reload model - - true - end - def apply_pending_delete! preclean_pendings statuses.delete(DomainStatus::PENDING_DELETE_CONFIRMATION) diff --git a/app/models/registrant_verification.rb b/app/models/registrant_verification.rb index 097f0cfa9..4e8e6afe9 100644 --- a/app/models/registrant_verification.rb +++ b/app/models/registrant_verification.rb @@ -18,24 +18,24 @@ class RegistrantVerification < ApplicationRecord def domain_registrant_change_confirm!(initiator) self.action_type = DOMAIN_REGISTRANT_CHANGE self.action = CONFIRMED - DomainUpdateConfirmJob.enqueue domain.id, CONFIRMED, initiator if save + DomainUpdateConfirmJob.perform_later domain.id, CONFIRMED, initiator if save end def domain_registrant_change_reject!(initiator) self.action_type = DOMAIN_REGISTRANT_CHANGE self.action = REJECTED - DomainUpdateConfirmJob.run domain.id, REJECTED, initiator if save + DomainUpdateConfirmJob.perform_later domain.id, REJECTED, initiator if save end def domain_registrant_delete_confirm!(initiator) self.action_type = DOMAIN_DELETE self.action = CONFIRMED - DomainDeleteConfirmJob.enqueue domain.id, CONFIRMED, initiator if save + DomainDeleteConfirmJob.perform_later domain.id, CONFIRMED, initiator if save end def domain_registrant_delete_reject!(initiator) self.action_type = DOMAIN_DELETE self.action = REJECTED - DomainDeleteConfirmJob.enqueue domain.id, REJECTED, initiator if save + DomainDeleteConfirmJob.perform_later domain.id, REJECTED, initiator if save end end diff --git a/app/models/registrar.rb b/app/models/registrar.rb index 3b5eb3502..c1168a887 100644 --- a/app/models/registrar.rb +++ b/app/models/registrar.rb @@ -137,7 +137,8 @@ class Registrar < ApplicationRecord def api_ip_white?(ip) return true unless Setting.api_ip_whitelist_enabled - white_ips.api.pluck(:ipv4, :ipv6).flatten.include?(ip) + + white_ips.api.include_ip?(ip) end # Audit log is needed, therefore no raw SQL diff --git a/app/models/repp_api.rb b/app/models/repp_api.rb new file mode 100644 index 000000000..a752a15af --- /dev/null +++ b/app/models/repp_api.rb @@ -0,0 +1,33 @@ +class ReppApi + def self.bulk_renew(domains, period, registrar) + payload = { domains: domains, renew_period: period } + uri = URI.parse("#{ENV['repp_url']}domains/renew/bulk") + req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') + req.body = payload.to_json + + ReppApi.request(req, uri, registrar: registrar).body + end + + def self.request(request, uri, registrar:) + request.basic_auth(registrar.username, registrar.plain_text_password) if registrar + client_cert = Rails.env.test? ? nil : File.read(ENV['cert_path']) + client_key = Rails.env.test? ? nil : File.read(ENV['key_path']) + params = ReppApi.compose_ca_auth_params(uri, client_cert, client_key) + + Net::HTTP.start(uri.hostname, uri.port, params) do |http| + http.request(request) + end + end + + def self.compose_ca_auth_params(uri, client_cert, client_key) + params = { use_ssl: (uri.scheme == 'https') } + params[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if Rails.env.test? || Rails.env.development? + + unless Rails.env.test? + params[:cert] = OpenSSL::X509::Certificate.new(client_cert) + params[:key] = OpenSSL::PKey::RSA.new(client_key) + end + + params + end +end diff --git a/app/models/tech_domain_contact.rb b/app/models/tech_domain_contact.rb index 04f36c4e4..92799061c 100644 --- a/app/models/tech_domain_contact.rb +++ b/app/models/tech_domain_contact.rb @@ -5,19 +5,20 @@ class TechDomainContact < DomainContact skipped_domains = [] tech_contacts = where(contact: current_contact) - transaction do - tech_contacts.each do |tech_contact| - if tech_contact.domain.discarded? - skipped_domains << tech_contact.domain.name - next - end - + tech_contacts.each do |tech_contact| + if tech_contact.domain.discarded? + skipped_domains << tech_contact.domain.name + next + end + begin tech_contact.contact = new_contact tech_contact.save! affected_domains << tech_contact.domain.name + rescue ActiveRecord::RecordNotUnique + skipped_domains << tech_contact.domain.name end end - return affected_domains.sort, skipped_domains.sort + [affected_domains.sort, skipped_domains.sort] end end diff --git a/app/models/white_ip.rb b/app/models/white_ip.rb index 303ff5886..417633b12 100644 --- a/app/models/white_ip.rb +++ b/app/models/white_ip.rb @@ -2,8 +2,8 @@ class WhiteIp < ApplicationRecord include Versions belongs_to :registrar - validates :ipv4, format: { with: /\A(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\z/, allow_blank: true } - validates :ipv6, format: { with: /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/, allow_blank: true } + validate :valid_ipv4? + validate :valid_ipv6? validate :validate_ipv4_and_ipv6 def validate_ipv4_and_ipv6 @@ -11,6 +11,22 @@ class WhiteIp < ApplicationRecord errors.add(:base, I18n.t(:ipv4_or_ipv6_must_be_present)) end + def valid_ipv4? + return if ipv4.blank? + + IPAddr.new(ipv4, Socket::AF_INET) + rescue StandardError => _e + errors.add(:ipv4, :invalid) + end + + def valid_ipv6? + return if ipv6.blank? + + IPAddr.new(ipv6, Socket::AF_INET6) + rescue StandardError => _e + errors.add(:ipv6, :invalid) + end + API = 'api' REGISTRAR = 'registrar' INTERFACES = [API, REGISTRAR] @@ -23,8 +39,37 @@ class WhiteIp < ApplicationRecord end class << self + # rubocop:disable Style/CaseEquality + # rubocop:disable Metrics/AbcSize def include_ip?(ip) - where('ipv4 = :ip OR ipv6 = :ip', ip: ip).any? + return false if ip.blank? + + where(id: ids_including(ip)).any? + end + + def ids_including(ip) + ipv4 = ipv6 = [] + if check_ip4(ip).present? + ipv4 = select { |white_ip| IPAddr.new(white_ip.ipv4, Socket::AF_INET) === check_ip4(ip) } + end + if check_ip6(ip).present? + ipv6 = select { |white_ip| IPAddr.new(white_ip.ipv6, Socket::AF_INET6) === check_ip6(ip) } + end + (ipv4 + ipv6).pluck(:id).flatten.uniq + end + # rubocop:enable Style/CaseEquality + # rubocop:enable Metrics/AbcSize + + def check_ip4(ip) + IPAddr.new(ip, Socket::AF_INET) + rescue StandardError => _e + nil + end + + def check_ip6(ip) + IPAddr.new(ip, Socket::AF_INET6) + rescue StandardError => _e + nil end end end diff --git a/app/services/registrant_change.rb b/app/services/registrant_change.rb index 35b631fb6..fdee7654a 100644 --- a/app/services/registrant_change.rb +++ b/app/services/registrant_change.rb @@ -5,6 +5,7 @@ class RegistrantChange end def confirm + Dispute.close_by_domain(@domain.name) if @domain.disputed? notify_registrant end diff --git a/app/views/admin/base/_menu.haml b/app/views/admin/base/_menu.haml index a327419fd..5853bd3e6 100644 --- a/app/views/admin/base/_menu.haml +++ b/app/views/admin/base/_menu.haml @@ -33,6 +33,7 @@ %li= link_to t('.blocked_domains'), admin_blocked_domains_path %li= link_to t('.reserved_domains'), admin_reserved_domains_path %li= link_to t('.disputed_domains'), admin_disputes_path + %li= link_to t('.bounced_email_addresses'), admin_bounced_mail_addresses_path %li= link_to t('.epp_log'), admin_epp_logs_path(created_after: 'today') %li= link_to t('.repp_log'), admin_repp_logs_path(created_after: 'today') %li= link_to t('.que'), '/admin/que' diff --git a/app/views/admin/bounced_mail_addresses/index.html.erb b/app/views/admin/bounced_mail_addresses/index.html.erb new file mode 100644 index 000000000..913cbd19d --- /dev/null +++ b/app/views/admin/bounced_mail_addresses/index.html.erb @@ -0,0 +1,36 @@ +

Bounced Mail Addresses

+ +
+
+
+
+ + + + + + + + + + + + + + <% @bounced_mail_addresses.each do |mail_addr| %> + + + + + + + + + + <% end %> + +
EmailActionStatusDiagnosticTrackedActions
<%= mail_addr.email %><%= mail_addr.action %><%= mail_addr.status %><%= mail_addr.diagnostic %><%= mail_addr.created_at %><%= link_to 'Detailed', admin_bounced_mail_address_path(mail_addr) %><%= link_to 'Destroy', admin_bounced_mail_address_path(mail_addr), method: :delete, data: { confirm: 'Are you sure?' } %>
+
+
+
+
diff --git a/app/views/admin/bounced_mail_addresses/show.html.erb b/app/views/admin/bounced_mail_addresses/show.html.erb new file mode 100644 index 000000000..5183ae5a1 --- /dev/null +++ b/app/views/admin/bounced_mail_addresses/show.html.erb @@ -0,0 +1,27 @@ +

+ Email: + <%= @bounced_mail_address.email %> +

+ +

+ Bounced message ID: + <%= @bounced_mail_address.message_id %> +

+ +

+ Overall bounce type: + <%= @bounced_mail_address.bounce_type %> (<%= @bounced_mail_address.bounce_subtype %> ) +

+ +

+ Bounced recipient status: + <%= @bounced_mail_address.action %> (<%= @bounced_mail_address.status %>) +

+ +

+ Bounced recipient diagnostic: +

<%= @bounced_mail_address.diagnostic %>
+

+ +<%= link_to 'Back', admin_bounced_mail_addresses_path %> +<%= link_to 'Destroy', admin_bounced_mail_address_path(@bounced_mail_address), method: :delete, data: { confirm: 'Are you sure?' } %> diff --git a/app/views/epp/contacts/info.xml.builder b/app/views/epp/contacts/info.xml.builder index 1945e7def..6ce0a17f4 100644 --- a/app/views/epp/contacts/info.xml.builder +++ b/app/views/epp/contacts/info.xml.builder @@ -14,11 +14,15 @@ xml.epp_head do end xml.tag!('contact:postalInfo', type: 'int') do - xml.tag!('contact:name', @contact.name) + if can? :view_full_info, @contact, @password + xml.tag!('contact:name', @contact.name) + else + xml.tag!('contact:name', 'No access') + end if can? :view_full_info, @contact, @password xml.tag!('contact:org', @contact.org_name) if @contact.org_name.present? - if address_processing? + if Contact.address_processing? xml.tag!('contact:addr') do xml.tag!('contact:street', @contact.street) xml.tag!('contact:city', @contact.city) @@ -31,7 +35,7 @@ xml.epp_head do else xml.tag!('contact:org', 'No access') - if address_processing? + if Contact.address_processing? xml.tag!('contact:addr') do xml.tag!('contact:street', 'No access') xml.tag!('contact:city', 'No access') diff --git a/app/views/mailers/domain_expire_mailer/expired_soft.html.erb b/app/views/mailers/domain_expire_mailer/expired_soft.html.erb index 1dd661a38..0bcfc6acd 100644 --- a/app/views/mailers/domain_expire_mailer/expired_soft.html.erb +++ b/app/views/mailers/domain_expire_mailer/expired_soft.html.erb @@ -2,7 +2,7 @@

Lugupeetud .ee domeeni registreerija/halduskontakt

-

Domeeninimi <%= @domain.name %> on aegunud ja ei ole alates <%= @domain.on_hold_date %> internetis kättesaadav. Domeeniga on seotud puudulike kontakti objekte, milles tulenevalt on Eesti Interneti SA blokeerinud domeeni pikendamise ja registripidaja vahetuse, kuniks kontaktandmed korrastatakse. Andmete korrastamiseks ja registreeringu pikendamiseks pöörduge palun oma registripidaja poole.

+

Domeeninimi <%= @domain.name %> on aegunud ja ei ole alates <%= @domain.on_hold_date %> internetis kättesaadav. Domeeniga on seotud puudulikke kontakti objekte, millest tulenevalt on Eesti Interneti SA blokeerinud domeeni pikendamise ja registripidaja vahetuse, kuniks kontaktandmed korrastatakse. Andmete korrastamiseks ja registreeringu pikendamiseks pöörduge palun oma registripidaja poole.

<%= @domain.name %> pikendamata jätmisel domeen kustub ja läheb <%= @domain.delete_date %> oksjonile .ee oksjonikeskkonda. Domeenioksjonite kohta loe lähemalt siit.

diff --git a/app/views/mailers/domain_expire_mailer/expired_soft.text.erb b/app/views/mailers/domain_expire_mailer/expired_soft.text.erb index 0e6d9c953..7be32f73b 100644 --- a/app/views/mailers/domain_expire_mailer/expired_soft.text.erb +++ b/app/views/mailers/domain_expire_mailer/expired_soft.text.erb @@ -2,7 +2,7 @@ Domeen <%= @domain.name %> on aegunud ning suunatud kustutusmenetlusse kuna olem Lugupeetud .ee domeeni registreerija/halduskontakt -Domeeninimi <%= @domain.name %> on aegunud ja ei ole alates <%= @domain.on_hold_date %> internetis kättesaadav. Domeeniga on seotud puudulike kontakti objekte, milles tulenevalt on Eesti Interneti SA blokeerinud domeeni pikendamise ja registripidaja vahetuse, kuniks kontaktandmed korrastatakse. Andmete korrastamiseks ja registreeringu pikendamiseks pöörduge palun oma registripidaja poole. +Domeeninimi <%= @domain.name %> on aegunud ja ei ole alates <%= @domain.on_hold_date %> internetis kättesaadav. Domeeniga on seotud puudulikke kontakti objekte, millest tulenevalt on Eesti Interneti SA blokeerinud domeeni pikendamise ja registripidaja vahetuse, kuniks kontaktandmed korrastatakse. Andmete korrastamiseks ja registreeringu pikendamiseks pöörduge palun oma registripidaja poole. <%= @domain.name %> pikendamata jätmisel domeen kustub ja läheb <%= @domain.delete_date %> oksjonile .ee oksjonikeskkonda. Domeenioksjonite kohta loe lähemalt siit https://www.internet.ee/domeenioksjonid. diff --git a/app/views/registrar/bulk_change/_api_errors.html.erb b/app/views/registrar/bulk_change/_api_errors.html.erb new file mode 100644 index 000000000..8d8862959 --- /dev/null +++ b/app/views/registrar/bulk_change/_api_errors.html.erb @@ -0,0 +1,13 @@ +<% if @api_errors %> +
+ +
+<% end %> diff --git a/app/views/registrar/bulk_change/_bulk_renew_form.html.erb b/app/views/registrar/bulk_change/_bulk_renew_form.html.erb new file mode 100644 index 000000000..5db40365f --- /dev/null +++ b/app/views/registrar/bulk_change/_bulk_renew_form.html.erb @@ -0,0 +1,57 @@ +<%= form_with url: registrar_bulk_renew_path, multipart: true, class: 'form-horizontal' do |f|%> + <%= render 'api_errors' %> + +
+
+ <%= f.label :current_balance, t('.current_balance') %> +
+
+ <%= number_to_currency current_registrar_user.registrar.balance %> +
+
+ + +
+
+ <%= f.label :expire_date, t('.expire_date') %> +
+
+ <%= f.text_field :expire_date, value: @expire_date, + class: 'form-control js-datepicker'%> +
+
+ +
+
+ <%= f.label :period, t('.period'), class: 'required' %> +
+
+ <%= select_tag 'period', + options_for_select(Depp::Domain::PERIODS, @period), { class: 'form-control' } %> +
+
+ + <% if @domains.present? %> +
+
+ <%= f.label :domain_ids, t('.domain_ids') %> +
+
+ <%= f.collection_check_boxes :domain_ids, @domains, :name, :name, + checked: @domains.map(&:name) do |b|%> +
+ <%= b.check_box %> + <%= b.label %> +
+ <% end %> +
+
+ <% end %> + +
+ <%= f.submit "#{t '.filter_btn'}", name: 'filter', class: 'btn btn-warning' %> + <% if @domains.present? %> + <%= f.submit "#{t '.renew_btn'}", name: 'renew', class: 'btn btn-primary' %> + <% end %> +
+<% end %> diff --git a/app/views/registrar/bulk_change/_tech_contact_form.html.erb b/app/views/registrar/bulk_change/_tech_contact_form.html.erb index dc0693599..2848e3634 100644 --- a/app/views/registrar/bulk_change/_tech_contact_form.html.erb +++ b/app/views/registrar/bulk_change/_tech_contact_form.html.erb @@ -1,7 +1,7 @@ <%= form_tag registrar_tech_contacts_path, method: :patch, class: 'form-horizontal' do %> <% if @error %>
- <%= @error[:message] %> + <%= @error %>
<% end %> diff --git a/app/views/registrar/bulk_change/new.html.erb b/app/views/registrar/bulk_change/new.html.erb index da85899ff..e61270b6d 100644 --- a/app/views/registrar/bulk_change/new.html.erb +++ b/app/views/registrar/bulk_change/new.html.erb @@ -19,6 +19,10 @@
  • <%= t '.bulk_transfer' %>
  • + +
  • + <%= t '.bulk_renew' %> +
  • @@ -34,4 +38,8 @@
    <%= render 'bulk_transfer_form' %>
    + +
    + <%= render 'bulk_renew_form' %> +
    diff --git a/app/views/registrar/contacts/_form.haml b/app/views/registrar/contacts/_form.haml index cf8217e13..953c502e5 100644 --- a/app/views/registrar/contacts/_form.haml +++ b/app/views/registrar/contacts/_form.haml @@ -5,7 +5,7 @@ .col-md-8 = render 'registrar/contacts/form/general', f: f -- if address_processing? +- if Contact.address_processing? .row .col-md-8 = render 'registrar/contacts/form/address', f: f diff --git a/app/views/registrar/domain_transfers/form/_api_errors.html.erb b/app/views/registrar/domain_transfers/form/_api_errors.html.erb index 56bf8c404..8d8862959 100644 --- a/app/views/registrar/domain_transfers/form/_api_errors.html.erb +++ b/app/views/registrar/domain_transfers/form/_api_errors.html.erb @@ -1,9 +1,13 @@ <% if @api_errors %>
    <% end %> diff --git a/app/views/registrar/domains/partials/_contacts.haml b/app/views/registrar/domains/partials/_contacts.haml index 48d1ac21f..e6ef9aa8f 100644 --- a/app/views/registrar/domains/partials/_contacts.haml +++ b/app/views/registrar/domains/partials/_contacts.haml @@ -13,5 +13,5 @@ - registrant = Contact.find_by_code(x.text) %tr %td= x['type'] - %td= registrant.name + %td= registrant.registrar == current_registrar_user.registrar ? registrant.name : 'N/A' %td= x.text diff --git a/app/views/registrar/domains/partials/_general.html.erb b/app/views/registrar/domains/partials/_general.html.erb index 3fb3a5df8..ff064857c 100644 --- a/app/views/registrar/domains/partials/_general.html.erb +++ b/app/views/registrar/domains/partials/_general.html.erb @@ -23,7 +23,7 @@ <% registrant = Contact.find_by_code(@data.css('registrant').text) %>
    <%= t('.registrant') %>
    -
    <%= "#{registrant.name} (#{@data.css('registrant').text})" %>
    +
    <%= registrant.registrar == current_registrar_user.registrar ? "#{registrant.name} (#{@data.css('registrant').text})" : @data.css('registrant').text %>
    <%= t('.registered') %>
    <%= @data.css('crDate').text %>
    diff --git a/config/application.rb b/config/application.rb index 5f4481512..a5fb17c9d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -36,6 +36,7 @@ module DomainNameRegistry # Autoload all model subdirs config.autoload_paths += Dir[Rails.root.join('app', 'models', '**/')] + config.autoload_paths += Dir[Rails.root.join('app', 'interactions', '**/')] config.eager_load_paths << config.root.join('lib', 'validators') config.watchable_dirs['lib'] = %i[rb] diff --git a/config/application.yml.sample b/config/application.yml.sample index e979e772a..5885c47a2 100644 --- a/config/application.yml.sample +++ b/config/application.yml.sample @@ -87,6 +87,12 @@ sk_digi_doc_service_name: 'Testimine' registrant_api_base_url: registrant_api_auth_allowed_ips: '127.0.0.1, 0.0.0.0' #ips, separated with commas +# Bounces API +api_shared_key: testkey + +# Base URL (inc. https://) of REST registrant portal +# Leave blank to use internal registrant portal +registrant_portal_verifications_base_url: '' # # MISC diff --git a/config/database_travis.yml b/config/database_travis.yml index b79e2c453..48ffd5e27 100644 --- a/config/database_travis.yml +++ b/config/database_travis.yml @@ -4,7 +4,7 @@ default: &default encoding: unicode pool: 5 username: postgres - password: + password: password test: <<: *default diff --git a/config/locales/admin/menu.en.yml b/config/locales/admin/menu.en.yml index 617341c6a..cb1060e6f 100644 --- a/config/locales/admin/menu.en.yml +++ b/config/locales/admin/menu.en.yml @@ -14,6 +14,7 @@ en: blocked_domains: Blocked domains reserved_domains: Reserved domains disputed_domains: Disputed domains + bounced_email_addresses: Bounced emails epp_log: EPP log repp_log: REPP log que: Que diff --git a/config/locales/en.yml b/config/locales/en.yml index 8b7b4f0fa..22f29a6e3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -588,6 +588,9 @@ en: create_bank_transaction: 'Create bank transaction' create_new_invoice: 'Create new invoice' billing_failure_credit_balance_low: 'Billing failure - credit balance low' + billing_failure_credit_balance_low_for_domain: 'Billing failure - credit balance low for %{domain}' + domain_renew_error_for_domain: 'Domain renew error for %{domain}' + not_enough_funds: 'Not enough funds for renew domains' create: 'Create' activity_type: 'Activity type' receipt_date_from: 'Receipt date from' @@ -602,6 +605,7 @@ en: notes: Notes active_price_for_this_operation_is: 'Active price for this operation is %{price}' active_price_missing_for_this_operation: 'Active price missing for this operation!' + active_price_missing_for_operation_with_domain: 'Active price missing for operation with %{domain}' valid_to_from: 'Valid to from' valid_to_until: 'Valid to until' registrant_ident: 'Registrant ident' @@ -662,3 +666,4 @@ en: iban: IBAN sign_in: "Sign in" signed_in_successfully: "Signed in successfully" + bulk_renew_completed: "Bulk renew for domains completed" diff --git a/config/locales/registrar/bulk_change.en.yml b/config/locales/registrar/bulk_change.en.yml index 7c1f11da9..75becfada 100644 --- a/config/locales/registrar/bulk_change.en.yml +++ b/config/locales/registrar/bulk_change.en.yml @@ -6,6 +6,7 @@ en: technical_contact: Technical contact nameserver: Nameserver bulk_transfer: Bulk transfer + bulk_renew: Bulk renew tech_contact_form: current_contact_id: Current contact ID @@ -29,3 +30,11 @@ en: submit_btn: Transfer help_btn: Toggle help help: Transfer domains in the csv file with correct transfer code to this registrar + + bulk_renew_form: + filter_btn: Filter + renew_btn: Renew + expire_date: Expire date until + domain_ids: Domains for bulk renewal + current_balance: Current balance + period: Period diff --git a/config/locales/registrar/domain_transfers.en.yml b/config/locales/registrar/domain_transfers.en.yml index c146a5fd1..fd39e54f7 100644 --- a/config/locales/registrar/domain_transfers.en.yml +++ b/config/locales/registrar/domain_transfers.en.yml @@ -6,7 +6,7 @@ en: create: header: Domain transfer - transferred: "%{count} domains have been successfully transferred" + transferred: "%{count} domains have been successfully transferred. Failed domains: %{failed}" form: submit_btn: Transfer diff --git a/config/routes.rb b/config/routes.rb index f58063fae..c31682a13 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -37,12 +37,36 @@ Rails.application.routes.draw do get 'error/:command', to: 'errors#error' end - mount Repp::API => '/' - namespace :repp do namespace :v1 do + resources :contacts do + collection do + get 'check/:id', to: 'contacts#check' + end + end + + resources :accounts do + collection do + get 'balance' + end + end resources :auctions, only: %i[index] resources :retained_domains, only: %i[index] + namespace :registrar do + resources :nameservers do + collection do + put '/', to: 'nameservers#update' + end + end + end + resources :domains do + collection do + get ':id/transfer_info', to: 'domains#transfer_info', constraints: { id: /.*/ } + post 'transfer', to: 'domains#transfer' + patch 'contacts', to: 'domains/contacts#update' + post 'renew/bulk', to: 'domains/renews#bulk_renew' + end + end end end @@ -56,6 +80,8 @@ Rails.application.routes.draw do namespace :v1 do namespace :registrant do post 'auth/eid', to: 'auth#eid' + get 'confirms/:name/:template/:token', to: 'confirms#index', constraints: { name: /[^\/]+/ } + post 'confirms/:name/:template/:token/:decision', to: 'confirms#update', constraints: { name: /[^\/]+/ } resources :domains, only: %i[index show], param: :uuid do resource :registry_lock, only: %i[create destroy] @@ -65,7 +91,7 @@ Rails.application.routes.draw do end resources :auctions, only: %i[index show update], param: :uuid - + resources :bounces, only: %i[create] end match '*all', controller: 'cors', action: 'cors_preflight_check', via: [:options], @@ -108,6 +134,7 @@ Rails.application.routes.draw do end resources :domain_transfers, only: %i[new create] resource :bulk_change, controller: :bulk_change, only: :new + post '/bulk_renew/new', to: 'bulk_change#bulk_renew', as: :bulk_renew resource :tech_contacts, only: :update resource :nameservers, only: :update resources :contacts, constraints: {:id => /[^\/]+(?=#{ ActionController::Renderers::RENDERERS.map{|e| "\\.#{e}\\z"}.join("|") })|[^\/]+/} do @@ -295,6 +322,7 @@ Rails.application.routes.draw do resources :delayed_jobs resources :epp_logs resources :repp_logs + resources :bounced_mail_addresses, only: %i[index show destroy] authenticate :admin_user do mount Que::Web, at: 'que' diff --git a/db/migrate/20200916125326_create_bounced_mail_addresses.rb b/db/migrate/20200916125326_create_bounced_mail_addresses.rb new file mode 100644 index 000000000..e1744cc9a --- /dev/null +++ b/db/migrate/20200916125326_create_bounced_mail_addresses.rb @@ -0,0 +1,15 @@ +class CreateBouncedMailAddresses < ActiveRecord::Migration[6.0] + def change + create_table :bounced_mail_addresses do |t| + t.string :email, null: false + t.string :message_id, null: false + t.string :bounce_type, null: false + t.string :bounce_subtype, null: false + t.string :action, null: false + t.string :status, null: false + t.string :diagnostic, null: true + + t.timestamps + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 6a30fbc84..acf134a55 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -475,6 +475,43 @@ CREATE SEQUENCE public.blocked_domains_id_seq ALTER SEQUENCE public.blocked_domains_id_seq OWNED BY public.blocked_domains.id; +-- +-- Name: bounced_mail_addresses; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE public.bounced_mail_addresses ( + id bigint NOT NULL, + email character varying NOT NULL, + message_id character varying NOT NULL, + bounce_type character varying NOT NULL, + bounce_subtype character varying NOT NULL, + action character varying NOT NULL, + status character varying NOT NULL, + diagnostic character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: bounced_mail_addresses_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.bounced_mail_addresses_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: bounced_mail_addresses_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.bounced_mail_addresses_id_seq OWNED BY public.bounced_mail_addresses.id; + + -- -- Name: certificates; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -2658,6 +2695,13 @@ ALTER TABLE ONLY public.bank_transactions ALTER COLUMN id SET DEFAULT nextval('p ALTER TABLE ONLY public.blocked_domains ALTER COLUMN id SET DEFAULT nextval('public.blocked_domains_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.bounced_mail_addresses ALTER COLUMN id SET DEFAULT nextval('public.bounced_mail_addresses_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2876,14 +2920,14 @@ ALTER TABLE ONLY public.log_payment_orders ALTER COLUMN id SET DEFAULT nextval(' -- --- Name: log_prices id; Type: DEFAULT; Schema: public; Owner: - +-- Name: id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.log_prices ALTER COLUMN id SET DEFAULT nextval('public.log_prices_id_seq'::regclass); -- --- Name: log_registrant_verifications id; Type: DEFAULT; Schema: public; Owner: - +-- Name: id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.log_registrant_verifications ALTER COLUMN id SET DEFAULT nextval('public.log_registrant_verifications_id_seq'::regclass); @@ -3100,6 +3144,14 @@ ALTER TABLE ONLY public.blocked_domains ADD CONSTRAINT blocked_domains_pkey PRIMARY KEY (id); +-- +-- Name: bounced_mail_addresses_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY public.bounced_mail_addresses + ADD CONSTRAINT bounced_mail_addresses_pkey PRIMARY KEY (id); + + -- -- Name: certificates_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -3349,7 +3401,7 @@ ALTER TABLE ONLY public.log_payment_orders -- --- Name: log_prices log_prices_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: log_prices_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.log_prices @@ -3357,7 +3409,7 @@ ALTER TABLE ONLY public.log_prices -- --- Name: log_registrant_verifications log_registrant_verifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: log_registrant_verifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.log_registrant_verifications @@ -4906,5 +4958,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200902131603'), ('20200908131554'), ('20200910085157'), -('20200910102028'); +('20200910102028'), +('20200916125326'); + diff --git a/doc/repp/v1/account.md b/doc/repp/v1/account.md index 511c51382..80afabcdb 100644 --- a/doc/repp/v1/account.md +++ b/doc/repp/v1/account.md @@ -19,7 +19,11 @@ Content-Length: 37 Content-Type: application/json { - "balance": "324.45", - "currency": "EUR" + "code": 1000, + "message": "Command completed successfully", + "data": { + "balance": "356.0", + "currency": "EUR" + } } ``` diff --git a/doc/repp/v1/contact.md b/doc/repp/v1/contact.md index 41f45551f..aa6904b72 100644 --- a/doc/repp/v1/contact.md +++ b/doc/repp/v1/contact.md @@ -88,3 +88,141 @@ Content-Type: application/json "total_number_of_records": 2 } ``` + +## POST /repp/v1/contacts +Creates new contact + + +#### Request +``` +POST /repp/v1/contacts HTTP/1.1 +Authorization: Basic dGVzdDp0ZXN0MTIz +Content-Type: application/json + +{ + "contact": { + "name": "John Doe", + "email": "john@doe.com", + "phone": "+371.1234567", + "ident": { + "ident": "12345678901", + "ident_type": "priv", + "ident_country_code": "EE" + } + } +} +``` + +#### Response +``` +HTTP/1.1 200 +Cache-Control: max-age=0, private, must-revalidate +Content-Type: application/json + +{ + "code": 1000, + "message": "Command completed successfully", + "data": { + "contact": { + "id": "ATSAA:20DCDCA1" + } + } +} +``` + +#### Failed response +``` +HTTP/1.1 400 +Cache-Control: max-age=0, private, must-revalidate +Content-Type: application/json + +{ + "code": 2005, + "message": "Ident code does not conform to national identification number format of Estonia", + "data": {} +} +``` + +## PUT /repp/v1/contacts/**contact id** +Updates existing contact + + +#### Request +``` +PUT /repp/v1/contacts/ATSAA:9CD5F321 HTTP/1.1 +Authorization: Basic dGVzdDp0ZXN0MTIz +Content-Type: application/json + +{ + "contact": { + "phone": "+372.123123123" + } +} +``` + +#### Response +``` +HTTP/1.1 200 +Cache-Control: max-age=0, private, must-revalidate +Content-Type: application/json + +{ + "code": 1000, + "message": "Command completed successfully", + "data": { + "contact": { + "id": "ATSAA:20DCDCA1" + } + } +} +``` + +#### Failed response +``` +HTTP/1.1 400 +Cache-Control: max-age=0, private, must-revalidate +Content-Type: application/json + +{ + "code": 2005, + "message": "Phone nr is invalid [phone]", + "data": {} +} +``` + +## DELETE /repp/v1/contacts/**contact id** +Deletes existing contact + + +#### Request +``` +DELETE /repp/v1/contacts/ATSAA:9CD5F321 HTTP/1.1 +Authorization: Basic dGVzdDp0ZXN0MTIz +Content-Type: application/json +``` + +#### Response +``` +HTTP/1.1 200 +Cache-Control: max-age=0, private, must-revalidate +Content-Type: application/json + +{ + "code": 1000, + "message": "Command completed successfully", + "data": {} +} +``` + +#### Failed response +``` +HTTP/1.1 400 +Cache-Control: max-age=0, private, must-revalidate +Content-Type: application/json + +{ + "code": 2305, + "message": "Object association prohibits operation [domains]", + "data": {} +} +``` diff --git a/doc/repp/v1/domain.md b/doc/repp/v1/domain.md index c6734cbe2..20607f3b9 100644 --- a/doc/repp/v1/domain.md +++ b/doc/repp/v1/domain.md @@ -25,44 +25,52 @@ Content-Type: application/json ``` HTTP/1.1 200 Cache-Control: max-age=0, private, must-revalidate -Content-Length: 808 Content-Type: application/json { - "domains": [ - { - "id": 1, - "name": "domain0.ee", - "registrar_id": 2, - "registered_at": "2015-09-09T09:11:14.861Z", - "status": null, - "valid_from": "2015-09-09T09:11:14.861Z", - "valid_to": "2016-09-09T09:11:14.861Z", - "registrant_id": 1, - "transfer_code": "98oiewslkfkd", - "created_at": "2015-09-09T09:11:14.861Z", - "updated_at": "2015-09-09T09:11:14.860Z", - "name_dirty": "domain0.ee", - "name_puny": "domain0.ee", - "period": 1, - "period_unit": "y", - "creator_str": null, - "updator_str": null, - "outzone_at": "2016-09-24T09:11:14.861Z", - "delete_date": "2016-10-24", - "registrant_verification_asked_at": null, - "registrant_verification_token": null, - "pending_json": { - }, - "force_delete_date": null, - "statuses": [ - "ok" - ], - "status_notes": { + "code": 1000, + "message": "Command completed successfully", + "data": { + "domains": [ + { + "id": 7, + "name": "private.ee", + "registrar_id": 2, + "valid_to": "2022-09-23T00:00:00.000+03:00", + "registrant_id": 11, + "created_at": "2020-09-22T14:16:47.420+03:00", + "updated_at": "2020-10-21T13:31:43.733+03:00", + "name_dirty": "private.ee", + "name_puny": "private.ee", + "period": 1, + "period_unit": "y", + "creator_str": "2-ApiUser: test", + "updator_str": null, + "outzone_at": null, + "delete_date": null, + "registrant_verification_asked_at": null, + "registrant_verification_token": null, + "pending_json": {}, + "force_delete_date": null, + "statuses": [ + "serverRenewProhibited" + ], + "status_notes": { + "ok": "", + "serverRenewProhibited": "" + }, + "upid": null, + "up_date": null, + "uuid": "6b6affa7-1449-4bd8-acf5-8b4752406705", + "locked_by_registrant_at": null, + "force_delete_start": null, + "force_delete_data": null, + "auth_info": "367b1e6d1f0d9aa190971ad8f571cd4d", + "valid_from": "2020-09-22T14:16:47.420+03:00" } - } - ], - "total_number_of_records": 2 + ], + "total_number_of_records": 10 + } } ``` @@ -83,14 +91,17 @@ Content-Type: application/json ``` HTTP/1.1 200 Cache-Control: max-age=0, private, must-revalidate -Content-Length: 54 Content-Type: application/json { - "domains": [ - "domain1.ee" - ], - "total_number_of_records": 2 + "code": 1000, + "message": "Command completed successfully", + "data": { + "domains": [ + "private.ee", + ], + "total_number_of_records": 1 + } } ``` @@ -117,65 +128,68 @@ Please note that domain transfer/authorisation code must be placed in header - * ``` HTTP/1.1 200 OK Cache-Control: max-age=0, private, must-revalidate -Content-Length: 784 Content-Type: application/json - { - "domain":"ee-test.ee", - "registrant":{ - "code":"EE:R1", - "name":"Registrant", - "ident":"17612535", - "ident_type":"org", - "ident_country_code":"EE", - "phone":"+372.1234567", - "email":"registrant@cache.ee", - "street":"Businesstreet 1", - "city":"Tallinn", - "zip":"10101", - "country_code":"EE", - "statuses":[ - "ok", - "linked" - ] - }, - "admin_contacts":[ - { - "code":"EE:A1", - "name":"Admin Contact", - "ident":"17612535376", - "ident_type":"priv", - "ident_country_code":"EE", - "phone":"+372.7654321", - "email":"admin@cache.ee", - "street":"Adminstreet 2", - "city":"Tallinn", - "zip":"12345", - "country_code":"EE", - "statuses":[ - "ok", - "linked" + "code": 1000, + "message": "Command completed successfully", + "data": { + "domain":"ee-test.ee", + "registrant":{ + "code":"EE:R1", + "name":"Registrant", + "ident":"17612535", + "ident_type":"org", + "ident_country_code":"EE", + "phone":"+372.1234567", + "email":"registrant@cache.ee", + "street":"Businesstreet 1", + "city":"Tallinn", + "zip":"10101", + "country_code":"EE", + "statuses":[ + "ok", + "linked" + ] + }, + "admin_contacts":[ + { + "code":"EE:A1", + "name":"Admin Contact", + "ident":"17612535376", + "ident_type":"priv", + "ident_country_code":"EE", + "phone":"+372.7654321", + "email":"admin@cache.ee", + "street":"Adminstreet 2", + "city":"Tallinn", + "zip":"12345", + "country_code":"EE", + "statuses":[ + "ok", + "linked" + ] + } + ], + "tech_contacts":[ + { + "code":"EE:T1", + "name":"Tech Contact", + "ident":"17612536", + "ident_type":"org", + "ident_country_code":"EE", + "phone":"+372.7654321", + "email":"tech@cache.ee", + "street":"Techstreet 1", + "city":"Tallinn", + "zip":"12345", + "country_code":"EE", + "statuses":[ + "ok", + "linked" + ] + } ] } - ], - "tech_contacts":[ - { - "code":"EE:T1", - "name":"Tech Contact", - "ident":"17612536", - "ident_type":"org", - "ident_country_code":"EE", - "phone":"+372.7654321", - "email":"tech@cache.ee", - "street":"Techstreet 1", - "city":"Tallinn", - "zip":"12345", - "country_code":"EE", - "statuses":[ - "ok", - "linked" - ] - } - ] + } } ``` diff --git a/doc/repp/v1/domain_contacts.md b/doc/repp/v1/domain_contacts.md index b412e9f73..2e542bf81 100644 --- a/doc/repp/v1/domain_contacts.md +++ b/doc/repp/v1/domain_contacts.md @@ -5,15 +5,26 @@ Replaces all domain contacts of the current registrar. ### Example request ``` -$ curl https://repp.internet.ee/v1/domains/contacts \ - -X PATCH \ - -u username:password \ - -d current_contact_id=foo \ - -d new_contact_id=bar +PATCH /repp/v1/domains/contacts HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic dGVzdDp0ZXN0dGVzdA== + +{ + "current_contact_id": "ATSAA:749AA80F", + "new_contact_id": "ATSAA:E36957D7" +} ``` ### Example response ``` { - "affected_domains": ["example.com", "example.org"] + "code": 1000, + "message": "Command completed successfully", + "data": { + "affected_domains": [ + "private.ee", + ], + "skipped_domains": [] + } } ``` diff --git a/doc/repp/v1/domain_transfers.md b/doc/repp/v1/domain_transfers.md index a6eb4683c..a11c0a852 100644 --- a/doc/repp/v1/domain_transfers.md +++ b/doc/repp/v1/domain_transfers.md @@ -1,28 +1,28 @@ # Domain transfers -## POST /repp/v1/domain_transfers +## POST /repp/v1/domains/transfer Transfers domains. #### Request ``` -POST /repp/v1/domain_transfers +POST /repp/v1/domains/transfer Accept: application/json Content-Type: application/json Authorization: Basic dGVzdDp0ZXN0dGVzdA== { - "data":{ - "domainTransfers":[ - { - "domainName":"example.com", - "transferCode":"63e7" - }, - { - "domainName":"example.org", - "transferCode":"15f9" - } - ] - } + "data": { + "domain_transfers": [ + { + "domain_name":"example.com", + "transferCode":"63e7" + }, + { + "domain_name":"example.org", + "transferCode":"15f9" + } + ] + } } ``` @@ -31,14 +31,21 @@ Authorization: Basic dGVzdDp0ZXN0dGVzdA== HTTP/1.1 200 Content-Type: application/json { - "data":[ + "code": 1000, + "message": "Command completed successfully", + "data": { + "success": [ { - "type":"domain_transfer" + "type": "domain_transfer", + "domain_name": "example.com" }, { - "type":"domain_transfer" + "type": "domain_transfer", + "domain_name": "example.org" } - ] + ], + "failed": [] + } } ``` @@ -48,13 +55,32 @@ Content-Type: application/json HTTP/1.1 400 Content-Type: application/json { - "errors":[ + "code": 1000, + "message": "Command completed successfully", + "data": { + "success": [], + "failed": [ { - "title":"example.com transfer code is wrong" + "type": "domain_transfer", + "domain_name": "example.com", + "errors": [ + { + "code": "2202", + "msg": "Invalid authorization information" + } + ] }, { - "title":"example.org does not exist" + "type": "domain_transfer", + "domain_name": "example.org", + "errors": [ + { + "code": "2304", + "msg": "Object status prohibits operation" + } + ] } - ] + ] + } } ``` diff --git a/doc/repp/v1/nameservers.md b/doc/repp/v1/nameservers.md index 8190530d7..ab53c72df 100644 --- a/doc/repp/v1/nameservers.md +++ b/doc/repp/v1/nameservers.md @@ -10,15 +10,15 @@ Accept: application/json Content-Type: application/json Authorization: Basic dGVzdDp0ZXN0dGVzdA== { - "data":{ - "type": "nameserver", - "id": "ns1.example.com", - "attributes": { - "hostname": "new-ns1.example.com", - "ipv4": ["192.0.2.1", "192.0.2.2"], - "ipv6": ["2001:db8::1", "2001:db8::2"] - }, + "data": { + "type": "nameserver", + "id": "ns1.example.com", + "attributes": { + "hostname": "new-ns1.example.com", + "ipv4": ["192.0.2.1", "192.0.2.2"], + "ipv6": ["2001:db8::1", "2001:db8::2"] } + } } ``` @@ -27,16 +27,26 @@ Authorization: Basic dGVzdDp0ZXN0dGVzdA== HTTP/1.1 200 Content-Type: application/json { - "data":{ + "code": 1000, + "message": "Command completed successfully", + "data": { "type": "nameserver", "id": "new-ns1.example.com", "attributes": { "hostname": "new-ns1.example.com", - "ipv4": ["192.0.2.1", "192.0.2.2"], - "ipv6": ["2001:db8::1", "2001:db8::2"] - } - }, - "affected_domains": ["example.com", "example.org"] + "ipv4": [ + "192.0.2.1", + "192.0.2.2" + ], + "ipv6": [ + "2001:db8::1", + "2001:db8::2" + ] + }, + "affected_domains": [ + "private.ee" + ] + } } ``` @@ -44,14 +54,10 @@ Content-Type: application/json ``` HTTP/1.1 400 Content-Type: application/json + { - "errors":[ - { - "title":"ns1.example.com does not exist" - }, - { - "title":"192.0.2.1 is not a valid IPv4 address" - } - ] + "code": 2005, + "message": "IPv4 is invalid [ipv4]", + "data": {} } ``` diff --git a/lib/deserializers/xml/contact_create.rb b/lib/deserializers/xml/contact_create.rb new file mode 100644 index 000000000..5dfa32ef7 --- /dev/null +++ b/lib/deserializers/xml/contact_create.rb @@ -0,0 +1,10 @@ +require 'deserializers/xml/legal_document' +require 'deserializers/xml/ident' +require 'deserializers/xml/contact' + +module Deserializers + module Xml + class ContactCreate < ContactUpdate + end + end +end diff --git a/lib/serializers/registrant_api/.DS_Store b/lib/serializers/registrant_api/.DS_Store new file mode 100644 index 000000000..5008ddfcf Binary files /dev/null and b/lib/serializers/registrant_api/.DS_Store differ diff --git a/lib/serializers/registrant_api/contact.rb b/lib/serializers/registrant_api/contact.rb index dd36b4400..023544174 100644 --- a/lib/serializers/registrant_api/contact.rb +++ b/lib/serializers/registrant_api/contact.rb @@ -1,14 +1,15 @@ module Serializers module RegistrantApi class Contact - attr_reader :contact + attr_reader :contact, :links - def initialize(contact) + def initialize(contact, links) @contact = contact + @links = links end - def to_json - { + def to_json(_obj = nil) + obj = { id: contact.uuid, name: contact.name, code: contact.code, @@ -31,6 +32,10 @@ module Serializers statuses: contact.statuses, disclosed_attributes: contact.disclosed_attributes, } + + obj[:links] = contact.related_domains if @links + + obj end end end diff --git a/lib/serializers/registrant_api/domain.rb b/lib/serializers/registrant_api/domain.rb index 542f2d0de..2359d77c9 100644 --- a/lib/serializers/registrant_api/domain.rb +++ b/lib/serializers/registrant_api/domain.rb @@ -3,11 +3,14 @@ module Serializers class Domain attr_reader :domain - def initialize(domain) + def initialize(domain, simplify: false) @domain = domain + @simplify = simplify end - def to_json + def to_json(_obj = nil) + return simple_object if @simplify + { id: domain.uuid, name: domain.name, @@ -49,6 +52,17 @@ module Serializers private + def simple_object + { + id: domain.uuid, name: domain.name, registered_at: domain.registered_at, + valid_to: domain.valid_to, outzone_at: domain.outzone_at, statuses: domain.statuses, + registrant_verification_asked_at: domain.registrant_verification_asked_at, + registrar: { name: domain.registrar.name, website: domain.registrar.website }, + registrant: { name: domain.registrant.name, id: domain.registrant.uuid, + phone: domain.registrant.phone, email: domain.registrant.email } + } + end + def dnssec_keys domain.dnskeys.map do |key| "#{key.flags} #{key.protocol} #{key.alg} #{key.public_key}" diff --git a/lib/serializers/repp/contact.rb b/lib/serializers/repp/contact.rb new file mode 100644 index 000000000..834402359 --- /dev/null +++ b/lib/serializers/repp/contact.rb @@ -0,0 +1,36 @@ +module Serializers + module Repp + class Contact + attr_reader :contact + + def initialize(contact, show_address:) + @contact = contact + @show_address = show_address + end + + def to_json(obj = contact) + json = { id: obj.code, name: obj.name, ident: ident, + email: obj.email, phone: obj.phone, fax: obj.fax, + auth_info: obj.auth_info, statuses: obj.statuses, + disclosed_attributes: obj.disclosed_attributes } + + json[:address] = address if @show_address + + json + end + + def ident + { + code: contact.ident, + type: contact.ident_type, + country_code: contact.ident_country_code, + } + end + + def address + { street: contact.street, zip: contact.zip, city: contact.city, + state: contact.state, country_code: contact.country_code } + end + end + end +end diff --git a/test/fixtures/bounced_mail_addresses.yml b/test/fixtures/bounced_mail_addresses.yml new file mode 100644 index 000000000..cbfdff680 --- /dev/null +++ b/test/fixtures/bounced_mail_addresses.yml @@ -0,0 +1,10 @@ +one: + email: bounced@registry.test + message_id: 010f0174a0c7d348-ea6e2fc1-0854-4073-b71f-5cecf9b0d0b2-000000 + bounce_type: Permanent + bounce_subtype: General + action: failed + status: '5.1.1' + diagnostic: 'smtp; 550 5.1.1 user unknown' + created_at: <%= Time.zone.parse('2010-07-05').to_s(:db) %> + updated_at: <%= Time.zone.parse('2010-07-05').to_s(:db) %> diff --git a/test/integration/api/domain_contacts_test.rb b/test/integration/api/domain_contacts_test.rb index 5336cc10a..6704739d1 100644 --- a/test/integration/api/domain_contacts_test.rb +++ b/test/integration/api/domain_contacts_test.rb @@ -27,8 +27,8 @@ class APIDomainContactsTest < ApplicationIntegrationTest headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response :ok - assert_equal ({ affected_domains: %w[airport.test shop.test], - skipped_domains: [] }), + assert_equal ({ code: 1000, message: 'Command completed successfully', data: { affected_domains: %w[airport.test shop.test], + skipped_domains: [] }}), JSON.parse(response.body, symbolize_names: true) end @@ -42,7 +42,7 @@ class APIDomainContactsTest < ApplicationIntegrationTest assert_response :ok assert_equal %w[airport.test shop.test], JSON.parse(response.body, - symbolize_names: true)[:skipped_domains] + symbolize_names: true)[:data][:skipped_domains] end def test_keep_other_tech_contacts_intact @@ -66,10 +66,8 @@ class APIDomainContactsTest < ApplicationIntegrationTest new_contact_id: 'william-002' }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert_response :bad_request - assert_equal ({ error: { type: 'invalid_request_error', - param: 'current_contact_id', - message: 'No such contact: jack-001' } }), + assert_response :not_found + assert_equal ({ code: 2303, message: 'Object does not exist' }), JSON.parse(response.body, symbolize_names: true) end @@ -77,10 +75,8 @@ class APIDomainContactsTest < ApplicationIntegrationTest patch '/repp/v1/domains/contacts', params: { current_contact_id: 'non-existent', new_contact_id: 'john-001' }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert_response :bad_request - assert_equal ({ error: { type: 'invalid_request_error', - param: 'current_contact_id', - message: 'No such contact: non-existent' } }), + assert_response :not_found + assert_equal ({ code: 2303, message: 'Object does not exist' }), JSON.parse(response.body, symbolize_names: true) end @@ -88,10 +84,8 @@ class APIDomainContactsTest < ApplicationIntegrationTest patch '/repp/v1/domains/contacts', params: { current_contact_id: 'william-001', new_contact_id: 'non-existent' }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert_response :bad_request - assert_equal ({ error: { type: 'invalid_request_error', - param: 'new_contact_id', - message: 'No such contact: non-existent' } }), + assert_response :not_found + assert_equal ({code: 2303, message: 'Object does not exist'}), JSON.parse(response.body, symbolize_names: true) end @@ -100,9 +94,7 @@ class APIDomainContactsTest < ApplicationIntegrationTest new_contact_id: 'invalid' }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response :bad_request - assert_equal ({ error: { type: 'invalid_request_error', - param: 'new_contact_id', - message: 'New contact must be valid' } }), + assert_equal ({ code: 2304, message: 'New contact must be valid', data: {} }), JSON.parse(response.body, symbolize_names: true) end @@ -111,8 +103,7 @@ class APIDomainContactsTest < ApplicationIntegrationTest new_contact_id: 'william-001' }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response :bad_request - assert_equal ({ error: { type: 'invalid_request_error', - message: 'New contact ID must be different from current contact ID' } }), + assert_equal ({ code: 2304, message: 'New contact must be different from current', data: {} }), JSON.parse(response.body, symbolize_names: true) end diff --git a/test/integration/api/domain_transfers_test.rb b/test/integration/api/domain_transfers_test.rb index aabaeb728..3e9c10100 100644 --- a/test/integration/api/domain_transfers_test.rb +++ b/test/integration/api/domain_transfers_test.rb @@ -12,34 +12,21 @@ class APIDomainTransfersTest < ApplicationIntegrationTest Setting.transfer_wait_time = @original_transfer_wait_time end - def test_returns_domain_transfers - post '/repp/v1/domain_transfers', params: request_params, as: :json, - headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert_response 200 - assert_equal ({ data: [{ - type: 'domain_transfer', - attributes: { - domain_name: 'shop.test' - }, - }] }), - JSON.parse(response.body, symbolize_names: true) - end - def test_creates_new_domain_transfer assert_difference -> { @domain.transfers.size } do - post '/repp/v1/domain_transfers', params: request_params, as: :json, + post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } end end def test_approves_automatically_if_auto_approval_is_enabled - post '/repp/v1/domain_transfers', params: request_params, as: :json, + post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert @domain.transfers.last.approved? end def test_assigns_new_registrar - post '/repp/v1/domain_transfers', params: request_params, as: :json, + post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } @domain.reload assert_equal @new_registrar, @domain.registrar @@ -48,7 +35,7 @@ class APIDomainTransfersTest < ApplicationIntegrationTest def test_regenerates_transfer_code @old_transfer_code = @domain.transfer_code - post '/repp/v1/domain_transfers', params: request_params, as: :json, + post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } @domain.reload refute_equal @domain.transfer_code, @old_transfer_code @@ -58,51 +45,28 @@ class APIDomainTransfersTest < ApplicationIntegrationTest @old_registrar = @domain.registrar assert_difference -> { @old_registrar.notifications.count } do - post '/repp/v1/domain_transfers', params: request_params, as: :json, + post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } end end def test_duplicates_registrant_admin_and_tech_contacts assert_difference -> { @new_registrar.contacts.size }, 3 do - post '/repp/v1/domain_transfers', params: request_params, as: :json, + post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } end end def test_reuses_identical_contact - post '/repp/v1/domain_transfers', params: request_params, as: :json, + post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_equal 1, @new_registrar.contacts.where(name: 'William').size end - def test_fails_if_domain_does_not_exist - post '/repp/v1/domain_transfers', - params: { data: { domainTransfers: [{ domainName: 'non-existent.test', - transferCode: 'any' }] } }, - as: :json, - headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert_response 400 - assert_equal ({ errors: [{ title: 'non-existent.test does not exist' }] }), - JSON.parse(response.body, symbolize_names: true) - end - - def test_fails_if_transfer_code_is_wrong - post '/repp/v1/domain_transfers', - params: { data: { domainTransfers: [{ domainName: 'shop.test', - transferCode: 'wrong' }] } }, - as: :json, - headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert_response 400 - refute_equal @new_registrar, @domain.registrar - assert_equal ({ errors: [{ title: 'shop.test transfer code is wrong' }] }), - JSON.parse(response.body, symbolize_names: true) - end - private def request_params - { data: { domainTransfers: [{ domainName: 'shop.test', transferCode: '65078d5' }] } } + { data: { domain_transfers: [{ domain_name: 'shop.test', transfer_code: '65078d5' }] } } end def http_auth_key diff --git a/test/integration/api/nameservers/put_test.rb b/test/integration/api/nameservers/put_test.rb index 853a20549..3ab4f4dd4 100644 --- a/test/integration/api/nameservers/put_test.rb +++ b/test/integration/api/nameservers/put_test.rb @@ -60,12 +60,14 @@ class APINameserversPutTest < ApplicationIntegrationTest headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response 200 - assert_equal ({ data: { type: 'nameserver', + assert_equal ({ code: 1000, + message: 'Command completed successfully', + data: { type: 'nameserver', id: 'ns55.bestnames.test', attributes: { hostname: 'ns55.bestnames.test', ipv4: ['192.0.2.55'], - ipv6: ['2001:db8::55'] } }, - affected_domains: ["airport.test", "shop.test"] }), + ipv6: ['2001:db8::55'] }, + affected_domains: ["airport.test", "shop.test"] }}), JSON.parse(response.body, symbolize_names: true) end @@ -85,7 +87,7 @@ class APINameserversPutTest < ApplicationIntegrationTest headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response 404 - assert_equal ({ errors: [{ title: 'Hostname non-existent.test does not exist' }] }), + assert_equal ({code: 2303, message: 'Object does not exist' }), JSON.parse(response.body, symbolize_names: true) end @@ -96,7 +98,8 @@ class APINameserversPutTest < ApplicationIntegrationTest headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response 400 - assert_equal ({ errors: [{ title: 'Hostname is missing' }] }), + assert_equal ({ code: 2003, + message: 'param is missing or the value is empty: hostname' }), JSON.parse(response.body, symbolize_names: true) end diff --git a/test/integration/api/registrant/registrant_api_domains_test.rb b/test/integration/api/registrant/registrant_api_domains_test.rb index 22516fecc..61d635e5f 100644 --- a/test/integration/api/registrant/registrant_api_domains_test.rb +++ b/test/integration/api/registrant/registrant_api_domains_test.rb @@ -50,10 +50,10 @@ class RegistrantApiDomainsTest < ApplicationIntegrationTest assert_equal(200, response.status) response_json = JSON.parse(response.body, symbolize_names: true) - array_of_domain_names = response_json.map { |x| x[:name] } + array_of_domain_names = response_json[:domains].map { |x| x[:name] } assert(array_of_domain_names.include?('hospital.test')) - array_of_domain_registrars = response_json.map { |x| x[:registrar] } + array_of_domain_registrars = response_json[:domains].map { |x| x[:registrar] } assert(array_of_domain_registrars.include?({name: 'Good Names', website: nil})) end @@ -63,12 +63,12 @@ class RegistrantApiDomainsTest < ApplicationIntegrationTest response_json = JSON.parse(response.body, symbolize_names: true) assert_equal(200, response.status) - assert_equal(2, response_json.count) + assert_equal(2, response_json[:domains].count) get '/api/v1/registrant/domains', headers: @auth_headers response_json = JSON.parse(response.body, symbolize_names: true) - assert_equal(4, response_json.count) + assert_equal(4, response_json[:domains].count) end def test_root_does_not_accept_limit_higher_than_200 diff --git a/test/integration/api/registrant/registrant_api_verifications_test.rb b/test/integration/api/registrant/registrant_api_verifications_test.rb new file mode 100644 index 000000000..821d0dee0 --- /dev/null +++ b/test/integration/api/registrant/registrant_api_verifications_test.rb @@ -0,0 +1,297 @@ +require 'test_helper' +require 'auth_token/auth_token_creator' + +class RegistrantApiVerificationsTest < ApplicationIntegrationTest + def setup + super + + @domain = domains(:hospital) + @registrant = @domain.registrant + @new_registrant = contacts(:jack) + @user = users(:api_bestnames) + + @token = 'verysecrettoken' + + @domain.update!(statuses: [DomainStatus::PENDING_UPDATE], + registrant_verification_asked_at: Time.zone.now - 1.day, + registrant_verification_token: @token) + + end + + def test_fetches_registrant_change_request + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update(pending_json: pending_json) + @domain.reload + + assert @domain.registrant_update_confirmable?(@token) + + get "/api/v1/registrant/confirms/#{@domain.name_puny}/change/#{@token}" + assert_equal(200, response.status) + + res = JSON.parse(response.body, symbolize_names: true) + expected_body = { + domain_name: "hospital.test", + current_registrant: { + name: @registrant.name, + ident: @registrant.ident, + country: @registrant.ident_country_code + }, + new_registrant: { + name: @new_registrant.name, + ident: @new_registrant.ident, + country: @new_registrant.ident_country_code + } + } + + assert_equal expected_body, res + end + + def test_approves_registrant_change_request + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update!(pending_json: pending_json) + @domain.reload + + assert @domain.registrant_update_confirmable?(@token) + + perform_enqueued_jobs do + post "/api/v1/registrant/confirms/#{@domain.name_puny}/change/#{@token}/confirmed" + assert_equal(200, response.status) + + res = JSON.parse(response.body, symbolize_names: true) + expected_body = { + domain_name: @domain.name, + current_registrant: { + name: @new_registrant.name, + ident: @new_registrant.ident, + country: @new_registrant.ident_country_code + }, + status: 'confirmed' + } + assert_equal expected_body, res + end + end + + def test_rejects_registrant_change_request + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update(pending_json: pending_json) + @domain.reload + + assert @domain.registrant_update_confirmable?(@token) + + post "/api/v1/registrant/confirms/#{@domain.name_puny}/change/#{@token}/rejected" + assert_equal(200, response.status) + + res = JSON.parse(response.body, symbolize_names: true) + expected_body = { + domain_name: @domain.name, + current_registrant: { + name: @registrant.name, + ident: @registrant.ident, + country: @registrant.ident_country_code + }, + status: 'rejected' + } + + assert_equal expected_body, res + end + + def test_registrant_change_requires_valid_attributes + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update(pending_json: pending_json) + @domain.reload + + get "/api/v1/registrant/confirms/#{@domain.name_puny}/change/123" + assert_equal 401, response.status + + get "/api/v1/registrant/confirms/aohldfjg.ee/change/123" + assert_equal 404, response.status + + post "/api/v1/registrant/confirms/#{@domain.name_puny}/change/#{@token}/invalidaction" + assert_equal 404, response.status + end + + def test_fetches_domain_delete_request + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update(pending_json: pending_json, statuses: [DomainStatus::PENDING_DELETE_CONFIRMATION]) + @domain.reload + + assert @domain.registrant_delete_confirmable?(@token) + + get "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/#{@token}" + assert_equal(200, response.status) + + res = JSON.parse(response.body, symbolize_names: true) + expected_body = { + domain_name: "hospital.test", + current_registrant: { + name: @registrant.name, + ident: @registrant.ident, + country: @registrant.ident_country_code + } + } + + assert_equal expected_body, res + end + + def test_approves_domain_delete_request + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update(pending_json: pending_json, statuses: [DomainStatus::PENDING_DELETE_CONFIRMATION]) + @domain.reload + + assert @domain.registrant_delete_confirmable?(@token) + + post "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/#{@token}/confirmed" + assert_equal(200, response.status) + + res = JSON.parse(response.body, symbolize_names: true) + expected_body = { + domain_name: @domain.name, + current_registrant: { + name: @registrant.name, + ident: @registrant.ident, + country: @registrant.ident_country_code + }, + status: 'confirmed' + } + + assert_equal expected_body, res + end + + def test_rejects_domain_delete_request + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update(pending_json: pending_json, statuses: [DomainStatus::PENDING_DELETE_CONFIRMATION]) + @domain.reload + + assert @domain.registrant_delete_confirmable?(@token) + + post "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/#{@token}/rejected" + assert_equal(200, response.status) + + res = JSON.parse(response.body, symbolize_names: true) + expected_body = { + domain_name: @domain.name, + current_registrant: { + name: @registrant.name, + ident: @registrant.ident, + country: @registrant.ident_country_code + }, + status: 'rejected' + } + + assert_equal expected_body, res + end + + def test_domain_delete_requires_valid_attributes + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update(pending_json: pending_json, statuses: [DomainStatus::PENDING_DELETE_CONFIRMATION]) + @domain.reload + + get "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/123" + assert_equal 401, response.status + + get "/api/v1/registrant/confirms/aohldfjg.ee/delete/123" + assert_equal 404, response.status + + post "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/#{@token}/invalidaction" + assert_equal 404, response.status + end + #def test_get_non_existent_domain_details_by_uuid + # get '/api/v1/registrant/domains/random-uuid', headers: @auth_headers + # assert_equal(404, response.status) + + # response_json = JSON.parse(response.body, symbolize_names: true) + # assert_equal({ errors: [base: ['Domain not found']] }, response_json) + #end + + #def test_root_returns_domain_list + # get '/api/v1/registrant/domains', headers: @auth_headers + # assert_equal(200, response.status) + + # response_json = JSON.parse(response.body, symbolize_names: true) + # array_of_domain_names = response_json.map { |x| x[:name] } + # assert(array_of_domain_names.include?('hospital.test')) + + # array_of_domain_registrars = response_json.map { |x| x[:registrar] } + # assert(array_of_domain_registrars.include?({name: 'Good Names', website: nil})) + #end + + #def test_root_accepts_limit_and_offset_parameters + # get '/api/v1/registrant/domains', params: { 'limit' => 2, 'offset' => 0 }, + # headers: @auth_headers + # response_json = JSON.parse(response.body, symbolize_names: true) + + # assert_equal(200, response.status) + # assert_equal(2, response_json.count) + + # get '/api/v1/registrant/domains', headers: @auth_headers + # response_json = JSON.parse(response.body, symbolize_names: true) + + # assert_equal(4, response_json.count) + #end + + #def test_root_does_not_accept_limit_higher_than_200 + # get '/api/v1/registrant/domains', params: { 'limit' => 400, 'offset' => 0 }, + # headers: @auth_headers + + # assert_equal(400, response.status) + # response_json = JSON.parse(response.body, symbolize_names: true) + # assert_equal({ errors: [{ limit: ['parameter is out of range'] }] }, response_json) + #end + + #def test_root_does_not_accept_offset_lower_than_0 + # get '/api/v1/registrant/domains', params: { 'limit' => 200, 'offset' => "-10" }, + # headers: @auth_headers + + # assert_equal(400, response.status) + # response_json = JSON.parse(response.body, symbolize_names: true) + # assert_equal({ errors: [{ offset: ['parameter is out of range'] }] }, response_json) + #end + + #def test_root_returns_401_without_authorization + # get '/api/v1/registrant/domains' + # assert_equal(401, response.status) + # json_body = JSON.parse(response.body, symbolize_names: true) + + # assert_equal({ errors: [base: ['Not authorized']] }, json_body) + #end + + #def test_details_returns_401_without_authorization + # get '/api/v1/registrant/domains/5edda1a5-3548-41ee-8b65-6d60daf85a37' + # assert_equal(401, response.status) + # json_body = JSON.parse(response.body, symbolize_names: true) + + # assert_equal({ errors: [base: ['Not authorized']] }, json_body) + #end +end diff --git a/test/integration/api/v1/bounces/create_test.rb b/test/integration/api/v1/bounces/create_test.rb new file mode 100644 index 000000000..899b6c5c7 --- /dev/null +++ b/test/integration/api/v1/bounces/create_test.rb @@ -0,0 +1,75 @@ +require 'test_helper' + +class BouncesApiV1CreateTest < ActionDispatch::IntegrationTest + def setup + @api_key = "Basic #{ENV['api_shared_key']}" + @headers = { "Authorization": "#{@api_key}" } + @json_body = { "data": valid_bounce_request }.as_json + end + + def test_authorizes_api_request + post api_v1_bounces_path, params: @json_body, headers: @headers + assert_response :created + + invalid_headers = { "Authorization": "Basic invalid_api_key" } + post api_v1_bounces_path, params: @json_body, headers: invalid_headers + assert_response :unauthorized + end + + def test_returns_bad_request_if_invalid_payload + invalid_json_body = @json_body.dup + invalid_json_body['data']['bounce']['bouncedRecipients'] = nil + + post api_v1_bounces_path, params: invalid_json_body, headers: @headers + assert_response :bad_request + + invalid_json_body = 'aaaa' + post api_v1_bounces_path, params: invalid_json_body, headers: @headers + assert_response :bad_request + end + + def test_saves_new_bounce_object + request_body = @json_body.dup + random_mail = "#{rand(10000..99999)}@registry.test" + request_body['data']['bounce']['bouncedRecipients'][0]['emailAddress'] = random_mail + + post api_v1_bounces_path, params: request_body, headers: @headers + assert_response :created + + bounced_mail = BouncedMailAddress.last + assert bounced_mail.email = random_mail + assert '5.1.1', bounced_mail.status + assert 'failed', bounced_mail.action + end + + def valid_bounce_request + { + "notificationType": "Bounce", + "mail": { + "source": "noreply@registry.test", + "sourceIp": "195.43.86.5", + "messageId": "010f0174a0c7d348-ea6e2fc1-0854-4073-b71f-5cecf9b0d0b2-000000", + "sourceArn": "arn:aws:ses:us-east-2:65026820000:identity/noreply@registry.test", + "timestamp": "2020-09-18T10:34:44.000Z", + "destination": [ "bounced@registry.test" ], + "sendingAccountId": "650268220000" + }, + "bounce": { + "timestamp": "2020-09-18T10:34:44.911Z", + "bounceType": "Permanent", + "feedbackId": "010f0174a0c7d4f9-27d59756-6111-4d5f-xxxx-26bee0d55fa2-000000", + "remoteMtaIp": "127.0.01", + "reportingMTA": "dsn; xxx.amazonses.com", + "bounceSubType": "General", + "bouncedRecipients": [ + { + "action": "failed", + "status": "5.1.1", + "emailAddress": "bounced@registry.test", + "diagnosticCode": "smtp; 550 5.1.1 user unknown" + } + ] + } + }.as_json + end +end diff --git a/test/integration/epp/contact/info/base_test.rb b/test/integration/epp/contact/info/base_test.rb index 80dad97e8..4e4a9190e 100644 --- a/test/integration/epp/contact/info/base_test.rb +++ b/test/integration/epp/contact/info/base_test.rb @@ -44,7 +44,7 @@ class EppContactInfoBaseTest < EppTestCase contact: xml_schema).text end - def test_hides_password_when_current_registrar_is_not_sponsoring + def test_hides_password_and_name_when_current_registrar_is_not_sponsoring non_sponsoring_registrar = registrars(:goodnames) @contact.update!(registrar: non_sponsoring_registrar) @@ -70,6 +70,7 @@ class EppContactInfoBaseTest < EppTestCase assert_epp_response :completed_successfully response_xml = Nokogiri::XML(response.body) assert_nil response_xml.at_xpath('//contact:authInfo', contact: xml_schema) + assert_equal 'No access', response_xml.at_xpath('//contact:name', contact: xml_schema).text end private diff --git a/test/integration/epp/domain/delete/base_test.rb b/test/integration/epp/domain/delete/base_test.rb index bfdfa9f75..56a3cc31e 100644 --- a/test/integration/epp/domain/delete/base_test.rb +++ b/test/integration/epp/domain/delete/base_test.rb @@ -35,7 +35,6 @@ class EppDomainDeleteBaseTest < EppTestCase XML post epp_delete_path, params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' } - # binding.pry assert_includes Domain.find_by(name: 'invalid.test').statuses, DomainStatus::PENDING_DELETE_CONFIRMATION assert_epp_response :completed_successfully_action_pending end @@ -90,7 +89,9 @@ class EppDomainDeleteBaseTest < EppTestCase XML - post epp_delete_path, params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' } + perform_enqueued_jobs do + post epp_delete_path, params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' } + end @domain.reload assert @domain.registrant_verification_asked? @@ -121,7 +122,9 @@ class EppDomainDeleteBaseTest < EppTestCase XML - post epp_delete_path, params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' } + perform_enqueued_jobs do + post epp_delete_path, params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' } + end @domain.reload assert_not @domain.registrant_verification_asked? @@ -152,7 +155,9 @@ class EppDomainDeleteBaseTest < EppTestCase XML - post epp_delete_path, params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' } + perform_enqueued_jobs do + post epp_delete_path, params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' } + end @domain.reload assert_not @domain.registrant_verification_asked? diff --git a/test/integration/epp/domain/update/base_test.rb b/test/integration/epp/domain/update/base_test.rb index 6ce455948..14e806fca 100644 --- a/test/integration/epp/domain/update/base_test.rb +++ b/test/integration/epp/domain/update/base_test.rb @@ -503,6 +503,30 @@ class EppDomainUpdateBaseTest < EppTestCase assert_not_includes(@domain.statuses, DomainStatus::CLIENT_HOLD) end + def test_update_domain_returns_error_when_removing_unassigned_status + assert_not_includes(@domain.statuses, DomainStatus::CLIENT_HOLD) + request_xml = <<-XML + + + + + + #{@domain.name} + + + + + + + + XML + + post epp_update_path, params: { frame: request_xml }, + headers: { 'HTTP_COOKIE' => 'session=api_bestnames' } + @domain.reload + assert_epp_response :object_does_not_exist + end + private def assert_verification_and_notification_emails diff --git a/test/integration/repp/v1/accounts/balance_test.rb b/test/integration/repp/v1/accounts/balance_test.rb new file mode 100644 index 000000000..785e0aee8 --- /dev/null +++ b/test/integration/repp/v1/accounts/balance_test.rb @@ -0,0 +1,22 @@ +require 'test_helper' + +class ReppV1BalanceTest < ActionDispatch::IntegrationTest + def setup + @registrar = users(:api_bestnames) + token = Base64.encode64("#{@registrar.username}:#{@registrar.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_can_query_balance + get '/repp/v1/accounts/balance', headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + assert_equal @registrar.registrar.cash_account.balance.to_s, json[:data][:balance] + assert_equal @registrar.registrar.cash_account.currency, json[:data][:currency] + end +end diff --git a/test/integration/repp/auctions_test.rb b/test/integration/repp/v1/auctions_test.rb similarity index 100% rename from test/integration/repp/auctions_test.rb rename to test/integration/repp/v1/auctions_test.rb diff --git a/test/integration/repp/v1/base_test.rb b/test/integration/repp/v1/base_test.rb new file mode 100644 index 000000000..d0baed30e --- /dev/null +++ b/test/integration/repp/v1/base_test.rb @@ -0,0 +1,63 @@ +require 'test_helper' + +class ReppV1BaseTest < ActionDispatch::IntegrationTest + def setup + @registrar = users(:api_bestnames) + token = Base64.encode64("#{@registrar.username}:#{@registrar.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_unauthorized_user_has_no_access + get repp_v1_contacts_path + response_json = JSON.parse(response.body, symbolize_names: true) + + assert_response :unauthorized + assert_equal 'Invalid authorization information', response_json[:message] + + invalid_token = Base64.encode64("nonexistant:user") + headers = { 'Authorization' => "Basic #{invalid_token}" } + + get repp_v1_contacts_path, headers: headers + response_json = JSON.parse(response.body, symbolize_names: true) + + assert_response :unauthorized + assert_equal 'Invalid authorization information', response_json[:message] + end + + def test_authenticates_valid_user + get repp_v1_contacts_path, headers: @auth_headers + response_json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + end + + def test_processes_invalid_base64_token_format_properly + token = '??as8d9sf kjsdjh klsdfjjf' + headers = { 'Authorization' => "Basic #{token}"} + get repp_v1_contacts_path, headers: headers + response_json = JSON.parse(response.body, symbolize_names: true) + + assert_response :unauthorized + assert_equal 'Invalid authorization information', response_json[:message] + end + + def test_takes_ip_whitelist_into_account + Setting.api_ip_whitelist_enabled = true + Setting.registrar_ip_whitelist_enabled = true + + whiteip = white_ips(:one) + whiteip.update(ipv4: '1.1.1.1') + + get repp_v1_contacts_path, headers: @auth_headers + response_json = JSON.parse(response.body, symbolize_names: true) + + assert_response :unauthorized + assert_equal 2202, response_json[:code] + assert response_json[:message].include? 'Access denied from IP' + + Setting.api_ip_whitelist_enabled = false + Setting.registrar_ip_whitelist_enabled = false + end +end diff --git a/test/integration/repp/v1/contacts/check_test.rb b/test/integration/repp/v1/contacts/check_test.rb new file mode 100644 index 000000000..be0d979b1 --- /dev/null +++ b/test/integration/repp/v1/contacts/check_test.rb @@ -0,0 +1,30 @@ +require 'test_helper' + +class ReppV1ContactsCheckTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_code_based_check_returns_true_for_available_contact + get '/repp/v1/contacts/check/nonexistant:code', headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 'nonexistant:code', json[:data][:contact][:id] + assert_equal true, json[:data][:contact][:available] + end + + def test_code_based_check_returns_true_for_available_contact + contact = contacts(:jack) + get "/repp/v1/contacts/check/#{contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal contact.code, json[:data][:contact][:id] + assert_equal false, json[:data][:contact][:available] + end +end diff --git a/test/integration/repp/v1/contacts/create_test.rb b/test/integration/repp/v1/contacts/create_test.rb new file mode 100644 index 000000000..f30bc368f --- /dev/null +++ b/test/integration/repp/v1/contacts/create_test.rb @@ -0,0 +1,156 @@ +require 'test_helper' + +class ReppV1ContactsCreateTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_creates_new_contact + request_body = { + "contact": { + "name": "Donald Trump", + "phone": "+372.51111112", + "email": "donald@trumptower.com", + "ident": { + "ident_type": "priv", + "ident_country_code": "EE", + "ident": "39708290069" + } + } + } + + post '/repp/v1/contacts', headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + contact = Contact.find_by(code: json[:data][:contact][:id]) + assert contact.present? + + assert_equal(request_body[:contact][:name], contact.name) + assert_equal(request_body[:contact][:phone], contact.phone) + assert_equal(request_body[:contact][:email], contact.email) + assert_equal(request_body[:contact][:ident][:ident_type], contact.ident_type) + assert_equal(request_body[:contact][:ident][:ident_country_code], contact.ident_country_code) + assert_equal(request_body[:contact][:ident][:ident], contact.ident) + end + + def test_removes_postal_info_when_contact_created + request_body = { + "contact": { + "name": "Donald Trump", + "phone": "+372.51111111", + "email": "donald@trump.com", + "ident": { + "ident_type": "priv", + "ident_country_code": "EE", + "ident": "39708290069" + }, + "addr": { + "city": "Tallinn", + "street": "Wismari 13", + "zip": "12345", + "country_code": "EE" + } + } + } + + post '/repp/v1/contacts', headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1100, json[:code] + assert_equal 'Command completed successfully; Postal address data discarded', json[:message] + + contact = Contact.find_by(code: json[:data][:contact][:id]) + assert contact.present? + + assert_nil contact.city + assert_nil contact.street + assert_nil contact.zip + assert_nil contact.country_code + end + + def test_requires_contact_address_when_processing_enabled + Setting.address_processing = true + + request_body = { + "contact": { + "name": "Donald Trump", + "phone": "+372.51111112", + "email": "donald@trumptower.com", + "ident": { + "ident_type": "priv", + "ident_country_code": "EE", + "ident": "39708290069" + } + } + } + + post '/repp/v1/contacts', headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2003, json[:code] + assert json[:message].include? 'param is missing or the value is empty' + + Setting.address_processing = false + end + + def test_validates_ident_code + request_body = { + "contact": { + "name": "Donald Trump", + "phone": "+372.51111112", + "email": "donald@trumptower.com", + "ident": { + "ident_type": "priv", + "ident_country_code": "EE", + "ident": "123123123" + } + } + } + + post '/repp/v1/contacts', headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2005, json[:code] + assert json[:message].include? 'Ident code does not conform to national identification number format' + end + + def test_attaches_legaldoc_if_present + request_body = { + "contact": { + "name": "Donald Trump", + "phone": "+372.51111112", + "email": "donald@trumptower.com", + "ident": { + "ident_type": "priv", + "ident_country_code": "EE", + "ident": "39708290069" + }, + }, + "legal_document": { + "type": "pdf", + "body": "#{'test' * 2000}" + } + } + + post '/repp/v1/contacts', headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + contact = Contact.find_by(code: json[:data][:contact][:id]) + assert contact.legal_documents.any? + end +end diff --git a/test/integration/repp/v1/contacts/delete_test.rb b/test/integration/repp/v1/contacts/delete_test.rb new file mode 100644 index 000000000..07438d8af --- /dev/null +++ b/test/integration/repp/v1/contacts/delete_test.rb @@ -0,0 +1,47 @@ +require 'test_helper' + +class ReppV1ContactsDeleteTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_deletes_unassociated_contact + contact = contacts(:invalid_email) + delete "/repp/v1/contacts/#{contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + end + + def test_can_not_delete_associated_contact + contact = contacts(:john) + delete "/repp/v1/contacts/#{contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2305, json[:code] + assert_equal 'Object association prohibits operation [domains]', json[:message] + end + + def test_handles_unknown_contact + delete "/repp/v1/contacts/definitely:unexistant", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + end + + def test_can_not_destroy_other_registrar_contact + contact = contacts(:jack) + + delete "/repp/v1/contacts/#{contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + end +end diff --git a/test/integration/repp/v1/contacts/list_test.rb b/test/integration/repp/v1/contacts/list_test.rb new file mode 100644 index 000000000..31c4baaf9 --- /dev/null +++ b/test/integration/repp/v1/contacts/list_test.rb @@ -0,0 +1,55 @@ +require 'test_helper' + +class ReppV1ContactsListTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_returns_registrar_contacts + get repp_v1_contacts_path, headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal @user.registrar.contacts.count, json[:total_number_of_records] + assert_equal @user.registrar.contacts.count, json[:contacts].length + + assert json[:contacts][0].is_a? String + end + + + def test_returns_detailed_registrar_contacts + get repp_v1_contacts_path(details: true), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal @user.registrar.contacts.count, json[:total_number_of_records] + assert_equal @user.registrar.contacts.count, json[:contacts].length + + assert json[:contacts][0].is_a? Hash + end + + def test_respects_limit + get repp_v1_contacts_path(details: true, limit: 2), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal 2, json[:contacts].length + end + + def test_respects_offset + offset = 1 + get repp_v1_contacts_path(details: true, offset: offset), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal (@user.registrar.contacts.count - offset), json[:contacts].length + end +end diff --git a/test/integration/repp/v1/contacts/show_test.rb b/test/integration/repp/v1/contacts/show_test.rb new file mode 100644 index 000000000..4a6f5b615 --- /dev/null +++ b/test/integration/repp/v1/contacts/show_test.rb @@ -0,0 +1,45 @@ +require 'test_helper' + +class ReppV1ContactsShowTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_returns_error_when_not_found + get repp_v1_contact_path(id: 'definitelynotexistant'), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + assert_equal 2303, json[:code] + assert_equal 'Object does not exist', json[:message] + end + + def test_shows_existing_contact + contact = @user.registrar.contacts.first + + get repp_v1_contact_path(id: contact.code), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + assert_equal contact.code, json[:data][:id] + end + + def test_can_not_access_out_of_scope_contacts + # Contact of registrar goodnames, we're using bestnames API credentials + contact = contacts(:jack) + + get repp_v1_contact_path(id: contact.code), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + assert_equal 2303, json[:code] + assert_equal 'Object does not exist', json[:message] + end +end diff --git a/test/integration/repp/v1/contacts/update_test.rb b/test/integration/repp/v1/contacts/update_test.rb new file mode 100644 index 000000000..cf27f98da --- /dev/null +++ b/test/integration/repp/v1/contacts/update_test.rb @@ -0,0 +1,119 @@ +require 'test_helper' + +class ReppV1ContactsUpdateTest < ActionDispatch::IntegrationTest + def setup + @contact = contacts(:john) + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_updates_contact + request_body = { + "contact": { + "email": "donaldtrump@yandex.ru" + } + } + + put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + contact = Contact.find_by(code: json[:data][:contact][:id]) + assert contact.present? + + assert_equal(request_body[:contact][:email], contact.email) + end + + def test_removes_postal_info_when_updated + request_body = { + "contact": { + "addr": { + "city": "Tallinn", + "street": "Wismari 13", + "zip": "12345", + "country_code": "EE" + } + } + } + + put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1100, json[:code] + assert_equal 'Command completed successfully; Postal address data discarded', json[:message] + + contact = Contact.find_by(code: json[:data][:contact][:id]) + assert contact.present? + + assert_nil contact.city + assert_nil contact.street + assert_nil contact.zip + assert_nil contact.country_code + end + + def test_can_not_change_ident_code + request_body = { + "contact": { + "name": "Donald Trumpster", + "ident": { + "ident_type": "priv", + "ident_country_code": "US", + "ident": "12345" + } + } + } + + put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + @contact.reload + assert_not @contact.ident == 12345 + assert_response :bad_request + assert_equal 2308, json[:code] + assert json[:message].include? 'Ident update is not allowed. Consider creating new contact object' + end + + def test_attaches_legaldoc_if_present + request_body = { + "contact": { + "email": "donaldtrump@yandex.ru" + }, + "legal_document": { + "type": "pdf", + "body": "#{'test' * 2000}" + } + } + + put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + @contact.reload + assert @contact.legal_documents.any? + end + + def test_returns_error_if_ident_wrong_format + request_body = { + "contact": { + "ident": "123" + } + } + + put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2308, json[:code] + assert_equal 'Ident update is not allowed. Consider creating new contact object', json[:message] + end +end diff --git a/test/integration/repp/v1/domains/bulk_renew_test.rb b/test/integration/repp/v1/domains/bulk_renew_test.rb new file mode 100644 index 000000000..4cec91914 --- /dev/null +++ b/test/integration/repp/v1/domains/bulk_renew_test.rb @@ -0,0 +1,91 @@ +require 'test_helper' + +class ReppV1DomainsBulkRenewTest < ActionDispatch::IntegrationTest + def setup + travel_to Time.zone.parse('2010-07-05 10:30') + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_renews_domains + payload = { + "domains": [ + 'shop.test', + 'airport.test', + 'library.test' + ], + "renew_period": "1y" + } + + assert_changes -> { Domain.find_by(name: 'shop.test').valid_to } do + assert_changes -> { Domain.find_by(name: 'airport.test').valid_to } do + assert_changes -> { Domain.find_by(name: 'library.test').valid_to } do + post "/repp/v1/domains/renew/bulk", headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + assert json[:data][:updated_domains].include? 'shop.test' + assert json[:data][:updated_domains].include? 'airport.test' + assert json[:data][:updated_domains].include? 'library.test' + end + end + end + end + + def test_throws_error_when_domain_not_renewable + payload = { + "domains": [ + 'invalid.test', + ], + "renew_period": "1y" + } + assert_no_changes -> { Domain.find_by(name: 'invalid.test').valid_to } do + post "/repp/v1/domains/renew/bulk", headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2002, json[:code] + assert_equal 'Domain renew error for invalid.test', json[:message] + end + end + + def test_throws_error_when_not_enough_balance + billing_prices(:renew_one_year).update(price_cents: 99999999) + payload = { + "domains": [ + 'invalid.test', + ], + "renew_period": "1y" + } + + assert_no_changes -> { Domain.find_by(name: 'invalid.test').valid_to } do + post "/repp/v1/domains/renew/bulk", headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2002, json[:code] + assert_equal 'Not enough funds for renew domains', json[:message] + end + end + + def test_throws_error_if_invalid_renew_period + payload = { + "domains": [ + 'shop.test' + ], + "renew_period": "nope" + } + + post "/repp/v1/domains/renew/bulk", headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2005, json[:code] + assert_equal 'Invalid renew period', json[:message] + end +end diff --git a/test/integration/repp/v1/domains/contact_replacement_test.rb b/test/integration/repp/v1/domains/contact_replacement_test.rb new file mode 100644 index 000000000..3cbd9eb8e --- /dev/null +++ b/test/integration/repp/v1/domains/contact_replacement_test.rb @@ -0,0 +1,65 @@ +require 'test_helper' + +class ReppV1DomainsContactReplacementTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_replaces_tech_contact_with_new_one + replaceable_contact = contacts(:william) + replacing_contact = contacts(:jane) + + payload = { + "current_contact_id": replaceable_contact.code, + "new_contact_id": replacing_contact.code + } + + patch '/repp/v1/domains/contacts', headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + assert json[:data][:affected_domains].include? 'airport.test' + assert json[:data][:affected_domains].include? 'shop.test' + + assert_empty json[:data][:skipped_domains] + end + + def test_tech_contact_id_must_differ + replaceable_contact = contacts(:william) + replacing_contact = contacts(:william) + + payload = { + "current_contact_id": replaceable_contact.code, + "new_contact_id": replacing_contact.code + } + + patch '/repp/v1/domains/contacts', headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2304, json[:code] + assert_equal 'New contact must be different from current', json[:message] + end + + def test_contact_codes_must_be_valid + payload = { + "current_contact_id": 'dfgsdfg', + "new_contact_id": 'vvv' + } + + patch '/repp/v1/domains/contacts', headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + assert_equal 2303, json[:code] + assert_equal 'Object does not exist', json[:message] + end + +end diff --git a/test/integration/repp/v1/domains/list_test.rb b/test/integration/repp/v1/domains/list_test.rb new file mode 100644 index 000000000..bee390e5c --- /dev/null +++ b/test/integration/repp/v1/domains/list_test.rb @@ -0,0 +1,54 @@ +require 'test_helper' + +class ReppV1DomainsListTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_returns_registrar_domains + get repp_v1_domains_path, headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal @user.registrar.domains.count, json[:data][:total_number_of_records] + assert_equal @user.registrar.domains.count, json[:data][:domains].length + + assert json[:data][:domains][0].is_a? String + end + + def test_returns_detailed_registrar_domains + get repp_v1_domains_path(details: true), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal @user.registrar.domains.count, json[:data][:total_number_of_records] + assert_equal @user.registrar.domains.count, json[:data][:domains].length + + assert json[:data][:domains][0].is_a? Hash + end + + def test_respects_limit + get repp_v1_domains_path(details: true, limit: 2), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal 2, json[:data][:domains].length + end + + def test_respects_offset + offset = 1 + get repp_v1_domains_path(details: true, offset: offset), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal (@user.registrar.domains.count - offset), json[:data][:domains].length + end +end diff --git a/test/integration/repp/v1/domains/transfer_info_test.rb b/test/integration/repp/v1/domains/transfer_info_test.rb new file mode 100644 index 000000000..f57675b06 --- /dev/null +++ b/test/integration/repp/v1/domains/transfer_info_test.rb @@ -0,0 +1,40 @@ +require 'test_helper' + +class ReppV1DomainsTransferInfoTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + @domain = domains(:shop) + @auth_headers = { 'Authorization' => token } + end + + def test_can_query_domain_info + headers = @auth_headers + headers['Auth-Code'] = @domain.transfer_code + + get "/repp/v1/domains/#{@domain.name}/transfer_info", headers: headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + assert_equal @domain.name, json[:data][:domain] + assert json[:data][:registrant].present? + assert json[:data][:admin_contacts].present? + assert json[:data][:tech_contacts].present? + end + + def test_respects_domain_authorization_code + headers = @auth_headers + headers['Auth-Code'] = 'jhfgifhdg' + + get "/repp/v1/domains/#{@domain.name}/transfer_info", headers: headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2202, json[:code] + assert_equal 'Authorization error', json[:message] + assert_empty json[:data] + end +end diff --git a/test/integration/repp/v1/domains/transfer_test.rb b/test/integration/repp/v1/domains/transfer_test.rb new file mode 100644 index 000000000..46d5c30c4 --- /dev/null +++ b/test/integration/repp/v1/domains/transfer_test.rb @@ -0,0 +1,127 @@ +require 'test_helper' + +class ReppV1DomainsTransferTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + @domain = domains(:hospital) + + @auth_headers = { 'Authorization' => token } + end + + def test_transfers_domain + payload = { + "data": { + "domain_transfers": [ + { "domain_name": @domain.name, "transfer_code": @domain.transfer_code } + ] + } + } + post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + assert_equal @domain.name, json[:data][:success][0][:domain_name] + + @domain.reload + + assert @domain.registrar = @user.registrar + end + + def test_does_not_transfer_domain_if_not_transferable + @domain.schedule_force_delete(type: :fast_track) + + payload = { + "data": { + "domain_transfers": [ + { "domain_name": @domain.name, "transfer_code": @domain.transfer_code } + ] + } + } + + post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + assert_equal 'Object status prohibits operation', json[:data][:failed][0][:errors][0][:msg] + + @domain.reload + + assert_not @domain.registrar == @user.registrar + end + + def test_does_not_transfer_domain_with_invalid_auth_code + payload = { + "data": { + "domain_transfers": [ + { "domain_name": @domain.name, "transfer_code": "sdfgsdfg" } + ] + } + } + post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + assert_equal "Invalid authorization information", json[:data][:failed][0][:errors][0][:msg] + end + + def test_does_not_transfer_domain_to_same_registrar + @domain.update!(registrar: @user.registrar) + + payload = { + "data": { + "domain_transfers": [ + { "domain_name": @domain.name, "transfer_code": @domain.transfer_code } + ] + } + } + + post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + assert_equal 'Domain already belongs to the querying registrar', json[:data][:failed][0][:errors][0][:msg] + + @domain.reload + + assert @domain.registrar == @user.registrar + end + + def test_does_not_transfer_domain_if_discarded + @domain.update!(statuses: [DomainStatus::DELETE_CANDIDATE]) + + payload = { + "data": { + "domain_transfers": [ + { "domain_name": @domain.name, "transfer_code": @domain.transfer_code } + ] + } + } + + post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + assert_equal 'Object is not eligible for transfer', json[:data][:failed][0][:errors][0][:msg] + + @domain.reload + + assert_not @domain.registrar == @user.registrar + end +end diff --git a/test/integration/repp/v1/registrar/nameservers_test.rb b/test/integration/repp/v1/registrar/nameservers_test.rb new file mode 100644 index 000000000..f01769dfb --- /dev/null +++ b/test/integration/repp/v1/registrar/nameservers_test.rb @@ -0,0 +1,77 @@ +require 'test_helper' + +class ReppV1RegistrarNameserversTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_updates_nameserver_values + nameserver = nameservers(:shop_ns1) + payload = { + "data": { + "id": nameserver.hostname, + "type": "nameserver", + "attributes": { + "hostname": "#{nameserver.hostname}.test", + "ipv4": ["1.1.1.1"] + } + } + } + + put '/repp/v1/registrar/nameservers', headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + assert_equal({ hostname: "#{nameserver.hostname}.test", ipv4: ["1.1.1.1"] }, json[:data][:attributes]) + assert_equal({ hostname: "#{nameserver.hostname}.test", ipv4: ["1.1.1.1"] }, json[:data][:attributes]) + assert json[:data][:affected_domains].include? 'airport.test' + assert json[:data][:affected_domains].include? 'shop.test' + end + + def test_nameserver_with_hostname_must_exist + payload = { + "data": { + "id": 'ns.nonexistant.test', + "type": "nameserver", + "attributes": { + "hostname": "ns1.dn.test", + "ipv4": ["1.1.1.1"] + } + } + } + + put '/repp/v1/registrar/nameservers', headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + assert_equal 2303, json[:code] + assert_equal 'Object does not exist', json[:message] + end + + def test_ip_must_be_in_correct_format + nameserver = nameservers(:shop_ns1) + payload = { + "data": { + "id": nameserver.hostname, + "type": "nameserver", + "attributes": { + "hostname": "#{nameserver.hostname}.test", + "ipv6": ["1.1.1.1"] + } + } + } + + put '/repp/v1/registrar/nameservers', headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2005, json[:code] + assert_equal 'IPv6 is invalid [ipv6]', json[:message] + end +end diff --git a/test/integration/repp/retained_domains_test.rb b/test/integration/repp/v1/retained_domains_test.rb similarity index 100% rename from test/integration/repp/retained_domains_test.rb rename to test/integration/repp/v1/retained_domains_test.rb diff --git a/test/interactions/domain_delete_interaction/domain_delete_test.rb b/test/interactions/domain_delete_interaction/domain_delete_test.rb new file mode 100644 index 000000000..9582fae03 --- /dev/null +++ b/test/interactions/domain_delete_interaction/domain_delete_test.rb @@ -0,0 +1,25 @@ +require 'test_helper' + +class DomainDeleteTest < ActiveSupport::TestCase + setup do + @domain = domains(:shop) + end + + def test_discards_domains_with_past_delete_date + @domain.update!(delete_date: '2010-07-04') + travel_to Time.zone.parse('2010-07-05') + + Domains::Delete::DoDelete.run(domain: @domain) + + assert @domain.destroyed? + end + + def test_sends_notification + @domain.update!(delete_date: '2010-07-04') + travel_to Time.zone.parse('2010-07-05') + + assert_difference '@domain.registrar.notifications.count', 1 do + Domains::Delete::DoDelete.run(domain: @domain) + end + end +end diff --git a/test/interactions/expire_period/start_test.rb b/test/interactions/expire_period/start_test.rb new file mode 100644 index 000000000..168b255bb --- /dev/null +++ b/test/interactions/expire_period/start_test.rb @@ -0,0 +1,28 @@ +require 'test_helper' + +class StartTest < ActiveSupport::TestCase + include ActionMailer::TestHelper + + setup do + @domain = domains(:shop) + @domain.update(expire_time: Time.zone.now - 1.day) + ActionMailer::Base.deliveries.clear + end + + def test_sets_expired + job_count = lambda do + QueJob.where("args->>0 = '#{@domain.id}'", job_class: DomainExpireEmailJob.name).count + end + + assert_difference job_count, 1 do + perform_enqueued_jobs do + DomainCron.start_expire_period + end + end + + @domain.reload + assert @domain.statuses.include?(DomainStatus::EXPIRED) + assert_equal @domain.outzone_at, @domain.expire_time + Domain.expire_warning_period + assert_equal @domain.delete_date, (@domain.outzone_at + Domain.redemption_grace_period).to_date + end +end diff --git a/test/interactions/redemption_grace_period/start_test.rb b/test/interactions/redemption_grace_period/start_test.rb new file mode 100644 index 000000000..8958030f0 --- /dev/null +++ b/test/interactions/redemption_grace_period/start_test.rb @@ -0,0 +1,25 @@ +require 'test_helper' + +class StartTest < ActiveSupport::TestCase + + setup do + @domain = domains(:shop) + @domain.update(outzone_time: Time.zone.now - 1.day) + end + + def test_sets_server_hold + DomainCron.start_redemption_grace_period + + @domain.reload + assert @domain.statuses.include?(DomainStatus::SERVER_HOLD) + end + + def test_doesnt_sets_server_hold_if_not_outzone + @domain.update(outzone_time: nil) + @domain.reload + DomainCron.start_redemption_grace_period + + @domain.reload + assert_not @domain.statuses.include?(DomainStatus::SERVER_HOLD) + end +end diff --git a/test/jobs/domain_delete_confirm_job_test.rb b/test/jobs/domain_delete_confirm_job_test.rb index b999bd3c7..cbe4b87f1 100644 --- a/test/jobs/domain_delete_confirm_job_test.rb +++ b/test/jobs/domain_delete_confirm_job_test.rb @@ -18,7 +18,7 @@ class DomainDeleteConfirmJobTest < ActiveSupport::TestCase new_registrant_email: @new_registrant.email, current_user_id: @user.id }) - DomainDeleteConfirmJob.enqueue(@domain.id, RegistrantVerification::REJECTED) + DomainDeleteConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) last_registrar_notification = @domain.registrar.notifications.last assert_equal(last_registrar_notification.attached_obj_id, @domain.id) @@ -31,7 +31,7 @@ class DomainDeleteConfirmJobTest < ActiveSupport::TestCase new_registrant_email: @new_registrant.email, current_user_id: @user.id }) - DomainDeleteConfirmJob.enqueue(@domain.id, RegistrantVerification::CONFIRMED) + DomainDeleteConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) last_registrar_notification = @domain.registrar.notifications.last assert_equal(last_registrar_notification.attached_obj_id, @domain.id) @@ -51,7 +51,7 @@ class DomainDeleteConfirmJobTest < ActiveSupport::TestCase assert @domain.registrant_delete_confirmable?(@domain.registrant_verification_token) assert_equal @user.id, @domain.pending_json['current_user_id'] - DomainDeleteConfirmJob.enqueue(@domain.id, RegistrantVerification::CONFIRMED) + DomainDeleteConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) @domain.reload assert @domain.statuses.include? DomainStatus::PENDING_DELETE @@ -72,7 +72,7 @@ class DomainDeleteConfirmJobTest < ActiveSupport::TestCase assert @domain.registrant_delete_confirmable?(@domain.registrant_verification_token) assert_equal @user.id, @domain.pending_json['current_user_id'] - DomainDeleteConfirmJob.enqueue(@domain.id, RegistrantVerification::REJECTED) + DomainDeleteConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) @domain.reload assert_equal ['ok'], @domain.statuses diff --git a/test/jobs/domain_update_confirm_job_test.rb b/test/jobs/domain_update_confirm_job_test.rb index 9cca81eb7..ded0d3d8a 100644 --- a/test/jobs/domain_update_confirm_job_test.rb +++ b/test/jobs/domain_update_confirm_job_test.rb @@ -20,7 +20,7 @@ class DomainUpdateConfirmJobTest < ActiveSupport::TestCase end def test_rejected_registrant_verification_notifies_registrar - DomainUpdateConfirmJob.enqueue(@domain.id, RegistrantVerification::REJECTED) + DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) last_registrar_notification = @domain.registrar.notifications.last assert_equal(last_registrar_notification.attached_obj_id, @domain.id) @@ -28,7 +28,7 @@ class DomainUpdateConfirmJobTest < ActiveSupport::TestCase end def test_accepted_registrant_verification_notifies_registrar - DomainUpdateConfirmJob.enqueue(@domain.id, RegistrantVerification::CONFIRMED) + DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) last_registrar_notification = @domain.registrar.notifications.last assert_equal(last_registrar_notification.attached_obj_id, @domain.id) @@ -44,10 +44,11 @@ class DomainUpdateConfirmJobTest < ActiveSupport::TestCase @domain.update(pending_json: @domain.pending_json) @domain.reload - DomainUpdateConfirmJob.enqueue(@domain.id, RegistrantVerification::CONFIRMED) + DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) @domain.reload assert_equal @domain.registrant.code, @new_registrant.code + assert @domain.statuses.include? DomainStatus::OK end def test_clears_pending_update_after_denial @@ -58,10 +59,84 @@ class DomainUpdateConfirmJobTest < ActiveSupport::TestCase @domain.pending_json['frame'] = epp_xml @domain.update(pending_json: @domain.pending_json) - DomainUpdateConfirmJob.enqueue(@domain.id, RegistrantVerification::REJECTED) + DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) @domain.reload assert_not @domain.statuses.include? DomainStatus::PENDING_DELETE_CONFIRMATION assert_not @domain.statuses.include? DomainStatus::PENDING_DELETE end + + def test_protects_statuses_after_denial + epp_xml = "\n\n \n \n \n #{@domain.name}\n" \ + " \n #{@new_registrant.code}\n \n \n \n \n \n" \ + " \n #{@legal_doc_path}\n \n" \ + " \n 20alla-1594199756\n \n\n" + @domain.pending_json['frame'] = epp_xml + @domain.update(pending_json: @domain.pending_json) + @domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED]) + + DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) + @domain.reload + + assert_not @domain.statuses.include? DomainStatus::PENDING_DELETE_CONFIRMATION + assert_not @domain.statuses.include? DomainStatus::PENDING_DELETE + assert @domain.statuses.include? DomainStatus::DELETE_CANDIDATE + assert @domain.statuses.include? DomainStatus::DISPUTED + end + + def test_protects_statuses_after_confirm + epp_xml = "\n\n \n \n \n #{@domain.name}\n" \ + " \n #{@new_registrant.code}\n \n \n \n \n \n" \ + " \n #{@legal_doc_path}\n \n" \ + " \n 20alla-1594199756\n \n\n" + @domain.pending_json['frame'] = epp_xml + @domain.update(pending_json: @domain.pending_json) + @domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED]) + + DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) + @domain.reload + + assert_not @domain.statuses.include? DomainStatus::PENDING_DELETE_CONFIRMATION + assert_not @domain.statuses.include? DomainStatus::PENDING_DELETE + assert @domain.statuses.include? DomainStatus::DELETE_CANDIDATE + assert @domain.statuses.include? DomainStatus::DISPUTED + end + + def test_clears_pending_update_and_inactive_after_denial + epp_xml = "\n\n \n \n \n #{@domain.name}\n" \ + " \n #{@new_registrant.code}\n \n \n \n \n \n" \ + " \n #{@legal_doc_path}\n \n" \ + " \n 20alla-1594199756\n \n\n" + @domain.pending_json['frame'] = epp_xml + @domain.update(pending_json: @domain.pending_json) + @domain.update(statuses: [DomainStatus::PENDING_UPDATE]) + @domain.nameservers.destroy_all + @domain.reload + + DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) + @domain.reload + + assert_not @domain.statuses.include? DomainStatus::PENDING_DELETE_CONFIRMATION + assert_not @domain.statuses.include? DomainStatus::PENDING_DELETE + assert_not @domain.statuses.include? DomainStatus::PENDING_UPDATE + assert @domain.statuses.include? DomainStatus::INACTIVE + end + + def test_clears_pending_update_and_sets_ok_after_denial + epp_xml = "\n\n \n \n \n #{@domain.name}\n" \ + " \n #{@new_registrant.code}\n \n \n \n \n \n" \ + " \n #{@legal_doc_path}\n \n" \ + " \n 20alla-1594199756\n \n\n" + @domain.pending_json['frame'] = epp_xml + @domain.update(pending_json: @domain.pending_json) + @domain.update(statuses: [DomainStatus::OK, DomainStatus::PENDING_UPDATE]) + + DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) + @domain.reload + + assert_not @domain.statuses.include? DomainStatus::PENDING_DELETE_CONFIRMATION + assert_not @domain.statuses.include? DomainStatus::PENDING_DELETE + assert_not @domain.statuses.include? DomainStatus::PENDING_UPDATE + assert @domain.statuses.include? DomainStatus::OK + end end diff --git a/test/lib/serializers/registrant_api/contact_test.rb b/test/lib/serializers/registrant_api/contact_test.rb index 165c91e00..8b84abd38 100644 --- a/test/lib/serializers/registrant_api/contact_test.rb +++ b/test/lib/serializers/registrant_api/contact_test.rb @@ -4,7 +4,7 @@ require 'serializers/registrant_api/contact' class SerializersRegistrantApiContactTest < ActiveSupport::TestCase def setup @contact = contacts(:william) - @serializer = Serializers::RegistrantApi::Contact.new(@contact) + @serializer = Serializers::RegistrantApi::Contact.new(@contact, false) @json = @serializer.to_json end diff --git a/test/models/bounced_mail_address_test.rb b/test/models/bounced_mail_address_test.rb new file mode 100644 index 000000000..4af401711 --- /dev/null +++ b/test/models/bounced_mail_address_test.rb @@ -0,0 +1,104 @@ +require 'test_helper' + +class BouncedMailAddressTest < ActiveSupport::TestCase + include ActionMailer::TestHelper + + def setup + @bounced_mail = BouncedMailAddress.new + @bounced_mail.email = 'recipient@registry.test' + @bounced_mail.message_id = '010f0174a0c7d348-ea6e2fc1-0854-4073-b71f-5cecf9b0d0b2-000000' + @bounced_mail.bounce_type = 'Permanent' + @bounced_mail.bounce_subtype = 'General' + @bounced_mail.action = 'failed' + @bounced_mail.status = '5.1.1' + @bounced_mail.diagnostic = 'smtp; 550 5.1.1 user unknown' + end + + def test_email_is_required + assert @bounced_mail.valid? + @bounced_mail.email = nil + assert @bounced_mail.invalid? + end + + def test_message_id_is_required + assert @bounced_mail.valid? + @bounced_mail.message_id = nil + assert @bounced_mail.invalid? + end + + def test_bounce_type_is_required + assert @bounced_mail.valid? + @bounced_mail.bounce_type = nil + assert @bounced_mail.invalid? + end + + def test_bounce_subtype_is_required + assert @bounced_mail.valid? + @bounced_mail.bounce_subtype = nil + assert @bounced_mail.invalid? + end + + def test_action_is_required + assert @bounced_mail.valid? + @bounced_mail.action = nil + assert @bounced_mail.invalid? + end + + def test_status_is_required + assert @bounced_mail.valid? + @bounced_mail.status = nil + assert @bounced_mail.invalid? + end + + def test_diagnostic_is_not_required + assert @bounced_mail.valid? + @bounced_mail.diagnostic = nil + assert @bounced_mail.valid? + end + + def test_bounce_reason_is_determined_dynamically + assert @bounced_mail.valid? + assert_equal 'failed (5.1.1 smtp; 550 5.1.1 user unknown)', @bounced_mail.bounce_reason + end + + def test_creates_objects_from_sns_json + BouncedMailAddress.record(sns_bounce_payload) + + bounced_mail = BouncedMailAddress.last + assert_equal domains(:shop).registrant.email, bounced_mail.email + assert_equal 'failed', bounced_mail.action + assert_equal '5.1.1', bounced_mail.status + assert_equal 'smtp; 550 5.1.1 user unknown', bounced_mail.diagnostic + end + + def sns_bounce_payload + { + "notificationType": "Bounce", + "mail": { + "source": "noreply@registry.test", + "sourceIp": "195.43.86.5", + "messageId": "010f0174a0c7d348-ea6e2fc1-0854-4073-b71f-5cecf9b0d0b2-000000", + "sourceArn": "arn:aws:ses:us-east-2:65026820000:identity/noreply@registry.test", + "timestamp": "2020-09-18T10:34:44.000Z", + "destination": [ "#{domains(:shop).registrant.email}" ], + "sendingAccountId": "650268220000" + }, + "bounce": { + "timestamp": "2020-09-18T10:34:44.911Z", + "bounceType": "Permanent", + "feedbackId": "010f0174a0c7d4f9-27d59756-6111-4d5f-xxxx-26bee0d55fa2-000000", + "remoteMtaIp": "127.0.01", + "reportingMTA": "dsn; xxx.amazonses.com", + "bounceSubType": "General", + "bouncedRecipients": [ + { + "action": "failed", + "status": "5.1.1", + "emailAddress": "#{domains(:shop).registrant.email}", + "diagnosticCode": "smtp; 550 5.1.1 user unknown" + } + ] + } + }.as_json + end +end diff --git a/test/models/domain/force_delete_test.rb b/test/models/domain/force_delete_test.rb index f0723c326..2bb1b5b8f 100644 --- a/test/models/domain/force_delete_test.rb +++ b/test/models/domain/force_delete_test.rb @@ -1,18 +1,20 @@ require 'test_helper' -class NewDomainForceDeleteTest < ActiveSupport::TestCase +class ForceDeleteTest < ActionMailer::TestCase setup do @domain = domains(:shop) Setting.redemption_grace_period = 30 + ActionMailer::Base.deliveries.clear end def test_schedules_force_delete_fast_track assert_not @domain.force_delete_scheduled? travel_to Time.zone.parse('2010-07-05') - @domain.schedule_force_delete(type: :fast_track) + @domain.schedule_force_delete(type: :fast_track, notify_by_email: true) @domain.reload + assert_emails 1 assert @domain.force_delete_scheduled? assert_equal Date.parse('2010-08-20'), @domain.force_delete_date.to_date assert_equal Date.parse('2010-07-06'), @domain.force_delete_start.to_date @@ -111,9 +113,12 @@ class NewDomainForceDeleteTest < ActiveSupport::TestCase def test_force_delete_cannot_be_scheduled_when_a_domain_is_discarded @domain.update!(statuses: [DomainStatus::DELETE_CANDIDATE]) - assert_raises StandardError do - @domain.schedule_force_delete(type: :fast_track) - end + result = Domains::ForceDelete::SetForceDelete.run(domain: @domain, type: :fast_track) + + assert_not result.valid? + assert_not @domain.force_delete_scheduled? + message = ["Force delete procedure cannot be scheduled while a domain is discarded"] + assert_equal message, result.errors.messages[:domain] end def test_cancels_force_delete @@ -201,9 +206,10 @@ class NewDomainForceDeleteTest < ActiveSupport::TestCase @domain.schedule_force_delete(type: :soft) travel_to Time.zone.parse('2010-08-21') - DomainCron.start_client_hold + Domains::ClientHold::SetClientHold.run! @domain.reload + assert_emails 1 assert_equal(@domain.purge_date.to_date, @domain.force_delete_date.to_date) assert_equal(@domain.outzone_date.to_date, @domain.force_delete_start.to_date + Setting.expire_warning_period.days) @@ -221,8 +227,10 @@ class NewDomainForceDeleteTest < ActiveSupport::TestCase @domain.schedule_force_delete(type: :soft) travel_to Time.zone.parse('2010-08-21') - DomainCron.start_client_hold + Domains::ClientHold::SetClientHold.run! @domain.reload + + assert_emails 1 assert_includes(@domain.statuses, asserted_status) end @@ -236,7 +244,7 @@ class NewDomainForceDeleteTest < ActiveSupport::TestCase @domain.schedule_force_delete(type: :soft) travel_to Time.zone.parse('2010-07-06') - DomainCron.start_client_hold + Domains::ClientHold::SetClientHold.run! @domain.reload assert_not_includes(@domain.statuses, asserted_status) @@ -251,7 +259,7 @@ class NewDomainForceDeleteTest < ActiveSupport::TestCase @domain.schedule_force_delete(type: :fast_track) travel_to Time.zone.parse('2010-07-25') - DomainCron.start_client_hold + Domains::ClientHold::SetClientHold.run! @domain.reload assert_includes(@domain.statuses, asserted_status) @@ -267,7 +275,7 @@ class NewDomainForceDeleteTest < ActiveSupport::TestCase @domain.schedule_force_delete(type: :fast_track) travel_to Time.zone.parse('2010-07-06') - DomainCron.start_client_hold + Domains::ClientHold::SetClientHold.run! @domain.reload assert_not_includes(@domain.statuses, asserted_status) diff --git a/test/models/domain_cron_test.rb b/test/models/domain_cron_test.rb index 0224b1a61..c417df04f 100644 --- a/test/models/domain_cron_test.rb +++ b/test/models/domain_cron_test.rb @@ -19,7 +19,9 @@ class DomainCronTest < ActiveSupport::TestCase registrant_verification_token: 'test', statuses: [DomainStatus::PENDING_DELETE_CONFIRMATION]) - DomainCron.clean_expired_pendings + perform_enqueued_jobs do + DomainCron.clean_expired_pendings + end assert_emails 1 end @@ -84,7 +86,9 @@ class DomainCronTest < ActiveSupport::TestCase assert @domain.pending_update? @domain.reload - DomainCron.clean_expired_pendings + perform_enqueued_jobs do + DomainCron.clean_expired_pendings + end @domain.reload assert_not @domain.pending_update? diff --git a/test/models/domain_test.rb b/test/models/domain_test.rb index 4a9240f57..ae12f4a1e 100644 --- a/test/models/domain_test.rb +++ b/test/models/domain_test.rb @@ -414,7 +414,7 @@ class DomainTest < ActiveSupport::TestCase force_delete_date: nil) @domain.update(template_name: 'legal_person') travel_to Time.zone.parse('2010-07-05') - @domain.schedule_force_delete(type: :fast_track) + Domains::ForceDelete::SetForceDelete.run!(domain: @domain, type: :fast_track) assert(@domain.force_delete_scheduled?) other_registrant = Registrant.find_by(code: 'jane-001') @domain.pending_json['new_registrant_id'] = other_registrant.id diff --git a/test/models/whois/record_test.rb b/test/models/whois/record_test.rb index e900a4965..d06b23cae 100644 --- a/test/models/whois/record_test.rb +++ b/test/models/whois/record_test.rb @@ -70,6 +70,6 @@ class Whois::RecordTest < ActiveSupport::TestCase end def registration_deadline - Time.zone.now + 10.days + @registration_deadline ||= Time.zone.now + 10.days end end diff --git a/test/system/admin_area/bounced_mail_addresses_test.rb b/test/system/admin_area/bounced_mail_addresses_test.rb new file mode 100644 index 000000000..36b81f0f8 --- /dev/null +++ b/test/system/admin_area/bounced_mail_addresses_test.rb @@ -0,0 +1,40 @@ +require 'application_system_test_case' + +class AdminBouncedMailAddressesTest < ApplicationSystemTestCase + include ActionView::Helpers::NumberHelper + + def setup + @bounced_mail = bounced_mail_addresses(:one) + @original_default_language = Setting.default_language + sign_in users(:admin) + end + + def teardown + Setting.default_language = @original_default_language + end + + def test_shows_bounced_emails + visit admin_bounced_mail_addresses_path + assert_text @bounced_mail.status + assert_text @bounced_mail.action + assert_text @bounced_mail.diagnostic + assert_text @bounced_mail.email + end + + def test_shows_detailed_bounced_email + visit admin_bounced_mail_address_path(@bounced_mail) + assert_text @bounced_mail.status + assert_text @bounced_mail.action + assert_text @bounced_mail.diagnostic + assert_text @bounced_mail.email + + assert_text @bounced_mail.message_id + end + + def test_deletes_registrar + visit admin_bounced_mail_address_path(@bounced_mail) + click_on 'Destroy' + + assert_text 'Bounced mail address was successfully destroyed.' + end +end diff --git a/test/system/registrant_area/domains/domain_delete_confirms_test.rb b/test/system/registrant_area/domains/domain_delete_confirms_test.rb index 0eb61ada8..765cd0149 100644 --- a/test/system/registrant_area/domains/domain_delete_confirms_test.rb +++ b/test/system/registrant_area/domains/domain_delete_confirms_test.rb @@ -1,6 +1,7 @@ require 'application_system_test_case' class DomainDeleteConfirmsTest < ApplicationSystemTestCase + include ActionMailer::TestHelper setup do @user = users(:registrant) sign_in @user @@ -13,7 +14,9 @@ class DomainDeleteConfirmsTest < ApplicationSystemTestCase def test_enqueues_approve_job_after_verification visit registrant_domain_delete_confirm_url(@domain.id, token: @domain.registrant_verification_token) - click_on 'Confirm domain delete' + perform_enqueued_jobs do + click_on 'Confirm domain delete' + end assert_text 'Domain registrant change has successfully received.' @domain.reload @@ -23,7 +26,9 @@ class DomainDeleteConfirmsTest < ApplicationSystemTestCase def test_enqueues_reject_job_after_verification visit registrant_domain_delete_confirm_url(@domain.id, token: @domain.registrant_verification_token) - click_on 'Reject domain delete' + perform_enqueued_jobs do + click_on 'Reject domain delete' + end assert_text 'Domain registrant change has been rejected successfully.' @domain.reload diff --git a/test/system/registrar_area/base_test.rb b/test/system/registrar_area/base_test.rb index 67b19a044..2529b009a 100644 --- a/test/system/registrar_area/base_test.rb +++ b/test/system/registrar_area/base_test.rb @@ -29,6 +29,16 @@ class RegistrarAreaBaseTestTest < ApplicationSystemTestCase assert_button 'Login' end + def test_user_can_access_when_ip_is_whitelisted_with_subnet + white_ips(:one).update!(ipv4: '127.0.0.1/32', interfaces: [WhiteIp::REGISTRAR]) + Setting.registrar_ip_whitelist_enabled = true + + visit new_registrar_user_session_url + + assert_no_text 'Access denied from IP 127.0.0.1' + assert_button 'Login' + end + def test_user_can_access_when_ip_is_not_whitelisted_and_whitelist_is_disabled Setting.registrar_ip_whitelist_enabled = false WhiteIp.delete_all diff --git a/test/system/registrar_area/bulk_change/bulk_renew_test.rb b/test/system/registrar_area/bulk_change/bulk_renew_test.rb new file mode 100644 index 000000000..5e1704afd --- /dev/null +++ b/test/system/registrar_area/bulk_change/bulk_renew_test.rb @@ -0,0 +1,70 @@ +require 'application_system_test_case' + +class BulkRenewTest < ApplicationSystemTestCase + setup do + @registrar = users(:api_bestnames).registrar + @price = billing_prices(:renew_one_year) + end + + def test_shows_domain_list + sign_in users(:api_bestnames) + travel_to Time.zone.parse('2010-07-05 10:30') + + visit new_registrar_bulk_change_url + click_link('Bulk renew') + assert_text 'Current balance' + page.has_css?('#registrar_balance', text: + ApplicationController.helpers.number_to_currency(@registrar.balance)) + + select '1 year', from: 'Period' + click_button 'Filter' + + @registrar.domains.pluck(:name).each do |domain_name| + assert_text domain_name + end + end + + def test_makes_bulk_renew + sign_in users(:api_bestnames) + travel_to Time.zone.parse('2010-07-05 10:30') + + req_body = { domains: ["shop.test", "airport.test", "library.test", "invalid.test"], renew_period: "1y" } + stub_request(:post, "#{ENV['repp_url']}domains/renew/bulk").with(body: req_body) + .to_return(status: 400, body: { + code: 2304, + message: "Domain renew error for invalid.test", + data: {} + }.to_json) + + visit new_registrar_bulk_change_url + click_link('Bulk renew') + select '1 year', from: 'Period' + click_button 'Filter' + click_button 'Renew' + + assert_text 'Domain renew error for invalid.test' + end + + def test_bulk_renew_checks_balance + sign_in users(:api_bestnames) + @price.update(price_cents: 99999999) + travel_to Time.zone.parse('2010-07-05 10:30') + + req_body = { domains: ["shop.test", "airport.test", "library.test", "invalid.test"], renew_period: "1y" } + stub_request(:post, "#{ENV['repp_url']}domains/renew/bulk").with(body: req_body) + .to_return(status: 400, body: { + code: 2304, + message: "Not enough funds for renew domains", + data: {} + }.to_json) + + visit new_registrar_bulk_change_url + click_link('Bulk renew') + select '1 year', from: 'Period' + click_button 'Filter' + click_button 'Renew' + + assert_text 'Not enough funds for renew domains' + + end +end diff --git a/test/system/registrar_area/bulk_change/bulk_transfer_test.rb b/test/system/registrar_area/bulk_change/bulk_transfer_test.rb index 69b755499..820b1cf96 100644 --- a/test/system/registrar_area/bulk_change/bulk_transfer_test.rb +++ b/test/system/registrar_area/bulk_change/bulk_transfer_test.rb @@ -6,14 +6,14 @@ class RegistrarAreaBulkTransferTest < ApplicationSystemTestCase end def test_transfer_multiple_domains_in_bulk - request_body = { data: { domainTransfers: [{ domainName: 'shop.test', transferCode: '65078d5' }] } } + request_body = { data: { domain_transfers: [{ domain_name: 'shop.test', transfer_code: '65078d5' }] } } headers = { 'Content-type' => Mime[:json] } - request_stub = stub_request(:post, /domain_transfers/).with(body: request_body, + request_stub = stub_request(:post, /domains\/transfer/).with(body: request_body, headers: headers, basic_auth: ['test_goodnames', 'testtest']) - .to_return(body: { data: [{ - type: 'domain_transfer' - }] }.to_json, status: 200) + .to_return(body: { data: { success: [{ type: 'domain_transfer', domain_name: 'shop.test' }], + failed: [] + } }.to_json, status: 200) visit registrar_domains_url click_link 'Bulk change' @@ -27,9 +27,9 @@ class RegistrarAreaBulkTransferTest < ApplicationSystemTestCase end def test_fail_gracefully - body = { errors: [{ title: 'epic fail' }] }.to_json + body = { message: 'epic fail' }.to_json headers = { 'Content-type' => Mime[:json] } - stub_request(:post, /domain_transfers/).to_return(status: 400, body: body, headers: headers) + stub_request(:post, /domains\/transfer/).to_return(status: 400, body: body, headers: headers) visit registrar_domains_url click_link 'Bulk change' diff --git a/test/system/registrar_area/bulk_change/nameserver_test.rb b/test/system/registrar_area/bulk_change/nameserver_test.rb index 38c2cb9e6..e97b2d1c3 100644 --- a/test/system/registrar_area/bulk_change/nameserver_test.rb +++ b/test/system/registrar_area/bulk_change/nameserver_test.rb @@ -15,10 +15,12 @@ class RegistrarAreaNameserverBulkChangeTest < ApplicationSystemTestCase request_stub = stub_request(:put, /registrar\/nameservers/).with(body: request_body, headers: { 'Content-type' => Mime[:json] }, basic_auth: ['test_goodnames', 'testtest']) - .to_return(body: { data: [{ - type: 'nameserver', - id: 'new-ns.bestnames.test'}], - affected_domains: ["airport.test", "shop.test"]}.to_json, status: 200) + .to_return(body: { data: { + type: 'nameserver', + id: 'new-ns.bestnames.test', + affected_domains: ["airport.test", "shop.test"] + } + }.to_json, status: 200) visit registrar_domains_url click_link 'Bulk change' @@ -38,7 +40,7 @@ class RegistrarAreaNameserverBulkChangeTest < ApplicationSystemTestCase def test_fails_gracefully stub_request(:put, /registrar\/nameservers/).to_return(status: 400, - body: { errors: [{ title: 'epic fail' }] }.to_json, + body: { message: 'epic fail' }.to_json, headers: { 'Content-type' => Mime[:json] }) visit registrar_domains_url @@ -68,9 +70,9 @@ class RegistrarAreaNameserverBulkChangeTest < ApplicationSystemTestCase request_stub = stub_request(:put, /registrar\/nameservers/).with(body: request_body, headers: { 'Content-type' => Mime[:json] }, basic_auth: ['test_goodnames', 'testtest']) - .to_return(body: { data: [{ + .to_return(body: { data: { type: 'nameserver', - id: 'new-ns.bestnames.test'}], + id: 'new-ns.bestnames.test'}, affected_domains: ["shop.test"]}.to_json, status: 200) visit registrar_domains_url diff --git a/test/system/registrar_area/bulk_change/tech_contact_test.rb b/test/system/registrar_area/bulk_change/tech_contact_test.rb index c678e8f34..e08457f60 100644 --- a/test/system/registrar_area/bulk_change/tech_contact_test.rb +++ b/test/system/registrar_area/bulk_change/tech_contact_test.rb @@ -9,8 +9,8 @@ class RegistrarAreaTechContactBulkChangeTest < ApplicationSystemTestCase request_stub = stub_request(:patch, /domains\/contacts/) .with(body: { current_contact_id: 'william-001', new_contact_id: 'john-001' }, basic_auth: ['test_bestnames', 'testtest']) - .to_return(body: { affected_domains: %w[foo.test bar.test], - skipped_domains: %w[baz.test qux.test] }.to_json, + .to_return(body: { data: { affected_domains: %w[foo.test bar.test], + skipped_domains: %w[baz.test qux.test] } }.to_json, status: 200) visit registrar_domains_url @@ -30,7 +30,7 @@ class RegistrarAreaTechContactBulkChangeTest < ApplicationSystemTestCase def test_fails_gracefully stub_request(:patch, /domains\/contacts/) .to_return(status: 400, - body: { error: { message: 'epic fail' } }.to_json, + body: { message: 'epic fail' }.to_json, headers: { 'Content-type' => Mime[:json] }) visit registrar_domains_url diff --git a/test/test_helper.rb b/test/test_helper.rb index 459d4f8f5..a1634f717 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -6,6 +6,7 @@ if ENV['COVERAGE'] add_filter '/lib/core_monkey_patches/' add_filter '/lib/daemons/' add_filter '/lib/gem_monkey_patches/' + add_filter '/lib/tasks/' end end