Add bulk replace to registrar area

#662
This commit is contained in:
Artur Beljajev 2018-03-19 00:55:35 +02:00
parent f7c2b25a66
commit 228078a84e
30 changed files with 474 additions and 66 deletions

View file

@ -59,5 +59,6 @@ module Repp
mount Repp::AccountV1
mount Repp::DomainTransfersV1
mount Repp::NameserversV1
mount Repp::DomainContactsV1
end
end

View file

@ -0,0 +1,35 @@
module Repp
class DomainContactsV1 < Grape::API
version 'v1', using: :path
resource :domains do
resource :contacts do
patch '/' do
predecessor = current_user.registrar.contacts.find_by(code: params[:predecessor])
successor = current_user.registrar.contacts.find_by(code: params[:successor])
unless predecessor
error!({ error: { type: 'invalid_request_error',
param: 'predecessor',
message: "No such contact: #{params[:predecessor]}" } }, :bad_request)
end
unless successor
error!({ error: { type: 'invalid_request_error',
param: 'successor',
message: "No such contact: #{params[:successor]}" } }, :bad_request)
end
if predecessor == successor
error!({ error: { type: 'invalid_request_error',
message: 'Successor contact must be different from predecessor' } },
:bad_request)
end
affected_domains, skipped_domains = TechDomainContact.replace(predecessor, successor)
@response = { affected_domains: affected_domains, skipped_domains: skipped_domains }
end
end
end
end
end

View file

@ -0,0 +1,20 @@
class Registrar
class BulkChangeController < DeppController
helper_method :available_contacts
def new
authorize! :manage, :repp
render file: 'registrar/bulk_change/new', locals: { active_tab: default_tab }
end
private
def available_contacts
current_user.registrar.contacts.order(:name).pluck(:name, :code)
end
def default_tab
:technical_contact
end
end
end

View file

@ -1,9 +1,5 @@
class Registrar
class RegistrarNameserversController < DeppController
def edit
authorize! :manage, :repp
end
class NameserversController < BulkChangeController
def update
authorize! :manage, :repp
@ -52,7 +48,7 @@ class Registrar
redirect_to registrar_domains_url
else
@api_errors = parsed_response[:errors]
render :edit
render file: 'registrar/bulk_change/new', locals: { active_tab: :nameserver }
end
end
end

View file

@ -0,0 +1,58 @@
class Registrar
class TechContactsController < BulkChangeController
def update
authorize! :manage, :repp
uri = URI.parse("#{ENV['repp_url']}domains/contacts")
request = Net::HTTP::Patch.new(uri)
request.set_form_data(predecessor: params[:predecessor], successor: params[:successor])
request.basic_auth(current_user.username, current_user.password)
if Rails.env.test?
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
http.request(request)
end
elsif Rails.env.development?
client_cert = File.read(ENV['cert_path'])
client_key = File.read(ENV['key_path'])
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
verify_mode: OpenSSL::SSL::VERIFY_NONE,
cert: OpenSSL::X509::Certificate.new(client_cert),
key: OpenSSL::PKey::RSA.new(client_key)) do |http|
http.request(request)
end
else
client_cert = File.read(ENV['cert_path'])
client_key = File.read(ENV['key_path'])
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: (uri.scheme == 'https'),
cert: OpenSSL::X509::Certificate.new(client_cert),
key: OpenSSL::PKey::RSA.new(client_key)) do |http|
http.request(request)
end
end
parsed_response = JSON.parse(response.body, symbolize_names: true)
if response.code == '200'
notices = [t('.replaced')]
notices << "#{t('.affected_domains')}: #{parsed_response[:affected_domains].join(', ')}"
if parsed_response[:skipped_domains]
notices << "#{t('.skipped_domains')}: #{parsed_response[:skipped_domains].join(', ')}"
end
flash[:notice] = notices
redirect_to registrar_domains_url
else
@error = parsed_response[:error]
render file: 'registrar/bulk_change/new', locals: { active_tab: :technical_contact }
end
end
end
end

View file

@ -1,2 +1,23 @@
class TechDomainContact < DomainContact
# Audit log is needed, therefore no raw SQL
def self.replace(predecessor, successor)
affected_domains = []
skipped_domains = []
tech_contacts = where(contact: predecessor)
transaction do
tech_contacts.each do |tech_contact|
if tech_contact.domain.discarded?
skipped_domains << tech_contact.domain.name
next
end
tech_contact.contact = successor
tech_contact.save!
affected_domains << tech_contact.domain.name
end
end
return affected_domains.sort, skipped_domains.sort
end
end

View file

@ -1,7 +1,11 @@
<% if flash[:notice] %>
<div class="alert alert-success alert-dismissible">
<button class="close" data-dismiss="alert" type=button><span>&times;</span></button>
<% if flash[:notice].respond_to?(:join) %>
<p><%= flash[:notice].join('<br>').html_safe %></p>
<% else %>
<p><%= flash[:notice] %></p>
<% end %>
</div>
<% end %>

View file

@ -39,7 +39,7 @@
</div>
</nav>
<div class="container">
<%= render 'shared/flash' %>
<%= render 'flash_messages' %>
<% if depp_controller? %>
<%= render 'registrar/shared/epp_results' %>
<% end %>

View file

@ -1,12 +1,13 @@
<%= form_tag registrar_update_registrar_nameserver_path, method: :put, class: 'form-horizontal' do %>
<%= form_tag registrar_nameservers_path, method: :patch, class: 'form-horizontal' do %>
<%= render 'registrar/domain_transfers/form/api_errors' %>
<div class="form-group">
<div class="col-md-2 control-label">
<%= label_tag :old_hostname %>
</div>
<div class="col-md-5">
<%= text_field_tag :old_hostname, params[:old_hostname], autofocus: true,
required: true,
<%= text_field_tag :old_hostname, params[:old_hostname], required: true,
class: 'form-control' %>
</div>
</div>

View file

@ -0,0 +1,48 @@
<%= form_tag registrar_tech_contacts_path, method: :patch, class: 'form-horizontal' do %>
<% if @error %>
<div class="alert alert-danger">
<%= @error[:message] %>
</div>
<% end %>
<div class="form-group">
<div class="col-md-2 control-label">
<%= label_tag :predecessor %>
</div>
<div class="col-md-5">
<%= text_field_tag :predecessor, params[:predecessor],
list: :contacts,
required: true,
autofocus: true,
class: 'form-control' %>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<%= label_tag :successor %>
</div>
<div class="col-md-5">
<%= text_field_tag :successor, params[:successor],
list: :contacts,
required: true,
class: 'form-control' %>
</div>
</div>
<div class="form-group">
<div class="col-md-5 col-md-offset-2 text-right">
<button class="btn btn-warning">
<%= t '.submit_btn' %>
</button>
</div>
</div>
<% end %>
<datalist id="contacts">
<% available_contacts.each do |data| %>
<option value="<%= data.second %>"><%= data.first %></option>
<% end %>
</datalist>

View file

@ -0,0 +1,32 @@
<ol class="breadcrumb">
<li><%= link_to t('registrar.domains.index.header'), registrar_domains_path %></li>
</ol>
<div class="page-header">
<h1><%= t '.header' %></h1>
</div>
<div class="row">
<div class="col-md-10">
<ul class="nav nav-tabs">
<li class="<%= 'active' if active_tab == :technical_contact %>">
<a href="#technical_contact" data-toggle="tab"><%= t '.technical_contact' %></a>
</li>
<li class="<%= 'active' if active_tab == :nameserver %>">
<a href="#nameserver" data-toggle="tab"><%= t '.nameserver' %></a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane<%= ' active' if active_tab == :technical_contact %>"
id="technical_contact">
<%= render 'tech_contact_form', available_contacts: available_contacts %>
</div>
<div class="tab-pane<%= ' active' if active_tab == :nameserver %>" id="nameserver">
<%= render 'nameserver_form' %>
</div>
</div>
</div>
</div>

View file

@ -1,13 +1,13 @@
<div class="page-header">
<div class="row">
<div class="col-sm-7">
<div class="col-sm-5">
<h1><%= t '.header' %></h1>
</div>
<div class="col-sm-5 text-right">
<div class="col-sm-7 text-right">
<%= link_to t('.new_btn'), new_registrar_domain_path, class: 'btn btn-primary' %>
<%= link_to t('.transfer_btn'), new_registrar_domain_transfer_path, class: 'btn btn-default' %>
<%= link_to t('.replace_nameserver_btn'), registrar_edit_registrar_nameserver_path,
<%= link_to t('.bulk_change_btn'), new_registrar_bulk_change_path,
class: 'btn btn-default' %>
</div>
</div>

View file

@ -1,11 +0,0 @@
<ol class="breadcrumb">
<li><%= link_to t('registrar.domains.index.header'), registrar_domains_path %></li>
</ol>
<div class="page-header">
<h1><%= t '.header' %></h1>
</div>
<%= render 'registrar/domain_transfers/form/api_errors' %>
<%= render 'form' %>

View file

@ -0,0 +1,14 @@
en:
registrar:
bulk_change:
new:
header: Bulk change
technical_contact: Technical contact
nameserver: Nameserver
tech_contact_form:
submit_btn: Replace technical contacts
nameserver_form:
ip_hint: One IP per line
replace_btn: Replace nameserver

View file

@ -5,7 +5,7 @@ en:
header: Domains
new_btn: New domain
transfer_btn: Transfer
replace_nameserver_btn: Replace nameserver
bulk_change_btn: Bulk change
csv:
domain_name: Domain
transfer_code: Transfer code

View file

@ -0,0 +1,5 @@
en:
registrar:
nameservers:
update:
replaced: Nameserver have been successfully replaced

View file

@ -1,13 +0,0 @@
en:
registrar:
registrar_nameservers:
edit:
header: Replace nameserver
replace_btn: Replace
form:
ip_hint: One IP per line
replace_btn: Replace nameserver
update:
replaced: Nameserver have been successfully replaced

View file

@ -0,0 +1,7 @@
en:
registrar:
tech_contacts:
update:
replaced: Technical contacts have been successfully replaced.
affected_domains: Affected domains
skipped_domains: Skipped domains

View file

@ -62,9 +62,9 @@ Rails.application.routes.draw do
end
end
resources :domain_transfers, only: %i[new create]
get 'registrar/nameservers', to: 'registrar_nameservers#edit', as: :edit_registrar_nameserver
put 'registrar/nameservers', to: 'registrar_nameservers#update', as: :update_registrar_nameserver
resource :bulk_change, controller: :bulk_change, only: :new
resource :tech_contacts, only: :update
resource :nameservers, only: :update
resources :contacts, constraints: {:id => /[^\/]+(?=#{ ActionController::Renderers::RENDERERS.map{|e| "\\.#{e}\\z"}.join("|") })|[^\/]+/} do
member do
get 'delete'

View file

@ -0,0 +1,19 @@
# Domain contacts
## PATCH https://repp.internet.ee/v1/domains/contacts
Replaces all domain contacts of the current registrar.
### Example request
```
$ curl https://repp.internet.ee/v1/domains/contacts \
-X PATCH \
-u username:password \
-d predecessor=foo \
-d successor=bar
```
### Example response
```
{
"affected_domains": ["example.com", "example.org"]
}
```

View file

@ -8,11 +8,26 @@ shop_william:
contact: william
type: TechDomainContact
shop_acme_ltd:
domain: shop
contact: acme_ltd
type: TechDomainContact
airport_john:
domain: airport
contact: john
type: AdminDomainContact
airport_william_admin:
domain: airport
contact: william
type: AdminDomainContact
airport_william_tech:
domain: airport
contact: william
type: TechDomainContact
library_john:
domain: library
contact: john

View file

@ -18,6 +18,10 @@ airport_ns1:
hostname: ns1.bestnames.test
domain: airport
airport_ns2:
hostname: ns2.bestnames.test
domain: airport
metro_ns1:
hostname: ns1.bestnames.test
domain: metro

View file

@ -0,0 +1,103 @@
require 'test_helper'
class APIDomainContactsTest < ActionDispatch::IntegrationTest
def test_replace_all_tech_contacts_of_the_current_registrar
patch '/repp/v1/domains/contacts', { predecessor: 'william-001', successor: 'john-001' },
{ 'HTTP_AUTHORIZATION' => http_auth_key }
assert_nil domains(:shop).tech_contacts.find_by(code: 'william-001')
assert domains(:shop).tech_contacts.find_by(code: 'john-001')
assert domains(:airport).tech_contacts.find_by(code: 'john-001')
end
def test_skip_discarded_domains
domains(:airport).update!(statuses: [DomainStatus::DELETE_CANDIDATE])
patch '/repp/v1/domains/contacts', { predecessor: 'william-001', successor: 'john-001' },
{ 'HTTP_AUTHORIZATION' => http_auth_key }
assert domains(:airport).tech_contacts.find_by(code: 'william-001')
end
def test_return_affected_domains_in_alphabetical_order
patch '/repp/v1/domains/contacts', { predecessor: 'william-001', successor: 'john-001' },
{ 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :ok
assert_equal ({ affected_domains: %w[airport.test shop.test],
skipped_domains: [] }),
JSON.parse(response.body, symbolize_names: true)
end
def test_return_skipped_domains_in_alphabetical_order
domains(:shop).update!(statuses: [DomainStatus::DELETE_CANDIDATE])
domains(:airport).update!(statuses: [DomainStatus::DELETE_CANDIDATE])
patch '/repp/v1/domains/contacts', { predecessor: 'william-001', successor: 'john-001' },
{ 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :ok
assert_equal %w[airport.test shop.test], JSON.parse(response.body,
symbolize_names: true)[:skipped_domains]
end
def test_keep_other_tech_contacts_intact
patch '/repp/v1/domains/contacts', { predecessor: 'william-001', successor: 'john-001' },
{ 'HTTP_AUTHORIZATION' => http_auth_key }
assert domains(:shop).tech_contacts.find_by(code: 'acme-ltd-001')
end
def test_keep_admin_contacts_intact
patch '/repp/v1/domains/contacts', { predecessor: 'william-001', successor: 'john-001' },
{ 'HTTP_AUTHORIZATION' => http_auth_key }
assert domains(:airport).admin_contacts.find_by(code: 'william-001')
end
def test_restrict_contacts_to_the_current_registrar
patch '/repp/v1/domains/contacts', { predecessor: 'jack-001', successor: 'william-002' },
{ 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :bad_request
assert_equal ({ error: { type: 'invalid_request_error',
param: 'predecessor',
message: 'No such contact: jack-001' } }),
JSON.parse(response.body, symbolize_names: true)
end
def test_non_existent_predecessor
patch '/repp/v1/domains/contacts', { predecessor: 'non-existent', successor: 'john-001' },
{ 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :bad_request
assert_equal ({ error: { type: 'invalid_request_error',
param: 'predecessor',
message: 'No such contact: non-existent' } }),
JSON.parse(response.body, symbolize_names: true)
end
def test_non_existent_successor
patch '/repp/v1/domains/contacts', { predecessor: 'william-001', successor: 'non-existent' },
{ 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :bad_request
assert_equal ({ error: { type: 'invalid_request_error',
param: 'successor',
message: 'No such contact: non-existent' } }),
JSON.parse(response.body, symbolize_names: true)
end
def test_disallow_self_replacement
patch '/repp/v1/domains/contacts', { predecessor: 'william-001', successor: 'william-001' },
{ 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :bad_request
assert_equal ({ error: { type: 'invalid_request_error',
message: 'Successor contact must be different from predecessor' } }),
JSON.parse(response.body, symbolize_names: true)
end
private
def http_auth_key
ActionController::HttpAuthentication::Basic.encode_credentials('test_bestnames', 'testtest')
end
end

View file

@ -53,7 +53,7 @@ class APIDomainTransfersTest < ActionDispatch::IntegrationTest
end
def test_duplicates_registrant_admin_and_tech_contacts
assert_difference -> { @new_registrar.contacts.size }, 2 do
assert_difference -> { @new_registrar.contacts.size }, 3 do
post '/repp/v1/domain_transfers', request_params, { 'HTTP_AUTHORIZATION' => http_auth_key }
end
end

View file

@ -49,7 +49,7 @@ class EppDomainTransferRequestTest < ActionDispatch::IntegrationTest
end
def test_duplicates_registrant_admin_and_tech_contacts
assert_difference -> { @new_registrar.contacts.size }, 2 do
assert_difference -> { @new_registrar.contacts.size }, 3 do
post '/epp/command/transfer', { frame: request_xml }, { 'HTTP_COOKIE' => 'session=api_goodnames' }
end
end

View file

@ -1,8 +1,7 @@
require 'test_helper'
class RegistrarNameserverReplacementTest < ActionDispatch::IntegrationTest
def setup
WebMock.reset!
class RegistrarAreaNameserverBulkChangeTest < ActionDispatch::IntegrationTest
setup do
login_as users(:api_goodnames)
end
@ -21,7 +20,8 @@ class RegistrarNameserverReplacementTest < ActionDispatch::IntegrationTest
}] }.to_json, status: 200)
visit registrar_domains_url
click_link 'Replace nameserver'
click_link 'Bulk change'
click_link 'Nameserver'
fill_in 'Old hostname', with: 'ns1.bestnames.test'
fill_in 'New hostname', with: 'new-ns.bestnames.test'
@ -40,7 +40,8 @@ class RegistrarNameserverReplacementTest < ActionDispatch::IntegrationTest
headers: { 'Content-type' => 'application/json' })
visit registrar_domains_url
click_link 'Replace nameserver'
click_link 'Bulk change'
click_link 'Nameserver'
fill_in 'Old hostname', with: 'old hostname'
fill_in 'New hostname', with: 'new hostname'

View file

@ -0,0 +1,47 @@
require 'test_helper'
class RegistrarAreaTechContactBulkChangeTest < ActionDispatch::IntegrationTest
setup do
login_as users(:api_bestnames)
end
def test_replace_domain_contacts_of_current_registrar
request_stub = stub_request(:patch, /domains\/contacts/)
.with(body: { predecessor: 'william-001', successor: '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,
status: 200)
visit registrar_domains_url
click_link 'Bulk change'
fill_in 'Predecessor', with: 'william-001'
fill_in 'Successor', with: 'john-001'
click_on 'Replace technical contacts'
assert_requested request_stub
assert_current_path registrar_domains_path
assert_text 'Technical contacts have been successfully replaced'
assert_text 'Affected domains: foo.test, bar.test'
assert_text 'Skipped domains: baz.test, qux.test'
end
def test_fails_gracefully
stub_request(:patch, /domains\/contacts/)
.to_return(status: 400,
body: { error: { message: 'epic fail' } }.to_json,
headers: { 'Content-type' => 'application/json' })
visit registrar_domains_url
click_link 'Bulk change'
fill_in 'Predecessor', with: 'william-001'
fill_in 'Successor', with: 'john-001'
click_on 'Replace technical contacts'
assert_text 'epic fail'
assert_field 'Predecessor', with: 'william-001'
assert_field 'Successor', with: 'john-001'
end
end

View file

@ -1,8 +1,7 @@
require 'test_helper'
class RegistrarDomainTransfersTest < ActionDispatch::IntegrationTest
def setup
WebMock.reset!
setup do
login_as users(:api_goodnames)
end

View file

@ -20,7 +20,7 @@ class DomainTransferTest < ActiveSupport::TestCase
body = 'Transfer of domain shop.test has been approved.' \
' It was associated with registrant john-001' \
' and contacts jane-001, william-001.'
' and contacts acme-ltd-001, jane-001, william-001.'
id = @domain_transfer.id
class_name = @domain_transfer.class.name

View file

@ -50,9 +50,11 @@ class NameserverTest < ActiveSupport::TestCase
end
def test_hostnames
assert_equal %w[ns1.bestnames.test
assert_equal %w[
ns1.bestnames.test
ns2.bestnames.test
ns1.bestnames.test
ns2.bestnames.test
ns1.bestnames.test], Nameserver.hostnames
end