diff --git a/app/controllers/api/v1/registrant/base_controller.rb b/app/controllers/api/v1/registrant/base_controller.rb
index 4497d68e6..4fec4ee26 100644
--- a/app/controllers/api/v1/registrant/base_controller.rb
+++ b/app/controllers/api/v1/registrant/base_controller.rb
@@ -8,6 +8,8 @@ module Api
before_action :authenticate
before_action :set_paper_trail_whodunnit
+ rescue_from ActiveRecord::RecordNotFound, with: :show_not_found_error
+ rescue_from ActiveRecord::RecordInvalid, with: :show_invalid_record_error
rescue_from(ActionController::ParameterMissing) do |parameter_missing_exception|
error = {}
error[parameter_missing_exception.param] = ['parameter is required']
@@ -49,6 +51,14 @@ module Api
def set_paper_trail_whodunnit
::PaperTrail.whodunnit = current_registrant_user.id_role_username
end
+
+ def show_not_found_error
+ render json: { errors: [{ base: ['Not found'] }] }, status: :not_found
+ end
+
+ def show_invalid_record_error(exception)
+ render json: { errors: exception.record.errors }, status: :bad_request
+ 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 1be620ba4..fcce291f9 100644
--- a/app/controllers/api/v1/registrant/contacts_controller.rb
+++ b/app/controllers/api/v1/registrant/contacts_controller.rb
@@ -32,6 +32,63 @@ module Api
end
end
+ def update
+ contact = @contacts_pool.find_by!(uuid: params[:uuid])
+ contact.name = params[:name] if params[:name].present?
+ contact.email = params[:email] if params[:email].present?
+ contact.phone = params[:phone] if params[:phone].present?
+
+ if Setting.address_processing && params[:address]
+ address = Contact::Address.new(params[:address][:street],
+ params[:address][:zip],
+ params[:address][:city],
+ params[:address][:state],
+ params[:address][:country_code])
+ contact.address = address
+ end
+
+ if !Setting.address_processing && params[:address]
+ error_msg = 'Address processing is disabled and therefore cannot be updated'
+ render json: { errors: [{ address: [error_msg] }] }, status: :bad_request and return
+ end
+
+ if ENV['fax_enabled'] == 'true'
+ contact.fax = params[:fax] if params[:fax].present?
+ end
+
+ if ENV['fax_enabled'] != 'true' && params[:fax]
+ error_msg = 'Fax processing is disabled and therefore cannot be updated'
+ render json: { errors: [{ address: [error_msg] }] }, status: :bad_request and return
+ end
+
+ contact.transaction do
+ contact.save!
+ action = current_registrant_user.actions.create!(contact: contact, operation: :update)
+ contact.registrar.notify(action)
+ end
+
+ render json: { id: contact.uuid,
+ name: contact.name,
+ code: contact.code,
+ ident: {
+ code: contact.ident,
+ type: contact.ident_type,
+ country_code: contact.ident_country_code,
+ },
+ email: contact.email,
+ phone: contact.phone,
+ fax: contact.fax,
+ address: {
+ street: contact.street,
+ zip: contact.zip,
+ city: contact.city,
+ state: contact.state,
+ country_code: contact.country_code,
+ },
+ auth_info: contact.auth_info,
+ statuses: contact.statuses }
+ end
+
private
def set_contacts_pool
diff --git a/app/controllers/registrant/contacts_controller.rb b/app/controllers/registrant/contacts_controller.rb
index b2ebad344..a2be10487 100644
--- a/app/controllers/registrant/contacts_controller.rb
+++ b/app/controllers/registrant/contacts_controller.rb
@@ -1,6 +1,8 @@
class Registrant::ContactsController < RegistrantController
helper_method :domain_ids
helper_method :domain
+ helper_method :fax_enabled?
+ skip_authorization_check only: %i[edit update]
def show
@contact = Contact.where(id: contacts).find_by(id: params[:id])
@@ -8,6 +10,25 @@ class Registrant::ContactsController < RegistrantController
authorize! :read, @contact
end
+ def edit
+ @contact = Contact.where(id: contacts).find(params[:id])
+ end
+
+ def update
+ @contact = Contact.where(id: contacts).find(params[:id])
+ @contact.attributes = contact_params
+ response = update_contact_via_api(@contact.uuid)
+ updated = response.is_a?(Net::HTTPSuccess)
+
+ if updated
+ redirect_to registrant_domain_contact_url(domain, @contact), notice: t('.updated')
+ else
+ parsed_response = JSON.parse(response.body, symbolize_names: true)
+ @errors = parsed_response[:errors]
+ render :edit
+ end
+ end
+
private
def contacts
@@ -41,4 +62,68 @@ class Registrant::ContactsController < RegistrantController
current_registrant_user.domains
end
end
-end
\ No newline at end of file
+
+ def contact_params
+ permitted = %i[
+ name
+ email
+ phone
+ ]
+
+ permitted << :fax if fax_enabled?
+ permitted += %i[street zip city state country_code] if Contact.address_processing?
+ params.require(:contact).permit(*permitted)
+ end
+
+ def access_token
+ uri = URI.parse("#{ENV['registrant_api_base_url']}/api/v1/registrant/auth/eid")
+ request = Net::HTTP::Post.new(uri)
+ request.form_data = access_token_request_params
+
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: (uri.scheme == 'https')) do |http|
+ http.request(request)
+ end
+
+ json_doc = JSON.parse(response.body, symbolize_names: true)
+ json_doc[:access_token]
+ end
+
+ def access_token_request_params
+ { ident: current_registrant_user.ident,
+ first_name: current_registrant_user.first_name,
+ last_name: current_registrant_user.last_name }
+ end
+
+ def fax_enabled?
+ ENV['fax_enabled'] == 'true'
+ end
+
+ def contact_update_api_params
+ params = contact_params
+ params = normalize_address_attributes_for_api(params) if Contact.address_processing?
+ params
+ end
+
+ def normalize_address_attributes_for_api(params)
+ normalized = params
+
+ Contact.address_attribute_names.each do |attr|
+ attr = attr.to_sym
+ normalized["address[#{attr}]"] = params[attr]
+ normalized.delete(attr)
+ end
+
+ normalized
+ end
+
+ def update_contact_via_api(uuid)
+ uri = URI.parse("#{ENV['registrant_api_base_url']}/api/v1/registrant/contacts/#{uuid}")
+ request = Net::HTTP::Patch.new(uri)
+ request['Authorization'] = "Bearer #{access_token}"
+ request.form_data = contact_update_api_params
+
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: (uri.scheme == 'https')) do |http|
+ http.request(request)
+ end
+ end
+end
diff --git a/app/models/action.rb b/app/models/action.rb
new file mode 100644
index 000000000..2a60f9a23
--- /dev/null
+++ b/app/models/action.rb
@@ -0,0 +1,17 @@
+class Action < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :contact
+
+ validates :operation, inclusion: { in: proc { |action| action.class.valid_operations } }
+
+ class << self
+ def valid_operations
+ %w[update]
+ end
+ end
+
+ def notification_key
+ raise 'Action object is missing' unless contact
+ "contact_#{operation}".to_sym
+ end
+end
\ No newline at end of file
diff --git a/app/models/contact.rb b/app/models/contact.rb
index 088ec0b59..9d166faee 100644
--- a/app/models/contact.rb
+++ b/app/models/contact.rb
@@ -526,4 +526,20 @@ class Contact < ActiveRecord::Base
domain_names
end
+
+ def address=(address)
+ self.street = address.street
+ self.zip = address.zip
+ self.city = address.city
+ self.state = address.state
+ self.country_code = address.country_code
+ end
+
+ def address
+ Address.new(street, zip, city, state, country_code)
+ end
+
+ def managed_by?(registrant_user)
+ ident == registrant_user.ident
+ end
end
diff --git a/app/models/contact/address.rb b/app/models/contact/address.rb
new file mode 100644
index 000000000..f7a6cfdab
--- /dev/null
+++ b/app/models/contact/address.rb
@@ -0,0 +1,25 @@
+class Contact
+ class Address
+ attr_reader :street
+ attr_reader :zip
+ attr_reader :city
+ attr_reader :state
+ attr_reader :country_code
+
+ def initialize(street, zip, city, state, country_code)
+ @street = street
+ @zip = zip
+ @city = city
+ @state = state
+ @country_code = country_code
+ end
+
+ def ==(other)
+ (street == other.street) &&
+ (zip == other.zip) &&
+ (city == other.city) &&
+ (state == other.state) &&
+ (country_code == other.country_code)
+ end
+ end
+end
\ No newline at end of file
diff --git a/app/models/notification.rb b/app/models/notification.rb
index 0b1829267..d6427323b 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -1,6 +1,8 @@
class Notification < ActiveRecord::Base
include Versions # version/notification_version.rb
+
belongs_to :registrar
+ belongs_to :action
scope :unread, -> { where(read: false) }
@@ -20,7 +22,7 @@ class Notification < ActiveRecord::Base
# Needed for EPP log
def name
- "-"
+ ''
end
private
diff --git a/app/models/registrant_user.rb b/app/models/registrant_user.rb
index ddbf2e664..4cf0949d4 100644
--- a/app/models/registrant_user.rb
+++ b/app/models/registrant_user.rb
@@ -56,6 +56,14 @@ class RegistrantUser < User
username
end
+ def first_name
+ username.split.first
+ end
+
+ def last_name
+ username.split.second
+ end
+
class << self
def find_or_create_by_idc_data(idc_data, issuer_organization)
return false if idc_data.blank?
diff --git a/app/models/registrar.rb b/app/models/registrar.rb
index 611dfc562..e939784de 100644
--- a/app/models/registrar.rb
+++ b/app/models/registrar.rb
@@ -157,6 +157,11 @@ class Registrar < ActiveRecord::Base
end
end
+ def notify(action)
+ text = I18n.t("notifications.texts.#{action.notification_key}", contact: action.contact.code)
+ notifications.create!(text: text)
+ end
+
private
def set_defaults
diff --git a/app/models/user.rb b/app/models/user.rb
index 8968e2736..573cddc94 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,6 +1,8 @@
class User < ActiveRecord::Base
include Versions # version/user_version.rb
+ has_many :actions, dependent: :restrict_with_exception
+
attr_accessor :phone
def id_role_username
diff --git a/app/views/epp/poll/_action.xml.builder b/app/views/epp/poll/_action.xml.builder
new file mode 100644
index 000000000..72d2c1cac
--- /dev/null
+++ b/app/views/epp/poll/_action.xml.builder
@@ -0,0 +1,9 @@
+builder.extension do
+ builder.tag!('changePoll:changeData',
+ 'xmlns:changePoll' => 'https://epp.tld.ee/schema/changePoll-1.0.xsd') do
+ builder.tag!('changePoll:operation', action.operation)
+ builder.tag!('changePoll:date', action.created_at.utc.xmlschema)
+ builder.tag!('changePoll:svTRID', action.id)
+ builder.tag!('changePoll:who', action.user)
+ end
+end
\ No newline at end of file
diff --git a/app/views/epp/poll/_contact.xml.builder b/app/views/epp/poll/_contact.xml.builder
new file mode 100644
index 000000000..731711e52
--- /dev/null
+++ b/app/views/epp/poll/_contact.xml.builder
@@ -0,0 +1,72 @@
+builder.resData do
+ builder.tag!('contact:infData', 'xmlns:contact' => 'https://epp.tld.ee/schema/contact-ee-1.1.xsd') do
+ builder.tag!('contact:id', contact.code)
+ builder.tag!('contact:roid', contact.roid)
+
+ contact.statuses.each do |status|
+ builder.tag!('contact:status', s: status)
+ end
+
+ builder.tag!('contact:postalInfo', type: 'int') do
+ builder.tag!('contact:name', contact.name)
+ if can? :view_full_info, contact, @password
+ builder.tag!('contact:org', contact.org_name) if contact.org_name.present?
+
+ if address_processing
+ builder.tag!('contact:addr') do
+ builder.tag!('contact:street', contact.street)
+ builder.tag!('contact:city', contact.city)
+ builder.tag!('contact:sp', contact.state)
+ builder.tag!('contact:pc', contact.zip)
+ builder.tag!('contact:cc', contact.country_code)
+ end
+ end
+
+ else
+ builder.tag!('contact:org', 'No access')
+
+ if address_processing
+ builder.tag!('contact:addr') do
+ builder.tag!('contact:street', 'No access')
+ builder.tag!('contact:city', 'No access')
+ builder.tag!('contact:sp', 'No access')
+ builder.tag!('contact:pc', 'No access')
+ builder.tag!('contact:cc', 'No access')
+ end
+ end
+
+ end
+ end
+
+ if can? :view_full_info, contact, @password
+ builder.tag!('contact:voice', contact.phone)
+ builder.tag!('contact:fax', contact.fax) if contact.fax.present?
+ builder.tag!('contact:email', contact.email)
+ else
+ builder.tag!('contact:voice', 'No access')
+ builder.tag!('contact:fax', 'No access')
+ builder.tag!('contact:email', 'No access')
+ end
+
+ builder.tag!('contact:clID', contact.registrar.try(:code))
+
+ builder.tag!('contact:crID', contact.cr_id)
+ builder.tag!('contact:crDate', contact.created_at.try(:iso8601))
+
+ if contact.updated_at > contact.created_at
+ upID = contact.updator.try(:registrar)
+ upID = upID.code if upID.present? # Did updator return a kind of User that has a registrar?
+ builder.tag!('contact:upID', upID) if upID.present? # optional upID
+ builder.tag!('contact:upDate', contact.updated_at.try(:iso8601))
+ end
+ if can? :view_password, contact, @password
+ builder.tag!('contact:authInfo') do
+ builder.tag!('contact:pw', contact.auth_info)
+ end
+ else
+ builder.tag!('contact:authInfo') do
+ builder.tag!('contact:pw', 'No access')
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/app/views/epp/poll/poll_req.xml.builder b/app/views/epp/poll/poll_req.xml.builder
index 9ddef8d09..b0c9f40e3 100644
--- a/app/views/epp/poll/poll_req.xml.builder
+++ b/app/views/epp/poll/poll_req.xml.builder
@@ -14,6 +14,21 @@ xml.epp_head do
xml << render('epp/domains/partials/transfer', builder: xml, dt: @object)
end if @object
end
+
+ if @notification.action&.contact
+ # render(partial: 'epp/poll/contact',
+ # locals: {
+ # builder: xml,
+ # contact: @notification.action.contact,
+ # address_processing: Setting.address_processing
+ # })
+ render(partial: 'epp/poll/action',
+ locals: {
+ builder: xml,
+ action: @notification.action
+ })
+ end
+
render('epp/shared/trID', builder: xml)
end
end
diff --git a/app/views/registrant/contacts/_api_errors.html.erb b/app/views/registrant/contacts/_api_errors.html.erb
new file mode 100644
index 000000000..35617fa99
--- /dev/null
+++ b/app/views/registrant/contacts/_api_errors.html.erb
@@ -0,0 +1,7 @@
+
+
+ <% errors.each_value do |errors| %>
+ - <%= errors.join('
') %>
+ <% end %>
+
+
\ No newline at end of file
diff --git a/app/views/registrant/contacts/_form.html.erb b/app/views/registrant/contacts/_form.html.erb
new file mode 100644
index 000000000..f203f39e8
--- /dev/null
+++ b/app/views/registrant/contacts/_form.html.erb
@@ -0,0 +1,64 @@
+<%= form_for [:registrant, domain, @contact], html: { class: 'form-horizontal' } do |f| %>
+ <% if @errors.present? %>
+ <%= render 'api_errors', errors: @errors %>
+ <% end %>
+
+
+
+
+
+
+
+ <% if Contact.address_processing? %>
+
+
<%= t '.address' %>
+
+ <%= render 'registrant/contacts/form/address', f: f %>
+
+
+ <% end %>
+
+ <% if fax_enabled? %>
+
+ <% end %>
+
+
+
+
+
+ <%= button_tag t('.submit_btn'), class: 'btn btn-success' %>
+
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/registrant/contacts/edit.html.erb b/app/views/registrant/contacts/edit.html.erb
new file mode 100644
index 000000000..0a453ded1
--- /dev/null
+++ b/app/views/registrant/contacts/edit.html.erb
@@ -0,0 +1,12 @@
+
+ - <%= link_to t('registrant.domains.index.header'), registrant_domains_path %>
+ - <%= link_to domain, registrant_domain_path(domain) %>
+ - <%= t 'registrant.contacts.contact_index' %>
+ - <%= link_to @contact, registrant_domain_contact_path(domain, @contact) %>
+
+
+
+
+<%= render 'form' %>
\ No newline at end of file
diff --git a/app/views/registrant/contacts/form/_address.html.erb b/app/views/registrant/contacts/form/_address.html.erb
new file mode 100644
index 000000000..a43784d3f
--- /dev/null
+++ b/app/views/registrant/contacts/form/_address.html.erb
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/views/registrant/contacts/show.html.erb b/app/views/registrant/contacts/show.html.erb
index f9a8a86fa..1f0a87b5f 100644
--- a/app/views/registrant/contacts/show.html.erb
+++ b/app/views/registrant/contacts/show.html.erb
@@ -5,7 +5,18 @@
diff --git a/config/application-example.yml b/config/application-example.yml
index b9917e69e..83164b14a 100644
--- a/config/application-example.yml
+++ b/config/application-example.yml
@@ -97,6 +97,7 @@ sk_digi_doc_service_endpoint: 'https://tsp.demo.sk.ee'
sk_digi_doc_service_name: 'Testimine'
# Registrant API
+registrant_api_base_url:
registrant_api_auth_allowed_ips: '127.0.0.1, 0.0.0.0' #ips, separated with commas
#
diff --git a/config/locales/notifications.en.yml b/config/locales/notifications.en.yml
index 46e03c689..1dff4a97c 100644
--- a/config/locales/notifications.en.yml
+++ b/config/locales/notifications.en.yml
@@ -5,3 +5,4 @@ en:
Transfer of domain %{domain_name} has been approved.
It was associated with registrant %{old_registrant_code}
and contacts %{old_contacts_codes}.
+ contact_update: Contact %{contact} has been updated by registrant
diff --git a/config/locales/registrant/contacts.en.yml b/config/locales/registrant/contacts.en.yml
index a44755832..4201bf1b6 100644
--- a/config/locales/registrant/contacts.en.yml
+++ b/config/locales/registrant/contacts.en.yml
@@ -4,6 +4,7 @@ en:
contact_index: Contacts
show:
+ edit_btn: Edit
general:
header: General
@@ -17,4 +18,15 @@ en:
domains:
header: Domains
- all: All roles
\ No newline at end of file
+ all: All roles
+
+ edit:
+ header: Edit contact
+
+ update:
+ updated: Contact has been successfully updated
+
+ form:
+ address: Address
+ submit_btn: Update contact
+
diff --git a/config/routes.rb b/config/routes.rb
index ff33ec652..a40ad9297 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -26,7 +26,7 @@ Rails.application.routes.draw do
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
+ resources :contacts, only: %i[index show update], param: :uuid
end
end
end
@@ -137,7 +137,7 @@ Rails.application.routes.draw do
resources :registrars, only: :show
resources :domains, only: %i[index show] do
- resources :contacts, only: %i[show]
+ resources :contacts, only: %i[show edit update]
collection do
get :download_list
diff --git a/db/migrate/20180824215326_create_actions.rb b/db/migrate/20180824215326_create_actions.rb
new file mode 100644
index 000000000..8bacc9a34
--- /dev/null
+++ b/db/migrate/20180824215326_create_actions.rb
@@ -0,0 +1,9 @@
+class CreateActions < ActiveRecord::Migration
+ def change
+ create_table :actions do |t|
+ t.belongs_to :user, foreign_key: true
+ t.string :operation
+ t.datetime :created_at
+ end
+ end
+end
\ No newline at end of file
diff --git a/db/migrate/20180825193437_change_actions_operation_to_not_null.rb b/db/migrate/20180825193437_change_actions_operation_to_not_null.rb
new file mode 100644
index 000000000..ce1cd2b9d
--- /dev/null
+++ b/db/migrate/20180825193437_change_actions_operation_to_not_null.rb
@@ -0,0 +1,5 @@
+class ChangeActionsOperationToNotNull < ActiveRecord::Migration
+ def change
+ change_column_null :actions, :operation, false
+ end
+end
diff --git a/db/migrate/20180825232819_add_contact_id_to_actions.rb b/db/migrate/20180825232819_add_contact_id_to_actions.rb
new file mode 100644
index 000000000..a6b10a256
--- /dev/null
+++ b/db/migrate/20180825232819_add_contact_id_to_actions.rb
@@ -0,0 +1,5 @@
+class AddContactIdToActions < ActiveRecord::Migration
+ def change
+ add_reference :actions, :contact, foreign_key: true
+ end
+end
diff --git a/db/migrate/20180826162821_add_action_id_to_notifications.rb b/db/migrate/20180826162821_add_action_id_to_notifications.rb
new file mode 100644
index 000000000..7e52fabec
--- /dev/null
+++ b/db/migrate/20180826162821_add_action_id_to_notifications.rb
@@ -0,0 +1,5 @@
+class AddActionIdToNotifications < ActiveRecord::Migration
+ def change
+ add_reference :notifications, :action, foreign_key: true
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index e9b0e7f43..6eede259a 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -370,6 +370,38 @@ CREATE SEQUENCE public.accounts_id_seq
ALTER SEQUENCE public.accounts_id_seq OWNED BY public.accounts.id;
+--
+-- Name: actions; Type: TABLE; Schema: public; Owner: -; Tablespace:
+--
+
+CREATE TABLE public.actions (
+ id integer NOT NULL,
+ user_id integer,
+ operation character varying NOT NULL,
+ created_at timestamp without time zone,
+ contact_id integer
+);
+
+
+--
+-- Name: actions_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.actions_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: actions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.actions_id_seq OWNED BY public.actions.id;
+
+
--
-- Name: bank_statements; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
@@ -2006,7 +2038,8 @@ CREATE TABLE public.notifications (
created_at timestamp without time zone,
updated_at timestamp without time zone,
creator_str character varying,
- updator_str character varying
+ updator_str character varying,
+ action_id integer
);
@@ -2486,6 +2519,13 @@ ALTER TABLE ONLY public.account_activities ALTER COLUMN id SET DEFAULT nextval('
ALTER TABLE ONLY public.accounts ALTER COLUMN id SET DEFAULT nextval('public.accounts_id_seq'::regclass);
+--
+-- Name: id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.actions ALTER COLUMN id SET DEFAULT nextval('public.actions_id_seq'::regclass);
+
+
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -2873,6 +2913,14 @@ ALTER TABLE ONLY public.accounts
ADD CONSTRAINT accounts_pkey PRIMARY KEY (id);
+--
+-- Name: actions_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
+--
+
+ALTER TABLE ONLY public.actions
+ ADD CONSTRAINT actions_pkey PRIMARY KEY (id);
+
+
--
-- Name: bank_statements_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
@@ -4057,6 +4105,30 @@ ALTER TABLE ONLY public.domain_transfers
ADD CONSTRAINT fk_rails_87b8e40c63 FOREIGN KEY (domain_id) REFERENCES public.domains(id);
+--
+-- Name: fk_rails_8c6b5c12eb; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.actions
+ ADD CONSTRAINT fk_rails_8c6b5c12eb FOREIGN KEY (user_id) REFERENCES public.users(id);
+
+
+--
+-- Name: fk_rails_8f9734b530; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.notifications
+ ADD CONSTRAINT fk_rails_8f9734b530 FOREIGN KEY (action_id) REFERENCES public.actions(id);
+
+
+--
+-- Name: fk_rails_a5ae3c203d; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.actions
+ ADD CONSTRAINT fk_rails_a5ae3c203d FOREIGN KEY (contact_id) REFERENCES public.contacts(id);
+
+
--
-- Name: fk_rails_adff2dc8e3; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -4777,5 +4849,15 @@ INSERT INTO schema_migrations (version) VALUES ('20180824092855');
INSERT INTO schema_migrations (version) VALUES ('20180824102834');
+INSERT INTO schema_migrations (version) VALUES ('20180824215326');
+
+INSERT INTO schema_migrations (version) VALUES ('20180825153657');
+
+INSERT INTO schema_migrations (version) VALUES ('20180825193437');
+
+INSERT INTO schema_migrations (version) VALUES ('20180825232819');
+
+INSERT INTO schema_migrations (version) VALUES ('20180826162821');
+
INSERT INTO schema_migrations (version) VALUES ('20181002090319');
diff --git a/doc/registrant-api/v1/contact.md b/doc/registrant-api/v1/contact.md
index 1102752b3..74c78bd5a 100644
--- a/doc/registrant-api/v1/contact.md
+++ b/doc/registrant-api/v1/contact.md
@@ -113,16 +113,17 @@ Update contact details for a contact.
#### Parameters
-| Field name | Required | Type | Allowed values | Description |
-| ---- | --- | --- | --- | --- |
-| email | false | String | | New email address |
-| phone | false | String | | New phone number |
-| fax | false | String | | New fax number |
-| city | false | String | | New city name |
-| street | false | String | | New street name |
-| zip | false | String | | New zip code |
-| country_code | false | String | | New country code in 2 letter format ('EE', 'LV') |
-| state | false | String | | New state name |
+| Field name | Required | Type | Allowed values | Description |
+| ---- | --- | --- | --- | --- |
+| name | false | String | | New name |
+| email | false | String | | New email |
+| phone | false | String | | New phone number |
+| fax | false | String | | New fax number |
+| address[street] | false | String | | New street name |
+| address[zip] | false | String | | New zip |
+| address[city] | false | String | | New city name |
+| address[state] | false | String | | New state name |
+| address[country_code] | false | String | | New country code in 2 letter format (ISO 3166-1 alpha-2) |
#### Request
@@ -133,14 +134,17 @@ Accept: application/json
Content-type: application/json
{
+ "name": "John Doe",
"email": "foo@bar.baz",
"phone": "+372.12345671",
"fax": "+372.12345672",
- "city": "New City",
- "street": "Main Street 123",
- "zip": "22222",
- "country_code": "LV",
- "state": "New state"
+ "address": {
+ "street": "Main Street 123",
+ "zip": "22222",
+ "city": "New City",
+ "state": "New state",
+ "country_code": "LV"
+ }
}
```
@@ -151,33 +155,28 @@ HTTP/1.1 200
Content-Type: application/json
{
- "uuid": "84c62f3d-e56f-40fa-9ca4-dc0137778949",
- "domain_names": ["example.com"],
- "code": "REGISTRAR2:SH022086480",
- "phone": "+372.12345671",
- "email": "foo@bar.baz",
- "fax": "+372.12345672",
- "created_at": "2015-09-09T09:11:14.130Z",
- "updated_at": "2018-09-09T09:11:14.130Z",
- "ident": "37605030299",
- "ident_type": "priv",
- "auth_info": "password",
+ "id": "84c62f3d-e56f-40fa-9ca4-dc0137778949",
"name": "Karson Kessler0",
- "org_name": null,
- "registrar_id": 2,
- "creator_str": null,
- "updator_str": null,
- "ident_country_code": "EE",
- "city": "New City",
- "street": "Main Street 123",
- "zip": "22222",
- "country_code": "LV",
- "state": "New state"
- "legacy_id": null,
+ "code": "REGISTRAR2:SH022086480",
+ "ident": {
+ "code": "37605030299",
+ "type": "priv",
+ "country_code": "EE"
+ },
+ "email": "foo@bar.baz",
+ "phone": "+372.12345671",
+ "fax": "+372.12345672",
+ "address": {
+ "street": "Main Street 123",
+ "zip": "22222",
+ "city": "New City",
+ "state": "New state",
+ "country_code": "LV"
+ },
+ "auth_info": "password",
"statuses": [
"ok"
- ],
- "status_notes": {}
+ ]
}
```
@@ -187,8 +186,8 @@ HTTP/1.1 400
Content-Type: application/json
{
- "errors": [
- { "phone": "Phone nr is invalid" }
- ]
+ "errors": {
+ "phone": ["Phone nr is invalid"]
+ }
}
```
diff --git a/lib/schemas/changePoll-1.0.xsd b/lib/schemas/changePoll-1.0.xsd
new file mode 100644
index 000000000..ebdf11443
--- /dev/null
+++ b/lib/schemas/changePoll-1.0.xsd
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+ Extensible Provisioning Protocol v1.0
+ Change Poll Mapping Schema.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/actions.yml b/test/fixtures/actions.yml
new file mode 100644
index 000000000..46736e0a1
--- /dev/null
+++ b/test/fixtures/actions.yml
@@ -0,0 +1,5 @@
+contact_update:
+ operation: update
+ contact: john
+ created_at: <%= Time.zone.parse('2010-07-05').to_s(:db) %>
+ user: registrant
\ No newline at end of file
diff --git a/test/fixtures/contacts.yml b/test/fixtures/contacts.yml
index 8a1232326..36d0a4ec5 100644
--- a/test/fixtures/contacts.yml
+++ b/test/fixtures/contacts.yml
@@ -9,8 +9,8 @@ john:
code: john-001
auth_info: cacb5b
uuid: eb2f2766-b44c-4e14-9f16-32ab1a7cb957
- created_at: <%= Time.zone.parse('2010-07-05').to_s(:db) %>
- updated_at: <%= Time.zone.parse('2010-07-06').to_s(:db) %>
+ created_at: <%= Time.zone.parse('2010-07-05') %>
+ updated_at: <%= Time.zone.parse('2010-07-06') %>
william: &william
name: William
diff --git a/test/fixtures/notifications.yml b/test/fixtures/notifications.yml
index 79acd4781..bb77c1f36 100644
--- a/test/fixtures/notifications.yml
+++ b/test/fixtures/notifications.yml
@@ -4,8 +4,8 @@ greeting:
registrar: bestnames
created_at: <%= Time.zone.parse('2010-07-04') %>
-domain_deleted:
- text: Your domain has been deleted
+complete:
+ text: Your domain has been updated
read: false
registrar: bestnames
created_at: <%= Time.zone.parse('2010-07-05') %>
diff --git a/test/integration/api/v1/registrant/contacts/update_test.rb b/test/integration/api/v1/registrant/contacts/update_test.rb
new file mode 100644
index 000000000..33ff41710
--- /dev/null
+++ b/test/integration/api/v1/registrant/contacts/update_test.rb
@@ -0,0 +1,179 @@
+require 'test_helper'
+require 'auth_token/auth_token_creator'
+
+class RegistrantApiV1ContactUpdateTest < ActionDispatch::IntegrationTest
+ setup do
+ @contact = contacts(:john)
+
+ @original_address_processing_setting = Setting.address_processing
+ @original_business_registry_cache_setting = Setting.days_to_keep_business_registry_cache
+ @original_fax_enabled_setting = ENV['fax_enabled']
+
+ Setting.days_to_keep_business_registry_cache = 1
+ travel_to Time.zone.parse('2010-07-05')
+ end
+
+ teardown do
+ Setting.address_processing = @original_address_processing_setting
+ Setting.days_to_keep_business_registry_cache = @original_business_registry_cache_setting
+ ENV['fax_enabled'] = @original_fax_enabled_setting
+ end
+
+ def test_update_contact
+ patch api_v1_registrant_contact_path(@contact.uuid), { name: 'new name',
+ email: 'new-email@coldmail.test',
+ phone: '+666.6' },
+ 'HTTP_AUTHORIZATION' => auth_token
+ assert_response :ok
+ @contact.reload
+ assert_equal 'new name', @contact.name
+ assert_equal 'new-email@coldmail.test', @contact.email
+ assert_equal '+666.6', @contact.phone
+ end
+
+ def test_notify_registrar
+ assert_difference -> { @contact.registrar.notifications.count } do
+ patch api_v1_registrant_contact_path(@contact.uuid), { name: 'new name' },
+ 'HTTP_AUTHORIZATION' => auth_token
+ end
+ notification = @contact.registrar.notifications.last
+ assert_equal 'Contact john-001 has been updated by registrant', notification.text
+ end
+
+ def test_update_fax_when_enabled
+ ENV['fax_enabled'] = 'true'
+ @contact = contacts(:william)
+
+ patch api_v1_registrant_contact_path(@contact.uuid), { 'fax' => '+777.7' },
+ 'HTTP_AUTHORIZATION' => auth_token
+
+ assert_response :ok
+ @contact.reload
+ assert_equal '+777.7', @contact.fax
+ end
+
+ def test_fax_cannot_be_updated_when_disabled
+ ENV['fax_enabled'] = 'false'
+
+ patch api_v1_registrant_contact_path(@contact.uuid), { 'fax' => '+823.7' },
+ 'HTTP_AUTHORIZATION' => auth_token
+
+ assert_response :bad_request
+ @contact.reload
+ assert_not_equal '+823.7', @contact.fax
+
+ error_msg = 'Fax processing is disabled and therefore cannot be updated'
+ assert_equal ({ errors: [{ address: [error_msg] }] }), JSON.parse(response.body,
+ symbolize_names: true)
+ end
+
+ def test_update_address_when_enabled
+ Setting.address_processing = true
+
+ patch api_v1_registrant_contact_path(@contact.uuid), { 'address[city]' => 'new city',
+ 'address[street]' => 'new street',
+ 'address[zip]' => '92837',
+ 'address[country_code]' => 'RU',
+ 'address[state]' => 'new state' },
+ 'HTTP_AUTHORIZATION' => auth_token
+
+ assert_response :ok
+ @contact.reload
+ assert_equal Contact::Address.new('new street', '92837', 'new city', 'new state', 'RU'),
+ @contact.address
+ end
+
+ def test_address_is_optional_when_enabled
+ @contact = contacts(:william)
+ Setting.address_processing = true
+
+ patch api_v1_registrant_contact_path(@contact.uuid), { 'name' => 'any' },
+ 'HTTP_AUTHORIZATION' => auth_token
+
+ assert_response :ok
+ end
+
+ def test_address_cannot_be_updated_when_disabled
+ @contact = contacts(:william)
+ @original_address = @contact.address
+ Setting.address_processing = false
+
+ patch api_v1_registrant_contact_path(@contact.uuid), { 'address[city]' => 'new city' },
+ 'HTTP_AUTHORIZATION' => auth_token
+
+ @contact.reload
+ assert_response :bad_request
+ assert_equal @original_address, @contact.address
+
+ error_msg = 'Address processing is disabled and therefore cannot be updated'
+ assert_equal ({ errors: [{ address: [error_msg] }] }), JSON.parse(response.body,
+ symbolize_names: true)
+ end
+
+ def test_return_contact_details
+ patch api_v1_registrant_contact_path(@contact.uuid), { name: 'new name' },
+ 'HTTP_AUTHORIZATION' => auth_token
+ assert_equal ({ id: @contact.uuid,
+ name: 'new name',
+ code: @contact.code,
+ fax: @contact.fax,
+ ident: {
+ code: @contact.ident,
+ type: @contact.ident_type,
+ country_code: @contact.ident_country_code,
+ },
+ email: @contact.email,
+ phone: @contact.phone,
+ address: {
+ street: @contact.street,
+ zip: @contact.zip,
+ city: @contact.city,
+ state: @contact.state,
+ country_code: @contact.country_code,
+ },
+ auth_info: @contact.auth_info,
+ statuses: @contact.statuses }), JSON.parse(response.body, symbolize_names: true)
+ end
+
+ def test_errors
+ patch api_v1_registrant_contact_path(@contact.uuid), { phone: 'invalid' },
+ 'HTTP_AUTHORIZATION' => auth_token
+
+ assert_response :bad_request
+ assert_equal ({ errors: { phone: ['Phone nr is invalid'] } }), JSON.parse(response.body,
+ symbolize_names: true)
+ end
+
+ def test_contact_of_another_user_cannot_be_updated
+ @contact = contacts(:jack)
+
+ patch api_v1_registrant_contact_path(@contact.uuid), { name: 'any' },
+ 'HTTP_AUTHORIZATION' => auth_token
+
+ assert_response :not_found
+ @contact.reload
+ assert_not_equal 'any', @contact.name
+ end
+
+ def test_non_existent_contact
+ patch api_v1_registrant_contact_path('non-existent'), nil, 'HTTP_AUTHORIZATION' => auth_token
+ assert_response :not_found
+ assert_equal ({ errors: [{ base: ['Not found'] }] }),
+ JSON.parse(response.body, symbolize_names: true)
+ end
+
+ def test_anonymous_user
+ patch api_v1_registrant_contact_path(@contact.uuid)
+ assert_response :unauthorized
+ assert_equal ({ errors: [{ base: ['Not authorized'] }] }),
+ JSON.parse(response.body, symbolize_names: true)
+ end
+
+ private
+
+ def auth_token
+ token_creator = AuthTokenCreator.create_with_defaults(users(:registrant))
+ hash = token_creator.token_in_hash
+ "Bearer #{hash[:access_token]}"
+ end
+end
\ No newline at end of file
diff --git a/test/integration/epp/poll_test.rb b/test/integration/epp/poll_test.rb
index 884872720..bc3a559cd 100644
--- a/test/integration/epp/poll_test.rb
+++ b/test/integration/epp/poll_test.rb
@@ -1,9 +1,33 @@
require 'test_helper'
class EppPollTest < ApplicationIntegrationTest
+ setup do
+ @notification = notifications(:complete)
+ end
+
# Deliberately does not conform to RFC5730, which requires the first notification to be returned
def test_return_latest_notification_when_queue_is_not_empty
- notification = notifications(:domain_deleted)
+ request_xml = <<-XML
+
+
+
+
+
+
+ XML
+ post '/epp/command/poll', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames'
+
+ xml_doc = Nokogiri::XML(response.body)
+ assert_equal 1301.to_s, xml_doc.at_css('result')[:code]
+ assert_equal 1, xml_doc.css('result').size
+ assert_equal 2.to_s, xml_doc.at_css('msgQ')[:count]
+ assert_equal @notification.id.to_s, xml_doc.at_css('msgQ')[:id]
+ assert_equal Time.zone.parse('2010-07-05').utc.xmlschema, xml_doc.at_css('msgQ qDate').text
+ assert_equal 'Your domain has been updated', xml_doc.at_css('msgQ msg').text
+ end
+
+ def test_return_action_data_when_present
+ @notification.update!(action: actions(:contact_update))
request_xml = <<-XML
@@ -14,14 +38,16 @@ class EppPollTest < ApplicationIntegrationTest
XML
post '/epp/command/poll', { frame: request_xml }, 'HTTP_COOKIE' => 'session=api_bestnames'
- response_xml = Nokogiri::XML(response.body)
- assert_equal 1301.to_s, response_xml.at_css('result')[:code]
- assert_equal 1, response_xml.css('result').size
- assert_equal 2.to_s, response_xml.at_css('msgQ')[:count]
- assert_equal notification.id.to_s, response_xml.at_css('msgQ')[:id]
- assert_equal Time.zone.parse('2010-07-05').utc.xmlschema, response_xml.at_css('msgQ qDate').text
- assert_equal 'Your domain has been deleted', response_xml.at_css('msgQ msg').text
+ xml_doc = Nokogiri::XML(response.body)
+ namespace = 'https://epp.tld.ee/schema/changePoll-1.0.xsd'
+ assert_equal 'update', xml_doc.xpath('//changePoll:operation', 'changePoll' => namespace).text
+ assert_equal Time.zone.parse('2010-07-05').utc.xmlschema,
+ xml_doc.xpath('//changePoll:date', 'changePoll' => namespace).text
+ assert_equal @notification.action.id.to_s, xml_doc.xpath('//changePoll:svTRID',
+ 'changePoll' => namespace).text
+ assert_equal 'Registrant User', xml_doc.xpath('//changePoll:who',
+ 'changePoll' => namespace).text
end
def test_no_notifications
diff --git a/test/models/action_test.rb b/test/models/action_test.rb
new file mode 100644
index 000000000..c68399abe
--- /dev/null
+++ b/test/models/action_test.rb
@@ -0,0 +1,20 @@
+require 'test_helper'
+
+class ActionTest < ActiveSupport::TestCase
+ setup do
+ @action = actions(:contact_update)
+ end
+
+ def test_fixture_is_valid
+ assert @action.valid?
+ end
+
+ def test_invalid_with_unsupported_operation
+ @action.operation = 'invalid'
+ assert @action.invalid?
+ end
+
+ def test_notification_key_for_contact
+ assert_equal :contact_update, @action.notification_key
+ end
+end
\ No newline at end of file
diff --git a/test/models/contact/address_test.rb b/test/models/contact/address_test.rb
new file mode 100644
index 000000000..858a54705
--- /dev/null
+++ b/test/models/contact/address_test.rb
@@ -0,0 +1,16 @@
+require 'test_helper'
+
+class ContactAddressTest < ActiveSupport::TestCase
+ setup do
+ @address = Contact::Address.new('Main Street', '1234', 'NY City', 'NY State', 'US')
+ end
+
+ def test_equal_when_all_parts_are_the_same
+ assert_equal @address, Contact::Address.new('Main Street', '1234', 'NY City', 'NY State', 'US')
+ end
+
+ def test_not_equal_when_some_part_is_different
+ assert_not_equal @address, Contact::Address.new('Main Street', '1234', 'NY City', 'NY State',
+ 'DE')
+ end
+end
\ No newline at end of file
diff --git a/test/models/contact/contact_test.rb b/test/models/contact/contact_test.rb
index b2a7a02a8..0c4fc1e4e 100644
--- a/test/models/contact/contact_test.rb
+++ b/test/models/contact/contact_test.rb
@@ -26,4 +26,16 @@ class ContactTest < ActiveSupport::TestCase
def test_not_in_use_if_acts_as_neither_registrant_nor_domain_contact
refute contacts(:not_in_use).in_use?
end
+
+ def test_managed_when_identity_codes_match
+ contact = Contact.new(ident: '1234')
+ user = RegistrantUser.new(registrant_ident: 'US-1234')
+ assert contact.managed_by?(user)
+ end
+
+ def test_unmanaged_when_identity_codes_do_not_match
+ contact = Contact.new(ident: '1234')
+ user = RegistrantUser.new(registrant_ident: 'US-12345')
+ assert_not contact.managed_by?(user)
+ end
end
diff --git a/test/models/contact/postal_address_test.rb b/test/models/contact/postal_address_test.rb
index baf06d9f4..d98a8019f 100644
--- a/test/models/contact/postal_address_test.rb
+++ b/test/models/contact/postal_address_test.rb
@@ -21,4 +21,13 @@ class ContactPostalAddressTest < ActiveSupport::TestCase
@contact.country_code = 'invalid'
assert @contact.valid?
end
+
+ def test_state_is_optional_when_address_is_enabled
+ Setting.address_processing = true
+ contact = contacts(:william)
+ assert contact.valid?
+
+ contact.state = ''
+ assert contact.valid?
+ end
end
diff --git a/test/models/contact_test.rb b/test/models/contact_test.rb
index 5651cc883..7a383bbae 100644
--- a/test/models/contact_test.rb
+++ b/test/models/contact_test.rb
@@ -34,4 +34,18 @@ class ContactTest < ActiveSupport::TestCase
@contact.phone = '+123.4'
assert @contact.valid?
end
+
+ def test_address
+ address = Contact::Address.new('new street', '83746', 'new city', 'new state', 'EE')
+ @contact.address = address
+ @contact.save!
+ @contact.reload
+
+ assert_equal 'new street', @contact.street
+ assert_equal '83746', @contact.zip
+ assert_equal 'new city', @contact.city
+ assert_equal 'new state', @contact.state
+ assert_equal 'EE', @contact.country_code
+ assert_equal address, @contact.address
+ end
end
\ No newline at end of file
diff --git a/test/models/registrant_user_test.rb b/test/models/registrant_user_test.rb
index 298d3a096..ac93b43db 100644
--- a/test/models/registrant_user_test.rb
+++ b/test/models/registrant_user_test.rb
@@ -35,4 +35,14 @@ class RegistrantUserTest < ActiveSupport::TestCase
assert_equal('1234', @user.ident)
assert_equal('US', @user.country_code)
end
+
+ def test_first_name_from_username
+ user = RegistrantUser.new(username: 'John Doe')
+ assert_equal 'John', user.first_name
+ end
+
+ def test_last_name_from_username
+ user = RegistrantUser.new(username: 'John Doe')
+ assert_equal 'Doe', user.last_name
+ end
end
diff --git a/test/system/registrant_area/contacts/update_test.rb b/test/system/registrant_area/contacts/update_test.rb
new file mode 100644
index 000000000..7bb3e3909
--- /dev/null
+++ b/test/system/registrant_area/contacts/update_test.rb
@@ -0,0 +1,177 @@
+require 'test_helper'
+
+class RegistrantAreaContactUpdateTest < ApplicationIntegrationTest
+ setup do
+ @domain = domains(:shop)
+ @contact = contacts(:john)
+ sign_in users(:registrant)
+
+ @original_address_processing_setting = Setting.address_processing
+ @original_business_registry_cache_setting = Setting.days_to_keep_business_registry_cache
+ @original_fax_enabled_setting = ENV['fax_enabled']
+ @original_registrant_api_base_url_setting = ENV['registrant_api_base_url']
+
+ ENV['registrant_api_base_url'] = 'https://api.test'
+ Setting.days_to_keep_business_registry_cache = 1
+ travel_to Time.zone.parse('2010-07-05')
+ end
+
+ teardown do
+ Setting.address_processing = @original_address_processing_setting
+ Setting.days_to_keep_business_registry_cache = @original_business_registry_cache_setting
+ ENV['fax_enabled'] = @original_fax_enabled_setting
+ ENV['registrant_api_base_url'] = @original_registrant_api_base_url_setting
+ end
+
+ def test_form_is_pre_populated_with_contact_data
+ visit edit_registrant_domain_contact_url(@domain, @contact)
+
+ assert_field 'Name', with: 'John'
+ assert_field 'Email', with: 'john@inbox.test'
+ assert_field 'Phone', with: '+555.555'
+ end
+
+ def test_update_contact
+ stub_auth_request
+
+ request_body = { name: 'new name', email: 'new@inbox.test', phone: '+666.6' }
+ headers = { 'Authorization' => 'Bearer test-access-token' }
+ url = "https://api.test/api/v1/registrant/contacts/#{@contact.uuid}"
+ update_request_stub = stub_request(:patch, url).with(body: request_body, headers: headers)
+ .to_return(body: '{}', status: 200)
+
+ visit registrant_domain_contact_url(@domain, @contact)
+ click_link_or_button 'Edit'
+
+ fill_in 'Name', with: 'new name'
+ fill_in 'Email', with: 'new@inbox.test'
+ fill_in 'Phone', with: '+666.6'
+
+ click_link_or_button 'Update contact'
+
+ assert_requested update_request_stub
+ assert_current_path registrant_domain_contact_path(@domain, @contact)
+ assert_text 'Contact has been successfully updated'
+ end
+
+ def test_form_is_pre_populated_with_fax_when_enabled
+ ENV['fax_enabled'] = 'true'
+ @contact.update!(fax: '+111.1')
+
+ visit edit_registrant_domain_contact_url(@domain, @contact)
+ assert_field 'Fax', with: '+111.1'
+ end
+
+ def test_update_fax_when_enabled
+ ENV['fax_enabled'] = 'true'
+ stub_auth_request
+
+ request_body = { email: 'john@inbox.test', name: 'John', phone: '+555.555', fax: '+222.2' }
+ headers = { 'Authorization' => 'Bearer test-access-token' }
+ url = "https://api.test/api/v1/registrant/contacts/#{@contact.uuid}"
+ update_request_stub = stub_request(:patch, url).with(body: request_body, headers: headers)
+ .to_return(body: '{}', status: 200)
+
+ visit edit_registrant_domain_contact_url(@domain, @contact)
+
+ fill_in 'Fax', with: '+222.2'
+ click_link_or_button 'Update contact'
+
+ assert_requested update_request_stub
+ assert_current_path registrant_domain_contact_path(@domain, @contact)
+ assert_text 'Contact has been successfully updated'
+ end
+
+ def test_hide_fax_field_when_disabled
+ visit edit_registrant_domain_contact_url(@domain, @contact)
+ assert_no_field 'Fax'
+ end
+
+ def test_form_is_pre_populated_with_address_when_enabled
+ Setting.address_processing = true
+ @contact = contacts(:william)
+
+ visit edit_registrant_domain_contact_url(@domain, @contact)
+
+ assert_field 'Street', with: 'Main Street'
+ assert_field 'Zip', with: '12345'
+ assert_field 'City', with: 'New York'
+ assert_field 'State', with: 'New York State'
+ assert_select 'Country', selected: 'United States'
+ end
+
+ def test_update_address_when_enabled
+ Setting.address_processing = true
+ stub_auth_request
+
+ request_body = { email: 'john@inbox.test',
+ name: 'John',
+ phone: '+555.555',
+ address: {
+ street: 'new street',
+ zip: '93742',
+ city: 'new city',
+ state: 'new state',
+ country_code: 'AT'
+ } }
+ headers = { 'Authorization' => 'Bearer test-access-token' }
+ url = "https://api.test/api/v1/registrant/contacts/#{@contact.uuid}"
+ update_request_stub = stub_request(:patch, url).with(body: request_body, headers: headers)
+ .to_return(body: '{}', status: 200)
+
+ visit edit_registrant_domain_contact_url(@domain, @contact)
+
+ fill_in 'Street', with: 'new street'
+ fill_in 'City', with: 'new city'
+ fill_in 'State', with: 'new state'
+ fill_in 'Zip', with: '93742'
+ select 'Austria', from: 'Country'
+ click_link_or_button 'Update contact'
+
+ assert_requested update_request_stub
+ assert_current_path registrant_domain_contact_path(@domain, @contact)
+ assert_text 'Contact has been successfully updated'
+ end
+
+ def test_hide_address_field_when_disabled
+ visit edit_registrant_domain_contact_url(@domain, @contact)
+ assert_no_field 'Address'
+ assert_no_field 'Street'
+ end
+
+ def test_unmanaged_contact_cannot_be_updated
+ @contact.update!(ident: '12345')
+ visit registrant_domain_contact_url(@domain, @contact)
+ assert_no_button 'Edit'
+ assert_no_link 'Edit'
+ end
+
+ def test_fail_gracefully
+ stub_auth_request
+
+ response_body = { errors: { name: ['Name is invalid'] } }.to_json
+ headers = { 'Authorization' => 'Bearer test-access-token' }
+ stub_request(:patch, "https://api.test/api/v1/registrant/contacts/#{@contact.uuid}")
+ .with(headers: headers)
+ .to_return(body: response_body, status: 400)
+
+ visit edit_registrant_domain_contact_url(@domain, @contact)
+ fill_in 'Name', with: 'invalid name'
+ click_link_or_button 'Update contact'
+
+ assert_current_path registrant_domain_contact_path(@domain, @contact)
+ assert_text 'Name is invalid'
+ assert_field 'Name', with: 'invalid name'
+ assert_no_text 'Contact has been successfully updated'
+ end
+
+ private
+
+ def stub_auth_request
+ body = { ident: '1234', first_name: 'Registrant', last_name: 'User' }
+ stub_request(:post, 'https://api.test/api/v1/registrant/auth/eid').with(body: body)
+ .to_return(body: { access_token: 'test-access-token' }.to_json,
+ headers: { 'Content-type' => 'application/json' },
+ status: 200)
+ end
+end