Incorporate some of the fixes done in RegistrarPremiumPriceAckAction

This is in preparation for merging and then removing
RegistrarPremiumPriceAckAction.

This includes:

test that the data the UI sent isn't stale
---------------------------------------------
Our system is "read, modify, write". However, if between the "read" and the "write" someone else changed the registry, my write will undo their change even if I didn't touch any of their fields.
To solve that - we use the "lastUpdateTime" timestamp of the registrar. the UI reads it with the rest of the data, and sends it back on "write". We will now make sure the registrar currently in datastore has the same timestamp.

support premium-price-ack flag
---------------------------------
Add support for reading and writing this flag. We still won't be using it - that's in a followup CL, but we support it.

support changing the URL
------------------------
Add changing the URL in the UI, under the "whois" section

Will replace the Ack endpoint with this (and remove that endpoint) in a followup CL

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=192154078
This commit is contained in:
guyben 2018-04-09 11:08:48 -07:00 committed by Ben McIlwain
parent 3bbaf585e5
commit 38bf86c0fd
9 changed files with 237 additions and 147 deletions

View file

@ -78,6 +78,8 @@ registry.json.Response.prototype.results;
* icannReferralEmail: string, * icannReferralEmail: string,
* ipAddressWhitelist: !Array<string>, * ipAddressWhitelist: !Array<string>,
* emailAddress: (string?|undefined), * emailAddress: (string?|undefined),
* lastUpdateTime: string,
* url: (string?|undefined),
* phonePasscode: (string?|undefined), * phonePasscode: (string?|undefined),
* phoneNumber: (string?|undefined), * phoneNumber: (string?|undefined),
* faxNumber: (string?|undefined), * faxNumber: (string?|undefined),

View file

@ -37,7 +37,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import javax.annotation.Nullable;
import org.joda.time.DateTime;
/** Form fields for validating input for the {@code Registrar} class. */ /** Form fields for validating input for the {@code Registrar} class. */
public final class RegistrarFormFields { public final class RegistrarFormFields {
@ -46,31 +47,18 @@ public final class RegistrarFormFields {
public static final Pattern ASCII_PATTERN = Pattern.compile("[[:ascii:]]*"); public static final Pattern ASCII_PATTERN = Pattern.compile("[[:ascii:]]*");
public static final String ASCII_ERROR = "Please only use ASCII-US characters."; public static final String ASCII_ERROR = "Please only use ASCII-US characters.";
private static final Function<String, CidrAddressBlock> CIDR_TRANSFORM =
input -> {
try {
return input != null ? CidrAddressBlock.create(input) : null;
} catch (IllegalArgumentException e) {
throw new FormFieldException("Not a valid CIDR notation IP-address block.", e);
}
};
private static final Function<String, String> HOSTNAME_TRANSFORM =
input -> {
if (input == null) {
return null;
}
if (!InternetDomainName.isValid(input)) {
throw new FormFieldException("Not a valid hostname.");
}
return canonicalizeDomainName(input);
};
public static final FormField<String, String> NAME_FIELD = public static final FormField<String, String> NAME_FIELD =
FormFields.NAME.asBuilderNamed("registrarName") FormFields.NAME.asBuilderNamed("registrarName")
.required() .required()
.build(); .build();
public static final FormField<String, DateTime> LAST_UPDATE_TIME =
FormFields.LABEL
.asBuilderNamed("lastUpdateTime")
.transform(DateTime.class, RegistrarFormFields::parseDateTime)
.required()
.build();
public static final FormField<String, String> EMAIL_ADDRESS_FIELD_REQUIRED = public static final FormField<String, String> EMAIL_ADDRESS_FIELD_REQUIRED =
FormFields.EMAIL.asBuilderNamed("emailAddress") FormFields.EMAIL.asBuilderNamed("emailAddress")
.matches(ASCII_PATTERN, ASCII_ERROR) .matches(ASCII_PATTERN, ASCII_ERROR)
@ -111,7 +99,7 @@ public final class RegistrarFormFields {
public static final FormField<String, String> WHOIS_SERVER_FIELD = public static final FormField<String, String> WHOIS_SERVER_FIELD =
FormFields.LABEL.asBuilderNamed("whoisServer") FormFields.LABEL.asBuilderNamed("whoisServer")
.transform(HOSTNAME_TRANSFORM) .transform(RegistrarFormFields::parseHostname)
.build(); .build();
public static final FormField<Boolean, Boolean> BLOCK_PREMIUM_NAMES_FIELD = public static final FormField<Boolean, Boolean> BLOCK_PREMIUM_NAMES_FIELD =
@ -173,7 +161,7 @@ public final class RegistrarFormFields {
public static final FormField<List<String>, List<CidrAddressBlock>> IP_ADDRESS_WHITELIST_FIELD = public static final FormField<List<String>, List<CidrAddressBlock>> IP_ADDRESS_WHITELIST_FIELD =
FormField.named("ipAddressWhitelist") FormField.named("ipAddressWhitelist")
.emptyToNull() .emptyToNull()
.transform(CidrAddressBlock.class, CIDR_TRANSFORM) .transform(CidrAddressBlock.class, RegistrarFormFields::parseCidr)
.asList() .asList()
.build(); .build();
@ -228,34 +216,9 @@ public final class RegistrarFormFields {
.asSet(Splitter.on(',').omitEmptyStrings().trimResults()) .asSet(Splitter.on(',').omitEmptyStrings().trimResults())
.build(); .build();
public static final Function<Map<String, ?>, RegistrarContact.Builder>
REGISTRAR_CONTACT_TRANSFORM =
args -> {
if (args == null) {
return null;
}
RegistrarContact.Builder builder = new RegistrarContact.Builder();
CONTACT_NAME_FIELD.extractUntyped(args).ifPresent(builder::setName);
CONTACT_EMAIL_ADDRESS_FIELD.extractUntyped(args).ifPresent(builder::setEmailAddress);
CONTACT_VISIBLE_IN_WHOIS_AS_ADMIN_FIELD
.extractUntyped(args)
.ifPresent(builder::setVisibleInWhoisAsAdmin);
CONTACT_VISIBLE_IN_WHOIS_AS_TECH_FIELD
.extractUntyped(args)
.ifPresent(builder::setVisibleInWhoisAsTech);
PHONE_AND_EMAIL_VISIBLE_IN_DOMAIN_WHOIS_AS_ABUSE_FIELD
.extractUntyped(args)
.ifPresent(builder::setVisibleInDomainWhoisAsAbuse);
CONTACT_PHONE_NUMBER_FIELD.extractUntyped(args).ifPresent(builder::setPhoneNumber);
CONTACT_FAX_NUMBER_FIELD.extractUntyped(args).ifPresent(builder::setFaxNumber);
CONTACT_TYPES.extractUntyped(args).ifPresent(builder::setTypes);
CONTACT_GAE_USER_ID_FIELD.extractUntyped(args).ifPresent(builder::setGaeUserId);
return builder;
};
public static final FormField<List<Map<String, ?>>, List<RegistrarContact.Builder>> public static final FormField<List<Map<String, ?>>, List<RegistrarContact.Builder>>
CONTACTS_FIELD = FormField.mapNamed("contacts") CONTACTS_FIELD = FormField.mapNamed("contacts")
.transform(RegistrarContact.Builder.class, REGISTRAR_CONTACT_TRANSFORM) .transform(RegistrarContact.Builder.class, RegistrarFormFields::toRegistrarContactBuilder)
.asList() .asList()
.build(); .build();
@ -316,19 +279,19 @@ public final class RegistrarFormFields {
public static final FormField<Map<String, ?>, RegistrarAddress> L10N_ADDRESS_FIELD = public static final FormField<Map<String, ?>, RegistrarAddress> L10N_ADDRESS_FIELD =
FormField.mapNamed("localizedAddress") FormField.mapNamed("localizedAddress")
.transform(RegistrarAddress.class, newAddressTransform( .transform(RegistrarAddress.class, (args) -> toNewAddress(
L10N_STREET_FIELD, L10N_CITY_FIELD, L10N_STATE_FIELD, L10N_ZIP_FIELD)) args, L10N_STREET_FIELD, L10N_CITY_FIELD, L10N_STATE_FIELD, L10N_ZIP_FIELD))
.build(); .build();
public static final FormField<Boolean, Boolean> PREMIUM_PRICE_ACK_REQUIRED = public static final FormField<Boolean, Boolean> PREMIUM_PRICE_ACK_REQUIRED =
FormField.named("premiumPriceAckRequired", Boolean.class).build(); FormField.named("premiumPriceAckRequired", Boolean.class).build();
private static Function<Map<String, ?>, RegistrarAddress> newAddressTransform( private static @Nullable RegistrarAddress toNewAddress(
@Nullable Map<String, ?> args,
final FormField<List<String>, List<String>> streetField, final FormField<List<String>, List<String>> streetField,
final FormField<String, String> cityField, final FormField<String, String> cityField,
final FormField<String, String> stateField, final FormField<String, String> stateField,
final FormField<String, String> zipField) { final FormField<String, String> zipField) {
return args -> {
if (args == null) { if (args == null) {
return null; return null;
} }
@ -352,6 +315,58 @@ public final class RegistrarFormFields {
} }
zipField.extractUntyped(args).ifPresent(builder::setZip); zipField.extractUntyped(args).ifPresent(builder::setZip);
return builder.build(); return builder.build();
}; }
private static CidrAddressBlock parseCidr(String input) {
try {
return input != null ? CidrAddressBlock.create(input) : null;
} catch (IllegalArgumentException e) {
throw new FormFieldException("Not a valid CIDR notation IP-address block.", e);
}
}
private static @Nullable String parseHostname(@Nullable String input) {
if (input == null) {
return null;
}
if (!InternetDomainName.isValid(input)) {
throw new FormFieldException("Not a valid hostname.");
}
return canonicalizeDomainName(input);
}
public static @Nullable DateTime parseDateTime(@Nullable String input) {
if (input == null) {
return null;
}
try {
return DateTime.parse(input);
} catch (IllegalArgumentException e) {
throw new FormFieldException("Not a valid ISO date-time string.", e);
}
}
private static @Nullable RegistrarContact.Builder toRegistrarContactBuilder(
@Nullable Map<String, ?> args) {
if (args == null) {
return null;
}
RegistrarContact.Builder builder = new RegistrarContact.Builder();
CONTACT_NAME_FIELD.extractUntyped(args).ifPresent(builder::setName);
CONTACT_EMAIL_ADDRESS_FIELD.extractUntyped(args).ifPresent(builder::setEmailAddress);
CONTACT_VISIBLE_IN_WHOIS_AS_ADMIN_FIELD
.extractUntyped(args)
.ifPresent(builder::setVisibleInWhoisAsAdmin);
CONTACT_VISIBLE_IN_WHOIS_AS_TECH_FIELD
.extractUntyped(args)
.ifPresent(builder::setVisibleInWhoisAsTech);
PHONE_AND_EMAIL_VISIBLE_IN_DOMAIN_WHOIS_AS_ABUSE_FIELD
.extractUntyped(args)
.ifPresent(builder::setVisibleInDomainWhoisAsAbuse);
CONTACT_PHONE_NUMBER_FIELD.extractUntyped(args).ifPresent(builder::setPhoneNumber);
CONTACT_FAX_NUMBER_FIELD.extractUntyped(args).ifPresent(builder::setFaxNumber);
CONTACT_TYPES.extractUntyped(args).ifPresent(builder::setTypes);
CONTACT_GAE_USER_ID_FIELD.extractUntyped(args).ifPresent(builder::setGaeUserId);
return builder;
} }
} }

View file

@ -34,6 +34,7 @@ java_library(
"@com_google_re2j", "@com_google_re2j",
"@io_bazel_rules_closure//closure/templates", "@io_bazel_rules_closure//closure/templates",
"@javax_servlet_api", "@javax_servlet_api",
"@joda_time",
"@org_joda_money", "@org_joda_money",
], ],
) )

View file

@ -27,7 +27,6 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
import com.googlecode.objectify.Work;
import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.Config;
import google.registry.export.sheet.SyncRegistrarsSheetAction; import google.registry.export.sheet.SyncRegistrarsSheetAction;
import google.registry.model.registrar.Registrar; import google.registry.model.registrar.Registrar;
@ -55,6 +54,7 @@ import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import javax.inject.Inject; import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTime;
/** /**
* Admin servlet that allows creating or updating a registrar. Deletes are not allowed so as to * Admin servlet that allows creating or updating a registrar. Deletes are not allowed so as to
@ -112,7 +112,7 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
try { try {
switch (op) { switch (op) {
case "update": case "update":
return update(args, initialRegistrar); return update(args, initialRegistrar.getClientId());
case "read": case "read":
return JsonResponseHelper.create(SUCCESS, "Success", initialRegistrar.toJsonMap()); return JsonResponseHelper.create(SUCCESS, "Success", initialRegistrar.toJsonMap());
default: default:
@ -135,38 +135,63 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
args); args);
return JsonResponseHelper.create(ERROR, e.getMessage()); return JsonResponseHelper.create(ERROR, e.getMessage());
} }
} }
Map<String, Object> update(final Map<String, ?> args, final Registrar registrar) { Map<String, Object> update(final Map<String, ?> args, String clientId) {
final String clientId = sessionUtils.getRegistrarClientId(request);
return ofy() return ofy()
.transact( .transact(
(Work<Map<String, Object>>)
() -> { () -> {
ImmutableSet<RegistrarContact> oldContacts = registrar.getContacts(); // We load the registrar here rather than use the initialRegistrar above - to make
Map<String, Object> existingRegistrarMap = // sure we have the latest version. This one is loaded inside the transaction, so it's
expandRegistrarWithContacts(oldContacts, registrar); // guaranteed to not change before we update it.
Registrar registrar = Registrar.loadByClientId(clientId).get();
// Verify that the registrar hasn't been changed.
// To do that - we find the latest update time (or null if the registrar has been
// deleted) and compare to the update time from the args. The update time in the args
// comes from the read that gave the UI the data - if it's out of date, then the UI
// had out of date data.
DateTime latest = registrar.getLastUpdateTime();
DateTime latestFromArgs =
RegistrarFormFields.LAST_UPDATE_TIME.extractUntyped(args).get();
if (!latestFromArgs.equals(latest)) {
logger.warningfmt(
"registrar changed since reading the data! "
+ " Last updated at %s, but args data last updated at %s",
latest, latestFromArgs);
return JsonResponseHelper.create(
ERROR, "registrar has been changed by someone else. Please reload and retry.");
}
// Keep the current contacts so we can later check that no required contact was
// removed, email the changes to the contacts
ImmutableSet<RegistrarContact> contacts = registrar.getContacts();
// Update the registrar from the request.
Registrar.Builder builder = registrar.asBuilder(); Registrar.Builder builder = registrar.asBuilder();
ImmutableSet<RegistrarContact> updatedContacts =
changeRegistrarFields(registrar, builder, args); changeRegistrarFields(registrar, builder, args);
// read the contacts from the request.
ImmutableSet<RegistrarContact> updatedContacts =
readContacts(registrar, args);
if (!updatedContacts.isEmpty()) { if (!updatedContacts.isEmpty()) {
builder.setContactsRequireSyncing(true); builder.setContactsRequireSyncing(true);
} }
// Save the updated registrar
Registrar updatedRegistrar = builder.build(); Registrar updatedRegistrar = builder.build();
if (!updatedRegistrar.equals(registrar)) {
ofy().save().entity(updatedRegistrar); ofy().save().entity(updatedRegistrar);
}
// Save the updated contacts
if (!updatedContacts.isEmpty()) { if (!updatedContacts.isEmpty()) {
checkContactRequirements(oldContacts, updatedContacts); checkContactRequirements(contacts, updatedContacts);
RegistrarContact.updateContacts(updatedRegistrar, updatedContacts); RegistrarContact.updateContacts(updatedRegistrar, updatedContacts);
} }
// Update the registrar map with updated contacts to bypass Objectify caching
// issues that come into play with calling getContacts(). // Email and return update.
Map<String, Object> updatedRegistrarMap =
expandRegistrarWithContacts(updatedContacts, updatedRegistrar);
sendExternalUpdatesIfNecessary( sendExternalUpdatesIfNecessary(
updatedRegistrar.getRegistrarName(), registrar, contacts, updatedRegistrar, updatedContacts);
existingRegistrarMap,
updatedRegistrarMap);
return JsonResponseHelper.create( return JsonResponseHelper.create(
SUCCESS, "Saved " + clientId, updatedRegistrar.toJsonMap()); SUCCESS, "Saved " + clientId, updatedRegistrar.toJsonMap());
}); });
@ -186,12 +211,16 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
} }
/** /**
* Updates a registrar builder with the supplied args from the http request, and returns a list of * Updates a registrar builder with the supplied args from the http request;
* the new registrar contacts.
*/ */
public static ImmutableSet<RegistrarContact> changeRegistrarFields( public static void changeRegistrarFields(
Registrar existingRegistrarObj, Registrar.Builder builder, Map<String, ?> args) { Registrar existingRegistrarObj, Registrar.Builder builder, Map<String, ?> args) {
// BILLING
RegistrarFormFields.PREMIUM_PRICE_ACK_REQUIRED
.extractUntyped(args)
.ifPresent(builder::setPremiumPriceAckRequired);
// WHOIS // WHOIS
builder.setWhoisServer( builder.setWhoisServer(
RegistrarFormFields.WHOIS_SERVER_FIELD.extractUntyped(args).orElse(null)); RegistrarFormFields.WHOIS_SERVER_FIELD.extractUntyped(args).orElse(null));
@ -212,6 +241,8 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
builder.setLocalizedAddress( builder.setLocalizedAddress(
RegistrarFormFields.L10N_ADDRESS_FIELD.extractUntyped(args).orElse(null)); RegistrarFormFields.L10N_ADDRESS_FIELD.extractUntyped(args).orElse(null));
builder.setUrl(RegistrarFormFields.URL_FIELD.extractUntyped(args).orElse(null));
// Security // Security
builder.setIpAddressWhitelist( builder.setIpAddressWhitelist(
RegistrarFormFields.IP_ADDRESS_WHITELIST_FIELD RegistrarFormFields.IP_ADDRESS_WHITELIST_FIELD
@ -226,17 +257,16 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
.ifPresent( .ifPresent(
certificate -> certificate ->
builder.setFailoverClientCertificate(certificate, ofy().getTransactionTime())); builder.setFailoverClientCertificate(certificate, ofy().getTransactionTime()));
}
builder.setUrl( /** Reads the contacts from the supplied args. */
RegistrarFormFields.URL_FIELD.extractUntyped(args).orElse(null)); public static ImmutableSet<RegistrarContact> readContacts(
builder.setReferralUrl( Registrar registrar, Map<String, ?> args) {
RegistrarFormFields.REFERRAL_URL_FIELD.extractUntyped(args).orElse(null));
// Contact
ImmutableSet.Builder<RegistrarContact> contacts = new ImmutableSet.Builder<>(); ImmutableSet.Builder<RegistrarContact> contacts = new ImmutableSet.Builder<>();
Optional<List<Builder>> builders = RegistrarFormFields.CONTACTS_FIELD.extractUntyped(args); Optional<List<Builder>> builders = RegistrarFormFields.CONTACTS_FIELD.extractUntyped(args);
if (builders.isPresent()) { if (builders.isPresent()) {
builders.get().forEach(c -> contacts.add(c.setParent(existingRegistrarObj).build())); builders.get().forEach(c -> contacts.add(c.setParent(registrar).build()));
} }
return contacts.build(); return contacts.build();
@ -332,10 +362,19 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
* enqueues a task to re-sync the registrar sheet. * enqueues a task to re-sync the registrar sheet.
*/ */
private void sendExternalUpdatesIfNecessary( private void sendExternalUpdatesIfNecessary(
String registrarName, Registrar existingRegistrar,
Map<String, Object> existingRegistrar, ImmutableSet<RegistrarContact> existingContacts,
Map<String, Object> updatedRegistrar) { Registrar updatedRegistrar,
Map<?, ?> diffs = DiffUtils.deepDiff(existingRegistrar, updatedRegistrar, true); ImmutableSet<RegistrarContact> updatedContacts) {
if (registrarChangesNotificationEmailAddresses.isEmpty()) {
return;
}
Map<?, ?> diffs =
DiffUtils.deepDiff(
expandRegistrarWithContacts(existingContacts, existingRegistrar),
expandRegistrarWithContacts(updatedContacts, updatedRegistrar),
true);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Set<String> changedKeys = (Set<String>) diffs.keySet(); Set<String> changedKeys = (Set<String>) diffs.keySet();
if (CollectionUtils.difference(changedKeys, "lastUpdateTime").isEmpty()) { if (CollectionUtils.difference(changedKeys, "lastUpdateTime").isEmpty()) {
@ -345,7 +384,7 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
if (!registrarChangesNotificationEmailAddresses.isEmpty()) { if (!registrarChangesNotificationEmailAddresses.isEmpty()) {
sendEmailUtils.sendEmail( sendEmailUtils.sendEmail(
registrarChangesNotificationEmailAddresses, registrarChangesNotificationEmailAddresses,
String.format("Registrar %s updated", registrarName), String.format("Registrar %s updated", existingRegistrar.getRegistrarName()),
"The following changes were made to the registrar:\n" "The following changes were made to the registrar:\n"
+ DiffUtils.prettyPrintDiffedMap(diffs, null)); + DiffUtils.prettyPrintDiffedMap(diffs, null));
} }

View file

@ -113,7 +113,8 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase {
.setTypes(ImmutableSet.of(RegistrarContact.Type.ADMIN, RegistrarContact.Type.TECH)) .setTypes(ImmutableSet.of(RegistrarContact.Type.ADMIN, RegistrarContact.Type.TECH))
.build(); .build();
// Lest we anger the timestamp inversion bug. // Lest we anger the timestamp inversion bug.
persistResource(registrar); // (we also update the registrar so we get the timestamp right)
registrar = persistResource(registrar);
persistSimpleResource(rc); persistSimpleResource(rc);
// Now try to remove the phone number. // Now try to remove the phone number.
@ -138,7 +139,8 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase {
.setVisibleInDomainWhoisAsAbuse(true) .setVisibleInDomainWhoisAsAbuse(true)
.build(); .build();
// Lest we anger the timestamp inversion bug. // Lest we anger the timestamp inversion bug.
persistResource(registrar); // (we also update the registrar so we get the timestamp right)
registrar = persistResource(registrar);
persistSimpleResource(rc); persistSimpleResource(rc);
// Now try to remove the contact. // Now try to remove the contact.
@ -164,7 +166,8 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase {
.setVisibleInDomainWhoisAsAbuse(true) .setVisibleInDomainWhoisAsAbuse(true)
.build(); .build();
// Lest we anger the timestamp inversion bug. // Lest we anger the timestamp inversion bug.
persistResource(registrar); // (we also update the registrar so we get the timestamp right)
registrar = persistResource(registrar);
persistSimpleResource(rc); persistSimpleResource(rc);
// Now try to set the phone number to null. // Now try to set the phone number to null.

View file

@ -36,6 +36,8 @@ import google.registry.testing.TaskQueueHelper.TaskMatcher;
import java.util.Map; import java.util.Map;
import javax.mail.internet.InternetAddress; import javax.mail.internet.InternetAddress;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
@ -47,7 +49,7 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase
@Test @Test
public void testSuccess_updateRegistrarInfo_andSendsNotificationEmail() throws Exception { public void testSuccess_updateRegistrarInfo_andSendsNotificationEmail() throws Exception {
String expectedEmailBody = loadFile(getClass(), "update_registrar_email.txt"); String expectedEmailBody = loadFile(getClass(), "update_registrar_email.txt");
action.handleJsonRequest(readJsonFromFile("update_registrar.json")); action.handleJsonRequest(readJsonFromFile("update_registrar.json", getLastUpdateTime()));
verify(rsp, never()).setStatus(anyInt()); verify(rsp, never()).setStatus(anyInt());
verify(emailService).createMessage(); verify(emailService).createMessage();
verify(emailService).sendMessage(message); verify(emailService).sendMessage(message);
@ -64,7 +66,8 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase
@Test @Test
public void testFailure_updateRegistrarInfo_duplicateContacts() throws Exception { public void testFailure_updateRegistrarInfo_duplicateContacts() throws Exception {
Map<String, Object> response = Map<String, Object> response =
action.handleJsonRequest(readJsonFromFile("update_registrar_duplicate_contacts.json")); action.handleJsonRequest(
readJsonFromFile("update_registrar_duplicate_contacts.json", getLastUpdateTime()));
assertThat(response).containsEntry("status", "ERROR"); assertThat(response).containsEntry("status", "ERROR");
assertThat((String) response.get("message")).startsWith("One email address"); assertThat((String) response.get("message")).startsWith("One email address");
} }
@ -89,11 +92,22 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase
} }
@Test @Test
public void testUpdate_emptyJsonObject_errorEmailFieldRequired() throws Exception { public void testUpdate_emptyJsonObject_errorLastUpdateTimeFieldRequired() throws Exception {
Map<String, Object> response = action.handleJsonRequest(ImmutableMap.of( Map<String, Object> response = action.handleJsonRequest(ImmutableMap.of(
"op", "update", "op", "update",
"args", ImmutableMap.of())); "args", ImmutableMap.of()));
assertThat(response).containsEntry("status", "ERROR"); assertThat(response).containsEntry("status", "ERROR");
assertThat(response).containsEntry("field", "lastUpdateTime");
assertThat(response).containsEntry("message", "This field is required.");
assertNoTasksEnqueued("sheet");
}
@Test
public void testUpdate_noEmail_errorEmailFieldRequired() throws Exception {
Map<String, Object> response = action.handleJsonRequest(ImmutableMap.of(
"op", "update",
"args", ImmutableMap.of("lastUpdateTime", getLastUpdateTime())));
assertThat(response).containsEntry("status", "ERROR");
assertThat(response).containsEntry("field", "emailAddress"); assertThat(response).containsEntry("field", "emailAddress");
assertThat(response).containsEntry("message", "This field is required."); assertThat(response).containsEntry("message", "This field is required.");
assertNoTasksEnqueued("sheet"); assertNoTasksEnqueued("sheet");
@ -105,7 +119,7 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase
Map<String, Object> response = action.handleJsonRequest(ImmutableMap.of( Map<String, Object> response = action.handleJsonRequest(ImmutableMap.of(
"op", "update", "op", "update",
"args", ImmutableMap.of())); "args", ImmutableMap.of("lastUpdateTime", getLastUpdateTime())));
assertThat(response).containsEntry("status", "SUCCESS"); assertThat(response).containsEntry("status", "SUCCESS");
} }
@ -113,23 +127,53 @@ public class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase
public void testUpdate_badEmail_errorEmailField() throws Exception { public void testUpdate_badEmail_errorEmailField() throws Exception {
Map<String, Object> response = action.handleJsonRequest(ImmutableMap.of( Map<String, Object> response = action.handleJsonRequest(ImmutableMap.of(
"op", "update", "op", "update",
"args", ImmutableMap.of( "args", ImmutableMap.of("lastUpdateTime", getLastUpdateTime(), "emailAddress", "lolcat")));
"emailAddress", "lolcat")));
assertThat(response).containsEntry("status", "ERROR"); assertThat(response).containsEntry("status", "ERROR");
assertThat(response).containsEntry("field", "emailAddress"); assertThat(response).containsEntry("field", "emailAddress");
assertThat(response).containsEntry("message", "Please enter a valid email address."); assertThat(response).containsEntry("message", "Please enter a valid email address.");
assertNoTasksEnqueued("sheet"); assertNoTasksEnqueued("sheet");
} }
@Test
public void testPost_nonParsableTime_getsAngry() throws Exception {
Map<String, Object> response = action.handleJsonRequest(ImmutableMap.of(
"op", "update",
"args", ImmutableMap.of("lastUpdateTime", "cookies")));
assertThat(response).containsEntry("status", "ERROR");
assertThat(response).containsEntry("field", "lastUpdateTime");
assertThat(response).containsEntry("message", "Not a valid ISO date-time string.");
assertNoTasksEnqueued("sheet");
}
@Test @Test
public void testPost_nonAsciiCharacters_getsAngry() throws Exception { public void testPost_nonAsciiCharacters_getsAngry() throws Exception {
Map<String, Object> response = action.handleJsonRequest(ImmutableMap.of( Map<String, Object> response = action.handleJsonRequest(ImmutableMap.of(
"op", "update", "op", "update",
"args", ImmutableMap.of( "args", ImmutableMap.of(
"lastUpdateTime", getLastUpdateTime(),
"emailAddress", "ヘ(◕。◕ヘ)@example.com"))); "emailAddress", "ヘ(◕。◕ヘ)@example.com")));
assertThat(response).containsEntry("status", "ERROR"); assertThat(response).containsEntry("status", "ERROR");
assertThat(response).containsEntry("field", "emailAddress"); assertThat(response).containsEntry("field", "emailAddress");
assertThat(response).containsEntry("message", "Please only use ASCII-US characters."); assertThat(response).containsEntry("message", "Please only use ASCII-US characters.");
assertNoTasksEnqueued("sheet"); assertNoTasksEnqueued("sheet");
} }
private static String getLastUpdateTime() {
return loadRegistrar(CLIENT_ID).getLastUpdateTime().toString();
}
static Map<String, Object> readJsonFromFile(String filename, String lastUpdateTime) {
String contents =
loadFile(
RegistrarSettingsActionTestCase.class,
filename,
ImmutableMap.of("LAST_UPDATE_TIME", lastUpdateTime));
try {
@SuppressWarnings("unchecked")
Map<String, Object> json = (Map<String, Object>) JSONValue.parseWithException(contents);
return json;
} catch (ParseException ex) {
throw new RuntimeException(ex);
}
}
} }

View file

@ -19,7 +19,6 @@ import static google.registry.config.RegistryConfig.getGSuiteOutgoingEmailDispla
import static google.registry.security.JsonHttpTestUtils.createJsonPayload; import static google.registry.security.JsonHttpTestUtils.createJsonPayload;
import static google.registry.security.JsonHttpTestUtils.createJsonResponseSupplier; import static google.registry.security.JsonHttpTestUtils.createJsonResponseSupplier;
import static google.registry.testing.DatastoreHelper.loadRegistrar; import static google.registry.testing.DatastoreHelper.loadRegistrar;
import static google.registry.testing.TestDataHelper.loadFile;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -50,8 +49,6 @@ import javax.mail.internet.MimeMessage;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -114,15 +111,4 @@ public class RegistrarSettingsActionTestCase {
.thenAnswer(x -> loadRegistrar(CLIENT_ID)); .thenAnswer(x -> loadRegistrar(CLIENT_ID));
when(modulesService.getVersionHostname("backend", null)).thenReturn("backend.hostname"); when(modulesService.getVersionHostname("backend", null)).thenReturn("backend.hostname");
} }
static Map<String, Object> readJsonFromFile(String filename) {
String contents = loadFile(RegistrarSettingsActionTestCase.class, filename);
try {
@SuppressWarnings("unchecked")
Map<String, Object> json = (Map<String, Object>) JSONValue.parseWithException(contents);
return json;
} catch (ParseException ex) {
throw new RuntimeException(ex);
}
}
} }

View file

@ -4,7 +4,7 @@
"clientIdentifier": "theregistrar", "clientIdentifier": "theregistrar",
"driveFolderId": null, "driveFolderId": null,
"registrarName": "The Registrar", "registrarName": "The Registrar",
"lastUpdateTime": "2015-01-22T17:27:36.999Z", "lastUpdateTime": "%LAST_UPDATE_TIME%",
"state": "ACTIVE", "state": "ACTIVE",
"type": "REAL", "type": "REAL",
"contacts": [ "contacts": [

View file

@ -4,7 +4,7 @@
"clientIdentifier": "theregistrar", "clientIdentifier": "theregistrar",
"driveFolderId": null, "driveFolderId": null,
"registrarName": "The Registrar", "registrarName": "The Registrar",
"lastUpdateTime": "2015-01-22T17:27:36.999Z", "lastUpdateTime": "%LAST_UPDATE_TIME%",
"state": "ACTIVE", "state": "ACTIVE",
"type": "REAL", "type": "REAL",
"contacts": [ "contacts": [