diff --git a/.codeclimate.yml b/.codeclimate.yml index be8f1df7a..294b9335c 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -27,3 +27,4 @@ exclude_patterns: - "vendor/" - "test/" - "spec/" + - "CHANGELOG.md" diff --git a/app/controllers/admin/domains/registry_lock_controller.rb b/app/controllers/admin/domains/registry_lock_controller.rb new file mode 100644 index 000000000..cd04c5f2a --- /dev/null +++ b/app/controllers/admin/domains/registry_lock_controller.rb @@ -0,0 +1,21 @@ +module Admin + module Domains + class RegistryLockController < BaseController + def destroy + set_domain + authorize! :manage, @domain + if @domain.remove_registry_lock + redirect_to edit_admin_domain_url(@domain), notice: t('.success') + else + redirect_to edit_admin_domain_url(@domain), alert: t('.error') + end + end + + private + + def set_domain + @domain = Domain.find(params[:domain_id]) + end + end + end +end diff --git a/app/controllers/admin/mail_templates_controller.rb b/app/controllers/admin/mail_templates_controller.rb index 93141ade6..d7ec4e6d0 100644 --- a/app/controllers/admin/mail_templates_controller.rb +++ b/app/controllers/admin/mail_templates_controller.rb @@ -47,7 +47,7 @@ module Admin def destroy @mail_template = MailTemplate.find(params[:id]) if @mail_template.destroy - redirect_to admin_mail_templates_path, notise: t(:deleted) + redirect_to admin_mail_templates_path, notice: t(:deleted) else flash.now[:alert] = I18n.t(:failure) render 'show' diff --git a/app/controllers/api/v1/registrant/base_controller.rb b/app/controllers/api/v1/registrant/base_controller.rb index 4df0d226c..4497d68e6 100644 --- a/app/controllers/api/v1/registrant/base_controller.rb +++ b/app/controllers/api/v1/registrant/base_controller.rb @@ -6,6 +6,7 @@ module Api module Registrant class BaseController < ActionController::API before_action :authenticate + before_action :set_paper_trail_whodunnit rescue_from(ActionController::ParameterMissing) do |parameter_missing_exception| error = {} @@ -22,16 +23,32 @@ module Api header.gsub(pattern, '') if header&.match(pattern) end + def associated_domains(user) + country_code, ident = user.registrant_ident.split('-') + + BusinessRegistryCache.fetch_associated_domains(ident, country_code) + rescue Soap::Arireg::NotAvailableError => error + Rails.logger.fatal("[EXCEPTION] #{error}") + user.domains + end + def authenticate decryptor = AuthTokenDecryptor.create_with_defaults(bearer_token) decryptor.decrypt_token if decryptor.valid? - sign_in decryptor.user + sign_in(:registrant_user, decryptor.user) else - render json: { errors: [{base: ['Not authorized']}] }, status: :unauthorized + render json: { errors: [{ base: ['Not authorized'] }] }, + status: :unauthorized end end + + # This controller does not inherit from ApplicationController, + # so user_for_paper_trail method is not usable. + def set_paper_trail_whodunnit + ::PaperTrail.whodunnit = current_registrant_user.id_role_username + end end end end diff --git a/app/controllers/api/v1/registrant/domains_controller.rb b/app/controllers/api/v1/registrant/domains_controller.rb index 97925701a..a416aaec0 100644 --- a/app/controllers/api/v1/registrant/domains_controller.rb +++ b/app/controllers/api/v1/registrant/domains_controller.rb @@ -30,17 +30,6 @@ module Api render json: { errors: [{ base: ['Domain not found'] }] }, status: :not_found end end - - private - - def associated_domains(user) - country_code, ident = user.registrant_ident.split('-') - - BusinessRegistryCache.fetch_associated_domains(ident, country_code) - rescue Soap::Arireg::NotAvailableError => error - Rails.logger.fatal("[EXCEPTION] #{error}") - user.domains - end end end end diff --git a/app/controllers/api/v1/registrant/registry_locks_controller.rb b/app/controllers/api/v1/registrant/registry_locks_controller.rb new file mode 100644 index 000000000..27c98cc9f --- /dev/null +++ b/app/controllers/api/v1/registrant/registry_locks_controller.rb @@ -0,0 +1,48 @@ +module Api + module V1 + module Registrant + class RegistryLocksController < BaseController + before_action :set_domain + before_action :authorized_to_manage_locks? + + def create + if @domain.apply_registry_lock + render json: @domain + else + render json: { errors: [{ base: ['Domain cannot be locked'] }] }, + status: :unprocessable_entity + end + end + + def destroy + if @domain.remove_registry_lock + render json: @domain + else + render json: { errors: [{ base: ['Domain is not locked'] }] }, + status: :unprocessable_entity + end + end + + private + + def set_domain + domain_pool = current_registrant_user.domains + @domain = domain_pool.find_by(uuid: params[:domain_uuid]) + + return if @domain + render json: { errors: [{ base: ['Domain not found'] }] }, + status: :not_found and return + end + + def authorized_to_manage_locks? + return if current_registrant_user.administered_domains.include?(@domain) + + render json: { errors: [ + { base: ['Only administrative contacts can manage registry locks'] } + ] }, + status: :unauthorized and return + end + end + end + end +end diff --git a/app/models/concerns/domain/registry_lockable.rb b/app/models/concerns/domain/registry_lockable.rb new file mode 100644 index 000000000..4a759296d --- /dev/null +++ b/app/models/concerns/domain/registry_lockable.rb @@ -0,0 +1,51 @@ +module Concerns + module Domain + module RegistryLockable + extend ActiveSupport::Concern + + def apply_registry_lock + return unless registry_lockable? + return if locked_by_registrant? + + transaction do + statuses << DomainStatus::SERVER_UPDATE_PROHIBITED + statuses << DomainStatus::SERVER_DELETE_PROHIBITED + statuses << DomainStatus::SERVER_TRANSFER_PROHIBITED + self.locked_by_registrant_at = Time.zone.now + + save! + end + end + + def registry_lockable? + (statuses & [DomainStatus::PENDING_DELETE_CONFIRMATION, + DomainStatus::PENDING_CREATE, DomainStatus::PENDING_UPDATE, + DomainStatus::PENDING_DELETE, DomainStatus::PENDING_RENEW, + DomainStatus::PENDING_TRANSFER, DomainStatus::FORCE_DELETE]).empty? + end + + def locked_by_registrant? + return false unless locked_by_registrant_at + + lock_statuses = [DomainStatus::SERVER_UPDATE_PROHIBITED, + DomainStatus::SERVER_DELETE_PROHIBITED, + DomainStatus::SERVER_TRANSFER_PROHIBITED] + + (statuses & lock_statuses).count == 3 + end + + def remove_registry_lock + return unless locked_by_registrant? + + transaction do + statuses.delete(DomainStatus::SERVER_UPDATE_PROHIBITED) + statuses.delete(DomainStatus::SERVER_DELETE_PROHIBITED) + statuses.delete(DomainStatus::SERVER_TRANSFER_PROHIBITED) + self.locked_by_registrant_at = nil + + save! + end + end + end + end +end diff --git a/app/models/concerns/versions.rb b/app/models/concerns/versions.rb index 53a984179..77bc484ae 100644 --- a/app/models/concerns/versions.rb +++ b/app/models/concerns/versions.rb @@ -37,7 +37,7 @@ module Versions registrar = Registrar.find_by(name: str) user = registrar.api_users.first if registrar - str_match = str.match(/^(\d+)-(ApiUser:|api-|AdminUser:)/) + str_match = str.match(/^(\d+)-(ApiUser:|api-|AdminUser:|RegistrantUser:)/) user ||= User.find_by(id: str_match[1]) if str_match user diff --git a/app/models/domain.rb b/app/models/domain.rb index a7ba227e6..50ab62242 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -7,6 +7,7 @@ class Domain < ActiveRecord::Base include Concerns::Domain::Discardable include Concerns::Domain::Deletable include Concerns::Domain::Transferable + include Concerns::Domain::RegistryLockable has_paper_trail class_name: "DomainVersion", meta: { children: :children_log } diff --git a/app/models/registrant_user.rb b/app/models/registrant_user.rb index f47b924f6..ddbf2e664 100644 --- a/app/models/registrant_user.rb +++ b/app/models/registrant_user.rb @@ -1,5 +1,5 @@ class RegistrantUser < User - ACCEPTED_ISSUER = 'AS Sertifitseerimiskeskus' + ACCEPTED_ISSUER = 'AS Sertifitseerimiskeskus'.freeze attr_accessor :idc_data devise :database_authenticatable, :trackable, :timeoutable @@ -10,16 +10,46 @@ class RegistrantUser < User delegate :can?, :cannot?, to: :ability def ident - registrant_ident.to_s.split("-").last + registrant_ident.to_s.split('-').last end + def country_code + registrant_ident.to_s.split('-').first + end + + # In Rails 5, can be replaced with a much simpler `or` query method and the raw SQL parts can be + # removed. + # https://guides.rubyonrails.org/active_record_querying.html#or-conditions def domains - ident_cc, ident = registrant_ident.to_s.split '-' - Domain.includes(:registrar, :registrant).where(contacts: { - ident_type: 'priv', - ident: ident, #identity_code, - ident_country_code: ident_cc #country_code - }) + domains_where_is_contact = begin + Domain.joins(:domain_contacts) + .where(domain_contacts: { contact_id: contacts }) + end + + domains_where_is_registrant = Domain.where(registrant_id: contacts) + + Domain.from( + "(#{domains_where_is_registrant.to_sql} UNION " \ + "#{domains_where_is_contact.to_sql}) AS domains" + ) + end + + def contacts + Contact.where(ident_type: 'priv', ident: ident, ident_country_code: country_code) + end + + def administered_domains + domains_where_is_administrative_contact = begin + Domain.joins(:domain_contacts) + .where(domain_contacts: { contact_id: contacts, type: [AdminDomainContact] }) + end + + domains_where_is_registrant = Domain.where(registrant_id: contacts) + + Domain.from( + "(#{domains_where_is_registrant.to_sql} UNION " \ + "#{domains_where_is_administrative_contact.to_sql}) AS domains" + ) end def to_s @@ -35,13 +65,13 @@ class RegistrantUser < User user_data = {} # handling here new and old mode - if idc_data.starts_with?("/") + if idc_data.starts_with?('/') user_data[:ident] = idc_data.scan(/serialNumber=(\d+)/).flatten.first user_data[:country_code] = idc_data.scan(/^\/C=(.{2})/).flatten.first user_data[:first_name] = idc_data.scan(%r{/GN=(.+)/serialNumber}).flatten.first user_data[:last_name] = idc_data.scan(%r{/SN=(.+)/GN}).flatten.first else - parse_str = "," + idc_data + parse_str = ',' + idc_data user_data[:ident] = parse_str.scan(/,serialNumber=(\d+)/).flatten.first user_data[:country_code] = parse_str.scan(/,C=(.{2})/).flatten.first user_data[:first_name] = parse_str.scan(/,GN=([^,]+)/).flatten.first diff --git a/app/presenters/domain_presenter.rb b/app/presenters/domain_presenter.rb index 03516af5e..445e5b87d 100644 --- a/app/presenters/domain_presenter.rb +++ b/app/presenters/domain_presenter.rb @@ -14,6 +14,11 @@ class DomainPresenter html += " #{label}" end + if domain.locked_by_registrant? + label = view.content_tag(:span, 'registryLock', class: 'label label-danger') + html += " #{label}" + end + html.html_safe end @@ -59,20 +64,30 @@ class DomainPresenter end end + def remove_registry_lock_btn + return unless domain.locked_by_registrant? + + view.link_to(view.t('admin.domains.registry_lock.destroy.btn'), + view.admin_domain_registry_lock_path(domain), + method: :delete, + data: { confirm: view.t('admin.domains.registry_lock.destroy.confirm') }, + class: 'dropdown-item') + end + def keep_btn return unless domain.discarded? view.link_to view.t('admin.domains.edit.keep_btn'), view.keep_admin_domain_path(@domain), method: :patch, data: { confirm: view.t('admin.domains.edit.keep_btn_confirm') }, - class: 'btn btn-default' + class: 'dropdown-item' end private def schedule_force_delete_btn view.content_tag(:a, view.t('admin.domains.force_delete_toggle_btn.schedule'), - class: 'btn btn-default', + class: 'dropdown-item', data: { toggle: 'modal', target: '.domain-edit-force-delete-dialog', @@ -86,14 +101,14 @@ class DomainPresenter data: { confirm: view.t('admin.domains.force_delete_toggle_btn.cancel_confirm'), }, - class: 'btn btn-primary' + class: 'dropdown-item' end def inactive_schedule_force_delete_btn view.content_tag :button, view.t('admin.domains.force_delete_toggle_btn.schedule'), title: view.t('admin.domains.force_delete_toggle_btn.unable_to_schedule'), disabled: true, - class: 'btn btn-default' + class: 'dropdown-item' end attr_reader :domain diff --git a/app/views/admin/domains/edit.html.erb b/app/views/admin/domains/edit.html.erb index 5bbb9f241..2317bc330 100644 --- a/app/views/admin/domains/edit.html.erb +++ b/app/views/admin/domains/edit.html.erb @@ -5,19 +5,31 @@
  • <%= link_to @domain, admin_domain_path(@domain) %>
  • + diff --git a/config/locales/admin/domains.en.yml b/config/locales/admin/domains.en.yml index 41751874a..7773331c8 100644 --- a/config/locales/admin/domains.en.yml +++ b/config/locales/admin/domains.en.yml @@ -13,7 +13,7 @@ en: reset_btn: Reset edit: - header: "Edit: %{domain}" + header: "Edit:" add_new_status_btn: Add new status keep_btn: Remove deleteCandidate status keep_btn_confirm: Are you sure you want to remove deleteCandidate status? @@ -28,6 +28,13 @@ en: close_btn: Close dialog submit_btn: Force delete domain + registry_lock: + destroy: + btn: Remove registry lock + confirm: Are you sure you want to remove the registry lock? + success: Registry lock removed + error: Registry lock could not be removed + versions: time: Time registrant: Registrant @@ -38,6 +45,8 @@ en: general: outzone_time: Outzone time delete_time: Delete time + force_delete_time: Force delete time + locked_by_registrant_at: Registry lock time admin_contacts: title: Admin. contacts @@ -53,4 +62,4 @@ en: unable_to_schedule: > Force delete procedure cannot be scheduled while a domain has deleteCandidate status cancel: Cancel force delete - cancel_confirm: Are you sure you want cancel force delete procedure? \ No newline at end of file + cancel_confirm: Are you sure you want cancel force delete procedure? diff --git a/config/routes.rb b/config/routes.rb index 362271516..f173ccad4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,7 +23,9 @@ Rails.application.routes.draw do namespace :registrant do post 'auth/eid', to: 'auth#eid' - resources :domains, only: %i[index show], param: :uuid + resources :domains, only: %i[index show], param: :uuid do + resource :registry_lock, only: %i[create destroy] + end resources :contacts, only: %i[index show], param: :uuid end end @@ -195,6 +197,7 @@ Rails.application.routes.draw do resources :pending_updates resources :pending_deletes resource :force_delete, controller: 'domains/force_delete', only: %i[create destroy] + resource :registry_lock, controller: 'domains/registry_lock', only: :destroy member do patch :keep @@ -265,4 +268,4 @@ Rails.application.routes.draw do # To prevent users seeing the default welcome message "Welcome aboard" from Rails root to: redirect('admin/sign_in') -end \ No newline at end of file +end diff --git a/db/migrate/20180808064402_add_registry_lock_time_column.rb b/db/migrate/20180808064402_add_registry_lock_time_column.rb new file mode 100644 index 000000000..5bcbd2c7e --- /dev/null +++ b/db/migrate/20180808064402_add_registry_lock_time_column.rb @@ -0,0 +1,7 @@ +class AddRegistryLockTimeColumn < ActiveRecord::Migration + def change + change_table(:domains) do |t| + t.column :locked_by_registrant_at, :datetime, null: true + end + end +end diff --git a/db/migrate/20180824092855_change_domain_pending_json_to_jsonb.rb b/db/migrate/20180824092855_change_domain_pending_json_to_jsonb.rb new file mode 100644 index 000000000..10639527a --- /dev/null +++ b/db/migrate/20180824092855_change_domain_pending_json_to_jsonb.rb @@ -0,0 +1,9 @@ +class ChangeDomainPendingJsonToJsonb < ActiveRecord::Migration + def up + change_column :domains, :pending_json, 'jsonb USING CAST(pending_json AS jsonb)' + end + + def down + change_column :domains, :pending_json, 'json USING CAST(pending_json AS json)' + end +end diff --git a/db/structure.sql b/db/structure.sql index c0b4742e6..b93136de2 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -904,7 +904,7 @@ CREATE TABLE public.domains ( delete_at timestamp without time zone, registrant_verification_asked_at timestamp without time zone, registrant_verification_token character varying, - pending_json json, + pending_json jsonb, force_delete_at timestamp without time zone, statuses character varying[], reserved boolean DEFAULT false, @@ -912,7 +912,8 @@ CREATE TABLE public.domains ( statuses_before_force_delete character varying[] DEFAULT '{}'::character varying[], upid integer, up_date timestamp without time zone, - uuid uuid DEFAULT public.gen_random_uuid() NOT NULL + uuid uuid DEFAULT public.gen_random_uuid() NOT NULL, + locked_by_registrant_at timestamp without time zone ); @@ -4762,6 +4763,8 @@ INSERT INTO schema_migrations (version) VALUES ('20180613045614'); INSERT INTO schema_migrations (version) VALUES ('20180713154915'); +INSERT INTO schema_migrations (version) VALUES ('20180808064402'); + INSERT INTO schema_migrations (version) VALUES ('20180816123540'); INSERT INTO schema_migrations (version) VALUES ('20180823161237'); @@ -4772,6 +4775,8 @@ INSERT INTO schema_migrations (version) VALUES ('20180823174331'); INSERT INTO schema_migrations (version) VALUES ('20180823212823'); +INSERT INTO schema_migrations (version) VALUES ('20180824092855'); + INSERT INTO schema_migrations (version) VALUES ('20180824102834'); INSERT INTO schema_migrations (version) VALUES ('20180825153657'); diff --git a/doc/registrant-api.md b/doc/registrant-api.md index c6f063a91..b475733f4 100644 --- a/doc/registrant-api.md +++ b/doc/registrant-api.md @@ -7,5 +7,5 @@ Main communication specification through Registrant API: [Authentication](registrant-api/v1/authentication.md) [Domains](registrant-api/v1/domain.md) -[Domain Lock](registrant-api/v1/domain_lock.md) +[Registry Lock](registrant-api/v1/registry_lock.md) [Contacts](registrant-api/v1/contact.md) diff --git a/doc/registrant-api/v1/domain_lock.md b/doc/registrant-api/v1/registry_lock.md similarity index 86% rename from doc/registrant-api/v1/domain_lock.md rename to doc/registrant-api/v1/registry_lock.md index 5f275edb4..b8d440554 100644 --- a/doc/registrant-api/v1/domain_lock.md +++ b/doc/registrant-api/v1/registry_lock.md @@ -1,4 +1,4 @@ -# Domain locks +# Registry lock ## POST api/v1/registrant/domains/$UUID/registry_lock @@ -59,12 +59,12 @@ Content-Type: application/json #### Response for failure ``` -HTTP/1.1 400 +HTTP/1.1 422 Content-Type: application/json { "errors": [ - { "base": "domain cannot be locked" } + { "base": "Domain cannot be locked" } ] } @@ -76,11 +76,23 @@ Content-Type: application/json { "errors": [ - { "base": "domain does not exist" } + { "base": "Domain not found" } ] } ``` +``` +HTTP/1.1 401 +Content-Type: application/json + +{ + "errors": [ + { "base": ["Only administrative contacts can manage registry locks"] } + ] +} + +``` + ## DELETE api/v1/registrant/domains/$UUID/registry_lock @@ -139,12 +151,12 @@ Content-Type: application/json #### Response for failure ``` -HTTP/1.1 400 +HTTP/1.1 422 Content-Type: application/json { "errors": [ - { "base": "domain cannot be unlocked" } + { "base": "Domain is not locked" } ] } @@ -156,7 +168,19 @@ Content-Type: application/json { "errors": [ - { "base": "domain does not exist" } + { "base": "Domain not found" } + ] +} + +``` + +``` +HTTP/1.1 401 +Content-Type: application/json + +{ + "errors": [ + { "base": ["Only administrative contacts can manage registry locks"] } ] } diff --git a/spec/models/domain_spec.rb b/spec/models/domain_spec.rb index c88e25a6f..075f9ea02 100644 --- a/spec/models/domain_spec.rb +++ b/spec/models/domain_spec.rb @@ -299,50 +299,6 @@ RSpec.describe Domain do @domain.registrant_update_confirmable?('123').should == false end end - - context 'with versioning' do - it 'should not have one version' do - with_versioning do - @domain.versions.size.should == 0 - @domain.name = 'new-test-name.ee' - @domain.save - @domain.errors.full_messages.should match_array([]) - @domain.versions.size.should == 1 - end - end - - it 'should return api_creator when created by api user' do - with_versioning do - @user = create(:admin_user) - @api_user = create(:api_user) - @user.id.should == 1 - @api_user.id.should == 2 - ::PaperTrail.whodunnit = '2-ApiUser: testuser' - - @domain = create(:domain) - @domain.creator_str.should == '2-ApiUser: testuser' - - @domain.creator.should == @api_user - @domain.creator.should_not == @user - end - end - - it 'should return api_creator when created by api user' do - with_versioning do - @user = create(:admin_user, id: 1000) - @api_user = create(:api_user, id: 2000) - @user.id.should == 1000 - @api_user.id.should == 2000 - ::PaperTrail.whodunnit = '1000-AdminUser: testuser' - - @domain = create(:domain) - @domain.creator_str.should == '1000-AdminUser: testuser' - - @domain.creator.should == @user - @domain.creator.should_not == @api_user - end - end - end end it 'validates domain name' do diff --git a/test/fixtures/contacts.yml b/test/fixtures/contacts.yml index cb9387090..06bd2c90e 100644 --- a/test/fixtures/contacts.yml +++ b/test/fixtures/contacts.yml @@ -15,7 +15,7 @@ william: &william email: william@inbox.test phone: '+555.555' fax: '+666.6' - ident: 1234 + ident: 12345 ident_type: priv ident_country_code: US registrar: bestnames @@ -34,7 +34,7 @@ jane: name: Jane email: jane@mail.test phone: '+555.555' - ident: 1234 + ident: 123456 ident_type: priv ident_country_code: US registrar: bestnames @@ -46,7 +46,7 @@ acme_ltd: name: Acme Ltd email: acme@outlook.test phone: '+555.555' - ident: 1234 + ident: 1234567 ident_type: org registrar: bestnames ident_country_code: US @@ -58,7 +58,7 @@ jack: name: Jack email: jack@inbox.test phone: '+555.555' - ident: 1234 + ident: 12345678 ident_type: org registrar: goodnames ident_country_code: US @@ -87,4 +87,4 @@ invalid: email: invalid@invalid.test auth_info: any registrar: bestnames - uuid: bd80c0f9-26ee-49e0-a2cb-2311d931c433 \ No newline at end of file + uuid: bd80c0f9-26ee-49e0-a2cb-2311d931c433 diff --git a/test/fixtures/domain_contacts.yml b/test/fixtures/domain_contacts.yml index 43d082e31..5c5d89e24 100644 --- a/test/fixtures/domain_contacts.yml +++ b/test/fixtures/domain_contacts.yml @@ -28,11 +28,36 @@ airport_william_tech: contact: william type: TechDomainContact -library_john: +library_acme_admin: + domain: library + contact: acme_ltd + type: AdminDomainContact + +library_john_tech: domain: library contact: john + type: TechDomainContact + +metro_jack_admin: + domain: metro + contact: jack type: AdminDomainContact +metro_jack_tech: + domain: metro + contact: jack + type: TechDomainContact + +hospital_john_admin: + domain: hospital + contact: john + type: AdminDomainContact + +hospital_john_tech: + domain: hospital + contact: john + type: TechDomainContact + invalid_invalid_admin: domain: invalid contact: invalid diff --git a/test/integration/api/registrant/registrant_api_contacts_test.rb b/test/integration/api/registrant/registrant_api_contacts_test.rb index ddeaee9f3..97f0c8886 100644 --- a/test/integration/api/registrant/registrant_api_contacts_test.rb +++ b/test/integration/api/registrant/registrant_api_contacts_test.rb @@ -25,7 +25,7 @@ class RegistrantApiContactsTest < ApplicationIntegrationTest assert_equal(200, response.status) json_body = JSON.parse(response.body, symbolize_names: true) - assert_equal(5, json_body.count) + assert_equal(4, json_body.count) array_of_contact_codes = json_body.map { |x| x[:code] } assert(array_of_contact_codes.include?('william-001')) assert(array_of_contact_codes.include?('jane-001')) @@ -39,7 +39,7 @@ class RegistrantApiContactsTest < ApplicationIntegrationTest get '/api/v1/registrant/contacts', {}, @auth_headers response_json = JSON.parse(response.body, symbolize_names: true) - assert_equal(5, response_json.count) + assert_equal(4, response_json.count) end def test_get_contact_details_by_uuid diff --git a/test/integration/api/registrant/registrant_api_domains_test.rb b/test/integration/api/registrant/registrant_api_domains_test.rb index 128d15e20..0764db3aa 100644 --- a/test/integration/api/registrant/registrant_api_domains_test.rb +++ b/test/integration/api/registrant/registrant_api_domains_test.rb @@ -57,7 +57,7 @@ class RegistrantApiDomainsTest < ApplicationIntegrationTest get '/api/v1/registrant/domains', {}, @auth_headers response_json = JSON.parse(response.body, symbolize_names: true) - assert_equal(5, response_json.count) + assert_equal(4, response_json.count) end def test_root_does_not_accept_limit_higher_than_200 diff --git a/test/integration/api/registrant/registrant_api_registry_locks_test.rb b/test/integration/api/registrant/registrant_api_registry_locks_test.rb new file mode 100644 index 000000000..bb50bdc1b --- /dev/null +++ b/test/integration/api/registrant/registrant_api_registry_locks_test.rb @@ -0,0 +1,131 @@ +require 'test_helper' +require 'auth_token/auth_token_creator' + +class RegistrantApiRegistryLocksTest < ApplicationIntegrationTest + def setup + super + + @original_registry_time = Setting.days_to_keep_business_registry_cache + Setting.days_to_keep_business_registry_cache = 1 + travel_to Time.zone.parse('2010-07-05') + + @user = users(:registrant) + @domain = domains(:airport) + @auth_headers = { 'HTTP_AUTHORIZATION' => auth_token } + end + + def teardown + super + + Setting.days_to_keep_business_registry_cache = @original_registry_time + travel_back + end + + def test_can_lock_a_not_locked_domain + post '/api/v1/registrant/domains/2df2c1a1-8f6a-490a-81be-8bdf29866880/registry_lock', + {}, @auth_headers + + response_json = JSON.parse(response.body, symbolize_names: true) + + assert(response_json[:statuses].include?(DomainStatus::SERVER_DELETE_PROHIBITED)) + assert(response_json[:statuses].include?(DomainStatus::SERVER_TRANSFER_PROHIBITED)) + assert(response_json[:statuses].include?(DomainStatus::SERVER_UPDATE_PROHIBITED)) + + @domain.reload + assert(@domain.locked_by_registrant?) + end + + def test_locking_a_domain_creates_a_version_record + assert_difference '@domain.versions.count', 1 do + post '/api/v1/registrant/domains/2df2c1a1-8f6a-490a-81be-8bdf29866880/registry_lock', + {}, @auth_headers + end + + @domain.reload + assert_equal(@domain.updator, @user) + end + + def test_cannot_lock_a_domain_in_pending_state + @domain.statuses << DomainStatus::PENDING_UPDATE + @domain.save + + post '/api/v1/registrant/domains/2df2c1a1-8f6a-490a-81be-8bdf29866880/registry_lock', + {}, @auth_headers + + response_json = JSON.parse(response.body, symbolize_names: true) + assert_equal(422, response.status) + assert_equal({ errors: [{ base: ['Domain cannot be locked'] }] }, response_json) + end + + def test_cannot_lock_an_already_locked_domain + @domain.apply_registry_lock + assert(@domain.locked_by_registrant?) + + post '/api/v1/registrant/domains/2df2c1a1-8f6a-490a-81be-8bdf29866880/registry_lock', + {}, @auth_headers + + response_json = JSON.parse(response.body, symbolize_names: true) + assert_equal(422, response.status) + assert_equal({ errors: [{ base: ['Domain cannot be locked'] }] }, response_json) + end + + def test_can_unlock_a_locked_domain + @domain.apply_registry_lock + + delete '/api/v1/registrant/domains/2df2c1a1-8f6a-490a-81be-8bdf29866880/registry_lock', + {}, @auth_headers + + response_json = JSON.parse(response.body, symbolize_names: true) + assert(response_json[:statuses].include?(DomainStatus::OK)) + @domain.reload + refute(@domain.locked_by_registrant?) + end + + def test_cannot_unlock_a_not_locked_domain + delete '/api/v1/registrant/domains/2df2c1a1-8f6a-490a-81be-8bdf29866880/registry_lock', + {}, @auth_headers + + response_json = JSON.parse(response.body, symbolize_names: true) + assert_equal(422, response.status) + assert_equal({ errors: [{ base: ['Domain is not locked'] }] }, response_json) + end + + def test_returns_404_when_domain_is_not_found + post '/api/v1/registrant/domains/random-uuid/registry_lock', + {}, @auth_headers + + response_json = JSON.parse(response.body, symbolize_names: true) + assert_equal(404, response.status) + assert_equal({ errors: [{ base: ['Domain not found'] }] }, response_json) + end + + def test_technical_contact_cannot_lock_a_domain + post '/api/v1/registrant/domains/647bcc48-8d5e-4a04-8ce5-2a3cd17b6eab/registry_lock', + {}, @auth_headers + + response_json = JSON.parse(response.body, symbolize_names: true) + assert_equal(401, response.status) + assert_equal({ errors: [{ base: ['Only administrative contacts can manage registry locks'] }] }, + response_json) + end + + def test_registrant_can_lock_a_domain + post '/api/v1/registrant/domains/1b3ee442-e8fe-4922-9492-8fcb9dccc69c/registry_lock', + {}, @auth_headers + + assert_equal(200, response.status) + response_json = JSON.parse(response.body, symbolize_names: true) + + assert(response_json[:statuses].include?(DomainStatus::SERVER_DELETE_PROHIBITED)) + assert(response_json[:statuses].include?(DomainStatus::SERVER_TRANSFER_PROHIBITED)) + assert(response_json[:statuses].include?(DomainStatus::SERVER_UPDATE_PROHIBITED)) + end + + private + + def auth_token + token_creator = AuthTokenCreator.create_with_defaults(@user) + hash = token_creator.token_in_hash + "Bearer #{hash[:access_token]}" + end +end diff --git a/test/models/domain/domain_version_test.rb b/test/models/domain/domain_version_test.rb new file mode 100644 index 000000000..74844f3af --- /dev/null +++ b/test/models/domain/domain_version_test.rb @@ -0,0 +1,52 @@ +require 'test_helper' + +class DomainVersionTest < ActiveSupport::TestCase + def setup + super + + @domain = domains(:shop) + @contacts = @domain.contacts + @user = users(:registrant) + end + + def teardown + super + end + + def test_assigns_creator_to_paper_trail_whodunnit + duplicate_domain = prepare_duplicate_domain + + PaperTrail.whodunnit = @user.id_role_username + assert_difference 'duplicate_domain.versions.count', 1 do + duplicate_domain.save! + end + + assert_equal(duplicate_domain.creator, @user) + assert_equal(duplicate_domain.updator, @user) + assert_equal(duplicate_domain.creator_str, @user.id_role_username) + assert_equal(duplicate_domain.updator_str, @user.id_role_username) + end + + def test_assigns_updator_to_paper_trail_whodunnit + PaperTrail.whodunnit = @user.id_role_username + + assert_difference '@domain.versions.count', 1 do + @domain.apply_registry_lock + end + + assert_equal(@domain.updator, @user) + assert_equal(@domain.updator_str, @user.id_role_username) + end + + private + + def prepare_duplicate_domain + duplicate_domain = @domain.dup + duplicate_domain.tech_contacts << @contacts + duplicate_domain.admin_contacts << @contacts + duplicate_domain.name = 'duplicate.test' + duplicate_domain.uuid = nil + + duplicate_domain + end +end diff --git a/test/models/domain/registry_lockable_test.rb b/test/models/domain/registry_lockable_test.rb new file mode 100644 index 000000000..cadef69d2 --- /dev/null +++ b/test/models/domain/registry_lockable_test.rb @@ -0,0 +1,72 @@ +require 'test_helper' + +class DomainRegistryLockableTest < ActiveSupport::TestCase + def setup + super + + @domain = domains(:airport) + end + + def test_registry_lock_on_lockable_domain + refute(@domain.locked_by_registrant?) + @domain.apply_registry_lock + + assert_equal( + [DomainStatus::SERVER_UPDATE_PROHIBITED, + DomainStatus::SERVER_DELETE_PROHIBITED, + DomainStatus::SERVER_TRANSFER_PROHIBITED], + @domain.statuses + ) + + assert(@domain.locked_by_registrant?) + assert(@domain.locked_by_registrant_at) + end + + def test_registry_lock_cannot_be_applied_twice + @domain.apply_registry_lock + refute(@domain.apply_registry_lock) + assert(@domain.locked_by_registrant?) + assert(@domain.locked_by_registrant_at) + end + + def test_registry_lock_cannot_be_applied_on_pending_statuses + @domain.statuses << DomainStatus::PENDING_RENEW + refute(@domain.apply_registry_lock) + refute(@domain.locked_by_registrant?) + refute(@domain.locked_by_registrant_at) + end + + def test_remove_registry_lock_on_locked_domain + @domain.apply_registry_lock + + assert_equal( + [DomainStatus::SERVER_UPDATE_PROHIBITED, + DomainStatus::SERVER_DELETE_PROHIBITED, + DomainStatus::SERVER_TRANSFER_PROHIBITED], + @domain.statuses + ) + + @domain.remove_registry_lock + + assert_equal(["ok"], @domain.statuses) + refute(@domain.locked_by_registrant?) + refute(@domain.locked_by_registrant_at) + end + + def test_remove_registry_lock_on_non_locked_domain + refute(@domain.locked_by_registrant?) + refute(@domain.remove_registry_lock) + + assert_equal([], @domain.statuses) + refute(@domain.locked_by_registrant?) + refute(@domain.locked_by_registrant_at) + end + + def test_registry_lock_cannot_be_removed_if_statuses_were_set_by_admin + @domain.statuses << DomainStatus::SERVER_UPDATE_PROHIBITED + @domain.statuses << DomainStatus::SERVER_DELETE_PROHIBITED + @domain.statuses << DomainStatus::SERVER_TRANSFER_PROHIBITED + + refute(@domain.remove_registry_lock) + end +end diff --git a/test/models/registrant_user/registrant_user_creation_test.rb b/test/models/registrant_user/registrant_user_creation_test.rb new file mode 100644 index 000000000..fc5a32b4c --- /dev/null +++ b/test/models/registrant_user/registrant_user_creation_test.rb @@ -0,0 +1,56 @@ +require 'test_helper' + +class RegistrantUserCreationTest < ActiveSupport::TestCase + def test_find_or_create_by_api_data_creates_a_user + user_data = { + ident: '37710100070', + first_name: 'JOHN', + last_name: 'SMITH' + } + + RegistrantUser.find_or_create_by_api_data(user_data) + + user = User.find_by(registrant_ident: 'EE-37710100070') + assert_equal('JOHN SMITH', user.username) + end + + def test_find_or_create_by_api_data_creates_a_user_after_upcasing_input + user_data = { + ident: '37710100070', + first_name: 'John', + last_name: 'Smith' + } + + RegistrantUser.find_or_create_by_api_data(user_data) + + user = User.find_by(registrant_ident: 'EE-37710100070') + assert_equal('JOHN SMITH', user.username) + end + + def test_find_or_create_by_mid_data_creates_a_user + user_data = OpenStruct.new(user_country: 'EE', user_id_code: '37710100070', + user_givenname: 'JOHN', user_surname: 'SMITH') + + RegistrantUser.find_or_create_by_mid_data(user_data) + user = User.find_by(registrant_ident: 'EE-37710100070') + assert_equal('JOHN SMITH', user.username) + end + + def test_find_or_create_by_idc_with_legacy_header_creates_a_user + header = '/C=EE/O=ESTEID/OU=authentication/CN=SMITH,JOHN,37710100070/SN=SMITH/GN=JOHN/serialNumber=37710100070' + + RegistrantUser.find_or_create_by_idc_data(header, RegistrantUser::ACCEPTED_ISSUER) + + user = User.find_by(registrant_ident: 'EE-37710100070') + assert_equal('JOHN SMITH', user.username) + end + + def test_find_or_create_by_idc_with_rfc2253_header_creates_a_user + header = 'serialNumber=37710100070,GN=JOHN,SN=SMITH,CN=SMITH\\,JOHN\\,37710100070,OU=authentication,O=ESTEID,C=EE' + + RegistrantUser.find_or_create_by_idc_data(header, RegistrantUser::ACCEPTED_ISSUER) + + user = User.find_by(registrant_ident: 'EE-37710100070') + assert_equal('JOHN SMITH', user.username) + end +end diff --git a/test/models/registrant_user_test.rb b/test/models/registrant_user_test.rb index 86ab5591a..298d3a096 100644 --- a/test/models/registrant_user_test.rb +++ b/test/models/registrant_user_test.rb @@ -1,62 +1,38 @@ +require 'test_helper' + class RegistrantUserTest < ActiveSupport::TestCase def setup super + + @user = users(:registrant) end def teardown super end - def test_find_or_create_by_api_data_creates_a_user - user_data = { - ident: '37710100070', - first_name: 'JOHN', - last_name: 'SMITH' - } + def test_domains_returns_an_list_of_distinct_domains_associated_with_a_specific_id_code + domain_names = @user.domains.pluck(:name) + assert_equal(4, domain_names.length) - RegistrantUser.find_or_create_by_api_data(user_data) - - user = User.find_by(registrant_ident: 'EE-37710100070') - assert_equal('JOHN SMITH', user.username) + # User is a registrant, but not a contact for the domain. Should be included in the list. + assert(domain_names.include?('shop.test')) end - def test_find_or_create_by_api_data_creates_a_user_after_upcasing_input - user_data = { - ident: '37710100070', - first_name: 'John', - last_name: 'Smith' - } + def test_administered_domains_returns_a_list_of_domains + domain_names = @user.administered_domains.pluck(:name) + assert_equal(3, domain_names.length) - RegistrantUser.find_or_create_by_api_data(user_data) - - user = User.find_by(registrant_ident: 'EE-37710100070') - assert_equal('JOHN SMITH', user.username) + # User is a tech contact for the domain. + refute(domain_names.include?('library.test')) end - def test_find_or_create_by_mid_data_creates_a_user - user_data = OpenStruct.new(user_country: 'EE', user_id_code: '37710100070', - user_givenname: 'JOHN', user_surname: 'SMITH') - - RegistrantUser.find_or_create_by_mid_data(user_data) - user = User.find_by(registrant_ident: 'EE-37710100070') - assert_equal('JOHN SMITH', user.username) + def test_contacts_returns_an_list_of_contacts_associated_with_a_specific_id_code + assert_equal(1, @user.contacts.count) end - def test_find_or_create_by_idc_with_legacy_header_creates_a_user - header = '/C=EE/O=ESTEID/OU=authentication/CN=SMITH,JOHN,37710100070/SN=SMITH/GN=JOHN/serialNumber=37710100070' - - RegistrantUser.find_or_create_by_idc_data(header, RegistrantUser::ACCEPTED_ISSUER) - - user = User.find_by(registrant_ident: 'EE-37710100070') - assert_equal('JOHN SMITH', user.username) - end - - def test_find_or_create_by_idc_with_rfc2253_header_creates_a_user - header = 'serialNumber=37710100070,GN=JOHN,SN=SMITH,CN=SMITH\\,JOHN\\,37710100070,OU=authentication,O=ESTEID,C=EE' - - RegistrantUser.find_or_create_by_idc_data(header, RegistrantUser::ACCEPTED_ISSUER) - - user = User.find_by(registrant_ident: 'EE-37710100070') - assert_equal('JOHN SMITH', user.username) + def test_ident_and_country_code_helper_methods + assert_equal('1234', @user.ident) + assert_equal('US', @user.country_code) end end diff --git a/test/system/admin_area/domains/registry_lock_test.rb b/test/system/admin_area/domains/registry_lock_test.rb new file mode 100644 index 000000000..ce09c445c --- /dev/null +++ b/test/system/admin_area/domains/registry_lock_test.rb @@ -0,0 +1,57 @@ +require 'test_helper' + +class AdminAreaRegistryLockTest < JavaScriptApplicationSystemTestCase + def setup + super + WebMock.allow_net_connect! + + sign_in users(:admin) + travel_to Time.zone.parse('2010-07-05 00:30:00') + @domain = domains(:airport) + end + + def teardown + travel_back + end + + def test_does_not_have_link_when_domain_is_not_locked + visit edit_admin_domain_path(@domain) + click_link_or_button('Actions') + refute(page.has_link?('Remove registry lock')) + end + + def test_can_remove_registry_lock_from_a_domain + @domain.apply_registry_lock + + visit edit_admin_domain_path(@domain) + click_link_or_button('Actions') + assert(page.has_link?('Remove registry lock')) + + accept_confirm('Are you sure you want to remove the registry lock?') do + click_link_or_button('Remove registry lock') + end + + assert_text('Registry lock removed') + + @domain.reload + refute @domain.locked_by_registrant? + end + + def test_cannot_remove_registry_lock_from_not_locked_domain + @domain.apply_registry_lock + visit edit_admin_domain_path(@domain) + @domain.remove_registry_lock + + refute @domain.locked_by_registrant? + + click_link_or_button('Actions') + assert(page.has_link?('Remove registry lock')) + + accept_confirm('Are you sure you want to remove the registry lock?') do + click_link_or_button('Remove registry lock') + end + + assert_text('Registry lock could not be removed') + refute @domain.locked_by_registrant? + end +end diff --git a/test/system/admin_area/domains_test.rb b/test/system/admin_area/domains_test.rb index cd7a8f10a..91e484fc7 100644 --- a/test/system/admin_area/domains_test.rb +++ b/test/system/admin_area/domains_test.rb @@ -3,6 +3,7 @@ require 'test_helper' class AdminDomainsTestTest < ApplicationSystemTestCase setup do sign_in users(:admin) + travel_to Time.zone.parse('2010-07-05 00:30:00') @domain = domains(:shop) end @@ -15,8 +16,19 @@ class AdminDomainsTestTest < ApplicationSystemTestCase assert_field nil, with: @domain.transfer_code end + def test_admin_registry_lock_date + visit admin_domain_path(@domain) + refute_text 'Registry lock time 2010-07-05 00:30' + + lockable_domain = domains(:airport) + lockable_domain.apply_registry_lock + + visit admin_domain_path(lockable_domain) + assert_text 'Registry lock time 2010-07-05 00:30' + assert_text 'registryLock' + end + def test_keep_a_domain - travel_to Time.zone.parse('2010-07-05 10:30') @domain.delete_at = Time.zone.parse('2010-07-05 10:00') @domain.discard