From da7cc2fbd950cf2826c94fa0ea1f4a004581f684 Mon Sep 17 00:00:00 2001 From: Artur Beljajev Date: Mon, 20 Aug 2018 17:19:54 +0300 Subject: [PATCH] Add Registrant API contact update action Closes #849 --- .../api/v1/registrant/base_controller.rb | 10 + .../api/v1/registrant/contacts_controller.rb | 57 ++++++ .../registrant/contacts_controller.rb | 85 +++++++++ app/models/action.rb | 17 ++ app/models/contact.rb | 16 ++ app/models/contact/address.rb | 25 +++ app/models/notification.rb | 4 +- app/models/registrant_user.rb | 8 + app/models/registrar.rb | 5 + app/models/user.rb | 2 + app/views/epp/poll/_action.xml.builder | 9 + app/views/epp/poll/_contact.xml.builder | 72 +++++++ app/views/epp/poll/poll_req.xml.builder | 15 ++ .../registrant/contacts/_api_errors.html.erb | 7 + app/views/registrant/contacts/_form.html.erb | 64 +++++++ app/views/registrant/contacts/edit.html.erb | 12 ++ .../contacts/form/_address.html.erb | 51 +++++ app/views/registrant/contacts/show.html.erb | 13 +- config/application-example.yml | 1 + config/locales/notifications.en.yml | 1 + config/locales/registrant/contacts.en.yml | 14 +- config/routes.rb | 4 +- db/migrate/20180824215326_create_actions.rb | 9 + ...37_change_actions_operation_to_not_null.rb | 5 + ...0180825232819_add_contact_id_to_actions.rb | 5 + ...26162821_add_action_id_to_notifications.rb | 5 + db/structure.sql | 84 +++++++- doc/registrant-api/v1/contact.md | 77 ++++---- lib/schemas/changePoll-1.0.xsd | 123 ++++++++++++ test/fixtures/actions.yml | 5 + test/fixtures/contacts.yml | 4 +- test/fixtures/notifications.yml | 4 +- .../api/v1/registrant/contacts/update_test.rb | 179 ++++++++++++++++++ test/integration/epp/poll_test.rb | 44 ++++- test/models/action_test.rb | 20 ++ test/models/contact/address_test.rb | 16 ++ test/models/contact/contact_test.rb | 12 ++ test/models/contact/postal_address_test.rb | 9 + test/models/contact_test.rb | 14 ++ test/models/registrant_user_test.rb | 10 + .../registrant_area/contacts/update_test.rb | 173 +++++++++++++++++ 41 files changed, 1232 insertions(+), 58 deletions(-) create mode 100644 app/models/action.rb create mode 100644 app/models/contact/address.rb create mode 100644 app/views/epp/poll/_action.xml.builder create mode 100644 app/views/epp/poll/_contact.xml.builder create mode 100644 app/views/registrant/contacts/_api_errors.html.erb create mode 100644 app/views/registrant/contacts/_form.html.erb create mode 100644 app/views/registrant/contacts/edit.html.erb create mode 100644 app/views/registrant/contacts/form/_address.html.erb create mode 100644 db/migrate/20180824215326_create_actions.rb create mode 100644 db/migrate/20180825193437_change_actions_operation_to_not_null.rb create mode 100644 db/migrate/20180825232819_add_contact_id_to_actions.rb create mode 100644 db/migrate/20180826162821_add_action_id_to_notifications.rb create mode 100644 lib/schemas/changePoll-1.0.xsd create mode 100644 test/fixtures/actions.yml create mode 100644 test/integration/api/v1/registrant/contacts/update_test.rb create mode 100644 test/models/action_test.rb create mode 100644 test/models/contact/address_test.rb create mode 100644 test/system/registrant_area/contacts/update_test.rb diff --git a/app/controllers/api/v1/registrant/base_controller.rb b/app/controllers/api/v1/registrant/base_controller.rb index 4497d68e6a..4fec4ee26b 100644 --- a/app/controllers/api/v1/registrant/base_controller.rb +++ b/app/controllers/api/v1/registrant/base_controller.rb @@ -8,6 +8,8 @@ class BaseController < ActionController::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 @@ def authenticate 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 1be620ba46..fcce291f93 100644 --- a/app/controllers/api/v1/registrant/contacts_controller.rb +++ b/app/controllers/api/v1/registrant/contacts_controller.rb @@ -32,6 +32,63 @@ def show 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 b2ebad344a..8bbfc06994 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 @@ def show 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 @@ def current_user_domains current_registrant_user.domains end end + + 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_url']}/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) 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_url']}/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) do |http| + http.request(request) + end + end end \ No newline at end of file diff --git a/app/models/action.rb b/app/models/action.rb new file mode 100644 index 0000000000..2a60f9a239 --- /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 088ec0b595..9d166faee6 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -526,4 +526,20 @@ def domain_names_with_roles 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 0000000000..f7a6cfdab8 --- /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 0b1829267f..d6427323b2 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 @@ def unread? # Needed for EPP log def name - "-" + '' end private diff --git a/app/models/registrant_user.rb b/app/models/registrant_user.rb index ddbf2e664e..4cf0949d4d 100644 --- a/app/models/registrant_user.rb +++ b/app/models/registrant_user.rb @@ -56,6 +56,14 @@ def to_s 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 611dfc562b..e939784de5 100644 --- a/app/models/registrar.rb +++ b/app/models/registrar.rb @@ -157,6 +157,11 @@ def effective_vat_rate 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 8968e27369..573cddc940 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 0000000000..72d2c1cacf --- /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 0000000000..731711e52d --- /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 9ddef8d091..b0c9f40e37 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 0000000000..35617fa996 --- /dev/null +++ b/app/views/registrant/contacts/_api_errors.html.erb @@ -0,0 +1,7 @@ +
+ +
\ 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 0000000000..f203f39e8d --- /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 %> + +
+
+ <%= f.label :name %> +
+ +
+ <%= f.text_field :name, required: true, autofocus: true, class: 'form-control' %> +
+
+ +
+
+ <%= f.label :email %> +
+ +
+ <%= f.email_field :email, required: true, class: 'form-control' %> +
+
+ +
+
+ <%= f.label :phone %> +
+ +
+ <%= f.text_field :phone, required: true, class: 'form-control' %> +
+
+ + <% if Contact.address_processing? %> +
+
<%= t '.address' %>
+
+ <%= render 'registrant/contacts/form/address', f: f %> +
+
+ <% end %> + + <% if fax_enabled? %> +
+
+ <%= f.label :fax %> +
+ +
+ <%= f.text_field :fax, class: 'form-control' %> +
+
+ <% 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 0000000000..0a453ded10 --- /dev/null +++ b/app/views/registrant/contacts/edit.html.erb @@ -0,0 +1,12 @@ + + + + +<%= 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 0000000000..a43784d3f4 --- /dev/null +++ b/app/views/registrant/contacts/form/_address.html.erb @@ -0,0 +1,51 @@ +
+
+ <%= f.label :street %> +
+ +
+ <%= f.text_field :street, required: true, class: 'form-control' %> +
+
+ +
+
+ <%= f.label :zip %> +
+ +
+ <%= f.text_field :zip, required: true, class: 'form-control' %> +
+
+ +
+
+ <%= f.label :city %> +
+ +
+ <%= f.text_field :city, required: true, class: 'form-control' %> +
+
+ +
+
+ <%= f.label :state %> +
+ +
+ <%= f.text_field :state, class: 'form-control' %> +
+
+ +
+
+ <%= f.label :country_code, 'Country' %> +
+ +
+ <%= f.select :country_code, SortedCountry.all_options(f.object.country_code), {}, + required: true, + class: 'form-control' %> +
+
\ 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 f9a8a86fab..1f0a87b5fa 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 b9917e69e2..62514ea940 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_url: https://api.registry.example 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 46e03c689e..1dff4a97c6 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 a447558322..4201bf1b63 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 ff33ec6522..a40ad9297c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,7 +26,7 @@ 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 @@ 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 0000000000..8bacc9a341 --- /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 0000000000..ce1cd2b9d3 --- /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 0000000000..a6b10a2569 --- /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 0000000000..7e52fabecb --- /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 21d39236ea..dcafe99a30 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: -- @@ -2007,7 +2039,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 ); @@ -2487,6 +2520,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: - -- @@ -2874,6 +2914,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: -- @@ -4058,6 +4106,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: - -- @@ -4778,3 +4850,13 @@ 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'); + diff --git a/doc/registrant-api/v1/contact.md b/doc/registrant-api/v1/contact.md index 1102752b33..74c78bd5ad 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"], + "id": "84c62f3d-e56f-40fa-9ca4-dc0137778949", + "name": "Karson Kessler0", "code": "REGISTRAR2:SH022086480", - "phone": "+372.12345671", + "ident": { + "code": "37605030299", + "type": "priv", + "country_code": "EE" + }, "email": "foo@bar.baz", + "phone": "+372.12345671", "fax": "+372.12345672", - "created_at": "2015-09-09T09:11:14.130Z", - "updated_at": "2018-09-09T09:11:14.130Z", - "ident": "37605030299", - "ident_type": "priv", + "address": { + "street": "Main Street 123", + "zip": "22222", + "city": "New City", + "state": "New state", + "country_code": "LV" + }, "auth_info": "password", - "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, "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 0000000000..ebdf11443e --- /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 0000000000..46736e0a17 --- /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 8a12323268..36d0a4ec57 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 79acd47810..bb77c1f36e 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 0000000000..33ff417101 --- /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 8848727204..bc3a559cd4 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 @@ def test_return_latest_notification_when_queue_is_not_empty 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 0000000000..c68399abef --- /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 0000000000..858a547058 --- /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 b2a7a02a8a..0c4fc1e4e3 100644 --- a/test/models/contact/contact_test.rb +++ b/test/models/contact/contact_test.rb @@ -26,4 +26,16 @@ def test_in_use_if_acts_as_a_domain_contact 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 baf06d9f4a..d98a8019fa 100644 --- a/test/models/contact/postal_address_test.rb +++ b/test/models/contact/postal_address_test.rb @@ -21,4 +21,13 @@ def test_valid_if_country_code_is_invalid_and_address_processing_is_off @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 5651cc8832..7a383bbae4 100644 --- a/test/models/contact_test.rb +++ b/test/models/contact_test.rb @@ -34,4 +34,18 @@ def test_phone_format_validation @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 298d3a0962..ac93b43db2 100644 --- a/test/models/registrant_user_test.rb +++ b/test/models/registrant_user_test.rb @@ -35,4 +35,14 @@ def test_ident_and_country_code_helper_methods 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 0000000000..a11fdaef17 --- /dev/null +++ b/test/system/registrant_area/contacts/update_test.rb @@ -0,0 +1,173 @@ +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'] + + 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_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' } + update_request_stub = stub_request(:patch, /v1\/registrant\/contacts/).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' } + update_request_stub = stub_request(:patch, /v1\/registrant\/contacts/).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' } + update_request_stub = stub_request(:patch, /v1\/registrant\/contacts/).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, /v1\/registrant\/contacts/).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, /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 \ No newline at end of file