Add Registrant API contact update action

Closes #849
This commit is contained in:
Artur Beljajev 2018-08-20 17:19:54 +03:00
parent 90ed23f64d
commit b6ecae6a35
41 changed files with 1239 additions and 61 deletions

View file

@ -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

View file

@ -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

View file

@ -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
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

17
app/models/action.rb Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,7 @@
<div class="alert alert-danger">
<ul>
<% errors.each_value do |errors| %>
<li><%= errors.join('<br>') %></li>
<% end %>
</ul>
</div>

View file

@ -0,0 +1,64 @@
<%= form_for [:registrant, domain, @contact], html: { class: 'form-horizontal' } do |f| %>
<% if @errors.present? %>
<%= render 'api_errors', errors: @errors %>
<% end %>
<div class="form-group">
<div class="col-md-2 control-label">
<%= f.label :name %>
</div>
<div class="col-md-4">
<%= f.text_field :name, required: true, autofocus: true, class: 'form-control' %>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<%= f.label :email %>
</div>
<div class="col-md-4">
<%= f.email_field :email, required: true, class: 'form-control' %>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<%= f.label :phone %>
</div>
<div class="col-md-4">
<%= f.text_field :phone, required: true, class: 'form-control' %>
</div>
</div>
<% if Contact.address_processing? %>
<div class="panel panel-default">
<div class="panel-heading"><%= t '.address' %></div>
<div class="panel-body">
<%= render 'registrant/contacts/form/address', f: f %>
</div>
</div>
<% end %>
<% if fax_enabled? %>
<div class="form-group">
<div class="col-md-2 control-label">
<%= f.label :fax %>
</div>
<div class="col-md-4">
<%= f.text_field :fax, class: 'form-control' %>
</div>
</div>
<% end %>
<hr/>
<div class="row">
<div class="col-md-6 text-right">
<%= button_tag t('.submit_btn'), class: 'btn btn-success' %>
</div>
</div>
<% end %>

View file

@ -0,0 +1,12 @@
<ol class="breadcrumb">
<li><%= link_to t('registrant.domains.index.header'), registrant_domains_path %></li>
<li><%= link_to domain, registrant_domain_path(domain) %></li>
<li><%= t 'registrant.contacts.contact_index' %></li>
<li><%= link_to @contact, registrant_domain_contact_path(domain, @contact) %></li>
</ol>
<div class="page-header">
<h1><%= t '.header' %></h1>
</div>
<%= render 'form' %>

View file

@ -0,0 +1,51 @@
<div class="form-group">
<div class="col-md-2 control-label">
<%= f.label :street %>
</div>
<div class="col-md-4">
<%= f.text_field :street, required: true, class: 'form-control' %>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<%= f.label :zip %>
</div>
<div class="col-md-4">
<%= f.text_field :zip, required: true, class: 'form-control' %>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<%= f.label :city %>
</div>
<div class="col-md-4">
<%= f.text_field :city, required: true, class: 'form-control' %>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<%= f.label :state %>
</div>
<div class="col-md-4">
<%= f.text_field :state, class: 'form-control' %>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<%= f.label :country_code, 'Country' %>
</div>
<div class="col-md-4">
<%= f.select :country_code, SortedCountry.all_options(f.object.country_code), {},
required: true,
class: 'form-control' %>
</div>
</div>

View file

@ -5,7 +5,18 @@
</ol>
<div class="page-header">
<h1><%= @contact.name %></h1>
<div class="row">
<div class="col-md-6">
<h1><%= @contact %></h1>
</div>
<% if @contact.managed_by?(current_registrant_user) %>
<div class="col-md-6 text-right">
<%= link_to t('.edit_btn'), edit_registrant_domain_contact_path(domain, @contact),
class: 'btn btn-primary' %>
</div>
<% end %>
</div>
</div>
<div class="row">

View file

@ -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
#

View file

@ -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

View file

@ -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
all: All roles
edit:
header: Edit contact
update:
updated: Contact has been successfully updated
form:
address: Address
submit_btn: Update contact

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,5 @@
class ChangeActionsOperationToNotNull < ActiveRecord::Migration
def change
change_column_null :actions, :operation, false
end
end

View file

@ -0,0 +1,5 @@
class AddContactIdToActions < ActiveRecord::Migration
def change
add_reference :actions, :contact, foreign_key: true
end
end

View file

@ -0,0 +1,5 @@
class AddActionIdToNotifications < ActiveRecord::Migration
def change
add_reference :notifications, :action, foreign_key: true
end
end

View file

@ -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');

View file

@ -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"]
}
}
```

View file

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<schema targetNamespace="urn:ietf:params:xml:ns:changePoll-1.0"
xmlns:eppcom="urn:ietf:params:xml:ns:eppcom-1.0"
xmlns:epp="urn:ietf:params:xml:ns:epp-1.0"
xmlns:changePoll="urn:ietf:params:xml:ns:changePoll-1.0"
xmlns="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified">
<!--
Import common element types.
-->
<import namespace="urn:ietf:params:xml:ns:eppcom-1.0"/>
<import namespace="urn:ietf:params:xml:ns:epp-1.0"/>
<annotation>
<documentation>
Extensible Provisioning Protocol v1.0
Change Poll Mapping Schema.
</documentation>
</annotation>
<!--
Change element.
-->
<element name="changeData" type="changePoll:changeDataType"/>
<!--
Attributes associated with the change.
-->
<complexType name="changeDataType">
<sequence>
<element name="operation" type="changePoll:operationType"/>
<element name="date" type="dateTime"/>
<element name="svTRID" type="epp:trIDStringType"/>
<element name="who" type="changePoll:whoType"/>
<element name="caseId" type="changePoll:caseIdType"
minOccurs="0"/>
<element name="reason" type="eppcom:reasonType"
minOccurs="0"/>
</sequence>
<attribute name="state" type="changePoll:stateType"
default="after"/>
</complexType>
<!--
Enumerated list of operations, with extensibility via "custom".
-->
<simpleType name="operationEnum">
<restriction base="token">
<enumeration value="create"/>
<enumeration value="delete"/>
<enumeration value="renew"/>
<enumeration value="transfer"/>
<enumeration value="update"/>
<enumeration value="restore"/>
<enumeration value="autoRenew"/>
<enumeration value="autoDelete"/>
<enumeration value="autoPurge"/>
<enumeration value="custom"/>
</restriction>
</simpleType>
<!--
Enumerated of state of the object in the poll message.
-->
<simpleType name="stateType">
<restriction base="token">
<enumeration value="before"/>
<enumeration value="after"/>
</restriction>
</simpleType>
<!--
Transform operation type
-->
<complexType name="operationType">
<simpleContent>
<extension base="changePoll:operationEnum">
<attribute name="op" type="token"/>
</extension>
</simpleContent>
</complexType>
<!--
Case identifier type
-->
<complexType name="caseIdType">
<simpleContent>
<extension base="token">
<attribute name="type" type="changePoll:caseTypeEnum"
use="required"/>
<attribute name="name" type="token"
use="optional"/>
</extension>
</simpleContent>
</complexType>
<!--
Enumerated list of case identifier types
-->
<simpleType name="caseTypeEnum">
<restriction base="token">
<enumeration value="udrp"/>
<enumeration value="urs"/>
<enumeration value="custom"/>
</restriction>
</simpleType>
<!--
Who type
-->
<simpleType name="whoType">
<restriction base="normalizedString">
<minLength value="1"/>
<maxLength value="255"/>
</restriction>
</simpleType>
<!--
End of schema.
-->
</schema>

5
test/fixtures/actions.yml vendored Normal file
View file

@ -0,0 +1,5 @@
contact_update:
operation: update
contact: john
created_at: <%= Time.zone.parse('2010-07-05').to_s(:db) %>
user: registrant

View file

@ -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

View file

@ -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') %>

View file

@ -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

View file

@ -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 version="1.0" encoding="UTF-8" standalone="no"?>
<epp xmlns="https://epp.tld.ee/schema/epp-ee-1.0.xsd">
<command>
<poll op="req"/>
</command>
</epp>
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
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
@ -14,14 +38,16 @@ class EppPollTest < ApplicationIntegrationTest
</epp>
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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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