Replace command.applyTo() with inlined builder operations

These are much easier to understand.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=137446273
This commit is contained in:
cgoldfeder 2016-10-27 15:03:48 -07:00 committed by Ben McIlwain
parent b84d7f1fb5
commit 1dbc5f6bb0
28 changed files with 489 additions and 426 deletions

View file

@ -978,6 +978,7 @@ are enqueued to update DNS accordingly.
* Resource status prohibits this operation.
* Cannot remove all IP addresses from a subordinate host.
* 2306
* Cannot add and remove the same value.
* Host names must be at least two levels below the public suffix.
## HostInfoFlow

View file

@ -15,6 +15,7 @@
package google.registry.flows;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Sets.intersection;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.queryDomainsUsingResource;
import static google.registry.model.domain.DomainResource.extendRegistrationWithCap;
@ -33,6 +34,8 @@ import com.googlecode.objectify.Work;
import google.registry.flows.EppException.AuthorizationErrorException;
import google.registry.flows.EppException.InvalidAuthorizationInformationErrorException;
import google.registry.flows.EppException.ObjectDoesNotExistException;
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
import google.registry.flows.EppException.ParameterValueRangeErrorException;
import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.flows.exceptions.NotTransferInitiatorException;
@ -359,6 +362,25 @@ public final class ResourceFlowUtils {
}
}
/** Check that the same values aren't being added and removed in an update command. */
public static void checkSameValuesNotAddedAndRemoved(
ImmutableSet<?> fieldsToAdd, ImmutableSet<?> fieldsToRemove)
throws AddRemoveSameValueException {
if (!intersection(fieldsToAdd, fieldsToRemove).isEmpty()) {
throw new AddRemoveSameValueException();
}
}
/** Check that all {@link StatusValue} objects in a set are client-settable. */
public static void verifyAllStatusesAreClientSettable(Set<StatusValue> statusValues)
throws StatusNotClientSettableException {
for (StatusValue statusValue : statusValues) {
if (!statusValue.isClientSettable()) {
throw new StatusNotClientSettableException(statusValue.getXmlName());
}
}
}
/** Resource with this id does not exist. */
public static class ResourceDoesNotExistException extends ObjectDoesNotExistException {
public ResourceDoesNotExistException(Class<?> type, String targetId) {
@ -380,4 +402,18 @@ public final class ResourceFlowUtils {
super("Authorization information for accessing resource is invalid");
}
}
/** Cannot add and remove the same value. */
public static class AddRemoveSameValueException extends ParameterValuePolicyErrorException {
public AddRemoveSameValueException() {
super("Cannot add and remove the same value");
}
}
/** The specified status value cannot be set by clients. */
public static class StatusNotClientSettableException extends ParameterValueRangeErrorException {
public StatusNotClientSettableException(String statusValue) {
super(String.format("Status value %s cannot be set by clients", statusValue));
}
}
}

View file

@ -65,12 +65,18 @@ public final class ContactCreateFlow extends Flow implements TransactionalFlow {
validateClientIsLoggedIn(clientId);
Create command = (Create) resourceCommand;
verifyResourceDoesNotExist(ContactResource.class, targetId, now);
Builder builder = new Builder();
command.applyTo(builder);
ContactResource newContact = builder
ContactResource newContact = new Builder()
.setContactId(targetId)
.setAuthInfo(command.getAuthInfo())
.setCreationClientId(clientId)
.setCurrentSponsorClientId(clientId)
.setRepoId(createContactHostRoid(ObjectifyService.allocateId()))
.setFaxNumber(command.getFax())
.setVoiceNumber(command.getVoice())
.setDisclose(command.getDisclose())
.setEmailAddress(command.getEmail())
.setInternationalizedPostalInfo(command.getInternationalizedPostalInfo())
.setLocalizedPostalInfo(command.getLocalizedPostalInfo())
.build();
validateAsciiPostalInfo(newContact.getInternationalizedPostalInfo());
validateContactAgainstPolicy(newContact);

View file

@ -14,8 +14,11 @@
package google.registry.flows.contact;
import static com.google.common.collect.Sets.union;
import static google.registry.flows.FlowUtils.validateClientIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.checkSameValuesNotAddedAndRemoved;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyAllStatusesAreClientSettable;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
@ -26,7 +29,6 @@ import static google.registry.model.ofy.ObjectifyService.ofy;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.googlecode.objectify.Key;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
@ -34,30 +36,30 @@ import google.registry.flows.Flow;
import google.registry.flows.FlowModule.ClientId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.exceptions.AddRemoveSameValueEppException;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.flows.exceptions.StatusNotClientSettableException;
import google.registry.model.contact.ContactCommand.Update;
import google.registry.model.contact.ContactCommand.Update.Change;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.ContactResource.Builder;
import google.registry.model.contact.PostalInfo;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppinput.ResourceCommand.AddRemoveSameValueException;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.reporting.HistoryEntry;
import javax.annotation.Nullable;
import javax.inject.Inject;
/**
* An EPP flow that updates a contact.
*
* @error {@link google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.exceptions.AddRemoveSameValueEppException}
* @error {@link google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException}
* @error {@link google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link google.registry.flows.exceptions.StatusNotClientSettableException}
* @error {@link ContactFlowUtils.BadInternationalizedPostalInfoException}
* @error {@link ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException}
*/
@ -88,15 +90,11 @@ public final class ContactUpdateFlow extends Flow implements TransactionalFlow {
Update command = (Update) resourceCommand;
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
verifyOptionalAuthInfoForResource(authInfo, existingContact);
if (!isSuperuser) {
ImmutableSet<StatusValue> statusToRemove = command.getInnerRemove().getStatusValues();
ImmutableSet<StatusValue> statusesToAdd = command.getInnerAdd().getStatusValues();
if (!isSuperuser) { // The superuser can update any contact and set any status.
verifyResourceOwnership(clientId, existingContact);
}
for (StatusValue statusValue : Sets.union(
command.getInnerAdd().getStatusValues(),
command.getInnerRemove().getStatusValues())) {
if (!isSuperuser && !statusValue.isClientSettable()) { // The superuser can set any status.
throw new StatusNotClientSettableException(statusValue.getXmlName());
}
verifyAllStatusesAreClientSettable(union(statusesToAdd, statusToRemove));
}
verifyNoDisallowedStatuses(existingContact, DISALLOWED_STATUSES);
historyBuilder
@ -104,15 +102,39 @@ public final class ContactUpdateFlow extends Flow implements TransactionalFlow {
.setModificationTime(now)
.setXmlBytes(null) // We don't want to store contact details in the history entry.
.setParent(Key.create(existingContact));
checkSameValuesNotAddedAndRemoved(statusesToAdd, statusToRemove);
Builder builder = existingContact.asBuilder();
try {
command.applyTo(builder);
} catch (AddRemoveSameValueException e) {
throw new AddRemoveSameValueEppException();
Change change = command.getInnerChange();
// The spec requires the following behaviors:
// * If you update part of a postal info, the fields that you didn't update are unchanged.
// * If you update one postal info but not the other, the other is deleted.
// Therefore, if you want to preserve one postal info and update another you need to send the
// update and also something that technically updates the preserved one, even if it only
// "updates" it by setting just one field to the same value.
PostalInfo internationalized = change.getInternationalizedPostalInfo();
PostalInfo localized = change.getLocalizedPostalInfo();
if (internationalized != null) {
builder.overlayInternationalizedPostalInfo(internationalized);
if (localized == null) {
builder.setLocalizedPostalInfo(null);
}
}
if (localized != null) {
builder.overlayLocalizedPostalInfo(localized);
if (internationalized == null) {
builder.setInternationalizedPostalInfo(null);
}
}
ContactResource newContact = builder
.setLastEppUpdateTime(now)
.setLastEppUpdateClientId(clientId)
.setAuthInfo(preferFirst(change.getAuthInfo(), existingContact.getAuthInfo()))
.setDisclose(preferFirst(change.getDisclose(), existingContact.getDisclose()))
.setEmailAddress(preferFirst(change.getEmail(), existingContact.getEmailAddress()))
.setFaxNumber(preferFirst(change.getFax(), existingContact.getFaxNumber()))
.setVoiceNumber(preferFirst(change.getVoice(), existingContact.getVoiceNumber()))
.addStatusValues(statusesToAdd)
.removeStatusValues(statusToRemove)
.build();
// If the resource is marked with clientUpdateProhibited, and this update did not clear that
// status, then the update must be disallowed (unless a superuser is requesting the change).
@ -126,4 +148,10 @@ public final class ContactUpdateFlow extends Flow implements TransactionalFlow {
ofy().save().<Object>entities(newContact, historyBuilder.build());
return createOutput(SUCCESS);
}
/** Return the first non-null param, or null if both are null. */
@Nullable
private static <T> T preferFirst(@Nullable T a, @Nullable T b) {
return a != null ? a : b;
}
}

View file

@ -146,9 +146,7 @@ public class DomainAllocateFlow extends Flow implements TransactionalFlow {
domainName, application, historyEntry, isSunrushAddGracePeriod, registry, years);
entitiesToSave.addAll(billsAndPolls);
DateTime registrationExpirationTime = leapSafeAddYears(now, years);
DomainResource.Builder domainBuilder = new DomainResource.Builder();
command.applyTo(domainBuilder);
DomainResource newDomain = domainBuilder
DomainResource newDomain = new DomainResource.Builder()
.setCreationClientId(clientId)
.setCurrentSponsorClientId(clientId)
.setRepoId(repoId)
@ -167,6 +165,11 @@ public class DomainAllocateFlow extends Flow implements TransactionalFlow {
.setStatusValues(ReservationType.NAME_COLLISION == getReservationType(domainName)
? ImmutableSet.of(StatusValue.SERVER_HOLD)
: ImmutableSet.<StatusValue>of())
.setRegistrant(command.getRegistrant())
.setAuthInfo(command.getAuthInfo())
.setFullyQualifiedDomainName(targetId)
.setNameservers(command.getNameservers())
.setContacts(command.getContacts())
.build();
entitiesToSave.add(
newDomain,

View file

@ -210,9 +210,7 @@ public final class DomainApplicationCreateFlow extends Flow implements Transacti
validateFeeChallenge(targetId, tld, now, feeCreate, commandOperations.getTotalCost());
SecDnsCreateExtension secDnsCreate =
validateSecDnsExtension(eppInput.getSingleExtension(SecDnsCreateExtension.class));
DomainApplication.Builder applicationBuilder = new DomainApplication.Builder();
command.applyTo(applicationBuilder);
applicationBuilder
DomainApplication newApplication = new DomainApplication.Builder()
.setCreationTrid(trid)
.setCreationClientId(clientId)
.setCurrentSponsorClientId(clientId)
@ -224,6 +222,11 @@ public final class DomainApplicationCreateFlow extends Flow implements Transacti
.setApplicationStatus(ApplicationStatus.VALIDATED)
.addStatusValue(StatusValue.PENDING_CREATE)
.setDsData(secDnsCreate == null ? null : secDnsCreate.getDsData())
.setRegistrant(command.getRegistrant())
.setAuthInfo(command.getAuthInfo())
.setFullyQualifiedDomainName(targetId)
.setNameservers(command.getNameservers())
.setContacts(command.getContacts())
.setEncodedSignedMarks(FluentIterable
.from(launchCreate.getSignedMarks())
.transform(new Function<AbstractSignedMark, EncodedSignedMark>() {
@ -231,8 +234,8 @@ public final class DomainApplicationCreateFlow extends Flow implements Transacti
public EncodedSignedMark apply(AbstractSignedMark abstractSignedMark) {
return (EncodedSignedMark) abstractSignedMark;
}})
.toList());
DomainApplication newApplication = applicationBuilder.build();
.toList())
.build();
HistoryEntry historyEntry = buildHistory(newApplication.getRepoId(), command.getPeriod());
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
handleExtraFlowLogic(

View file

@ -16,7 +16,11 @@ package google.registry.flows.domain;
import static com.google.common.base.CaseFormat.LOWER_CAMEL;
import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.Sets.union;
import static google.registry.flows.FlowUtils.validateClientIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.checkSameValuesNotAddedAndRemoved;
import static google.registry.flows.ResourceFlowUtils.verifyAllStatusesAreClientSettable;
import static google.registry.flows.ResourceFlowUtils.verifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource;
@ -34,7 +38,6 @@ import static google.registry.flows.domain.DomainFlowUtils.validateRequiredConta
import static google.registry.flows.domain.DomainFlowUtils.verifyApplicationDomainMatchesTargetId;
import static google.registry.flows.domain.DomainFlowUtils.verifyClientUpdateNotProhibited;
import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPendingDelete;
import static google.registry.flows.domain.DomainFlowUtils.verifyStatusChangesAreClientSettable;
import static google.registry.model.EppResourceUtils.loadDomainApplication;
import static google.registry.model.domain.fee.Fee.FEE_UPDATE_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
@ -52,11 +55,11 @@ import google.registry.flows.FlowModule.ApplicationId;
import google.registry.flows.FlowModule.ClientId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.exceptions.AddRemoveSameValueEppException;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainApplication;
import google.registry.model.domain.DomainApplication.Builder;
import google.registry.model.domain.DomainCommand.Update;
import google.registry.model.domain.DomainCommand.Update.AddRemove;
import google.registry.model.domain.DomainCommand.Update.Change;
import google.registry.model.domain.launch.ApplicationStatus;
import google.registry.model.domain.launch.LaunchUpdateExtension;
import google.registry.model.domain.metadata.MetadataExtension;
@ -64,7 +67,6 @@ import google.registry.model.domain.secdns.SecDnsUpdateExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppinput.ResourceCommand.AddRemoveSameValueException;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.reporting.HistoryEntry;
import javax.inject.Inject;
@ -76,12 +78,12 @@ import javax.inject.Inject;
* cannot change the domain name that is being applied for.
*
* @error {@link google.registry.flows.EppException.UnimplementedExtensionException}
* @error {@link google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.exceptions.AddRemoveSameValueEppException}
* @error {@link google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException}
* @error {@link google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link google.registry.flows.exceptions.StatusNotClientSettableException}
* @error {@link DomainFlowUtils.ApplicationDomainNameMismatchException}
* @error {@link DomainFlowUtils.DuplicateContactForRoleException}
* @error {@link DomainFlowUtils.EmptySecDnsUpdateException}
@ -151,10 +153,12 @@ public class DomainApplicationUpdateFlow extends Flow implements TransactionalFl
protected final void verifyUpdateAllowed(
DomainApplication existingApplication, Update command) throws EppException {
AddRemove add = command.getInnerAdd();
AddRemove remove = command.getInnerRemove();
if (!isSuperuser) {
verifyResourceOwnership(clientId, existingApplication);
verifyClientUpdateNotProhibited(command, existingApplication);
verifyStatusChangesAreClientSettable(command);
verifyAllStatusesAreClientSettable(union(add.getStatusValues(), remove.getStatusValues()));
}
String tld = existingApplication.getTld();
checkAllowedAccessToTld(clientId, tld);
@ -164,14 +168,14 @@ public class DomainApplicationUpdateFlow extends Flow implements TransactionalFl
existingApplication.getApplicationStatus());
}
verifyNotInPendingDelete(
command.getInnerAdd().getContacts(),
add.getContacts(),
command.getInnerChange().getRegistrant(),
command.getInnerAdd().getNameservers());
validateContactsHaveTypes(command.getInnerAdd().getContacts());
validateContactsHaveTypes(command.getInnerRemove().getContacts());
add.getNameservers());
validateContactsHaveTypes(add.getContacts());
validateContactsHaveTypes(remove.getContacts());
validateRegistrantAllowedOnTld(tld, command.getInnerChange().getRegistrantContactId());
validateNameserversAllowedOnTld(
tld, command.getInnerAdd().getNameserverFullyQualifiedHostNames());
tld, add.getNameserverFullyQualifiedHostNames());
}
private HistoryEntry buildHistory(DomainApplication existingApplication) {
@ -183,20 +187,30 @@ public class DomainApplicationUpdateFlow extends Flow implements TransactionalFl
}
private DomainApplication updateApplication(
DomainApplication existingApplication, Update command) throws EppException {
Builder builder = existingApplication.asBuilder();
try {
command.applyTo(builder);
} catch (AddRemoveSameValueException e) {
throw new AddRemoveSameValueEppException();
}
builder.setLastEppUpdateTime(now).setLastEppUpdateClientId(clientId);
// Handle the secDNS extension.
DomainApplication application, Update command) throws EppException {
AddRemove add = command.getInnerAdd();
AddRemove remove = command.getInnerRemove();
checkSameValuesNotAddedAndRemoved(add.getNameservers(), remove.getNameservers());
checkSameValuesNotAddedAndRemoved(add.getContacts(), remove.getContacts());
checkSameValuesNotAddedAndRemoved(add.getStatusValues(), remove.getStatusValues());
Change change = command.getInnerChange();
SecDnsUpdateExtension secDnsUpdate = eppInput.getSingleExtension(SecDnsUpdateExtension.class);
if (secDnsUpdate != null) {
builder.setDsData(updateDsData(existingApplication.getDsData(), secDnsUpdate));
}
return builder.build();
return application.asBuilder()
// Handle the secDNS extension.
.setDsData(secDnsUpdate != null
? updateDsData(application.getDsData(), secDnsUpdate)
: application.getDsData())
.setLastEppUpdateTime(now)
.setLastEppUpdateClientId(clientId)
.addStatusValues(add.getStatusValues())
.removeStatusValues(remove.getStatusValues())
.addNameservers(add.getNameservers())
.removeNameservers(remove.getNameservers())
.addContacts(add.getContacts())
.removeContacts(remove.getContacts())
.setRegistrant(firstNonNull(change.getRegistrant(), application.getRegistrant()))
.setAuthInfo(firstNonNull(change.getAuthInfo(), application.getAuthInfo()))
.build();
}
private void validateNewApplication(DomainApplication newApplication) throws EppException {

View file

@ -245,9 +245,7 @@ public class DomainCreateFlow extends Flow implements TransactionalFlow {
if (!commandOperations.getEapCost().isZero()) {
entitiesToSave.add(createEapBillingEvent(commandOperations, createBillingEvent));
}
DomainResource.Builder domainBuilder = new DomainResource.Builder();
command.applyTo(domainBuilder);
DomainResource newDomain = domainBuilder
DomainResource newDomain = new DomainResource.Builder()
.setCreationClientId(clientId)
.setCurrentSponsorClientId(clientId)
.setRepoId(repoId)
@ -261,6 +259,11 @@ public class DomainCreateFlow extends Flow implements TransactionalFlow {
? verifySignedMarks(launchCreate.getSignedMarks(), domainLabel, now).getId()
: null)
.setDsData(secDnsCreate == null ? null : secDnsCreate.getDsData())
.setRegistrant(command.getRegistrant())
.setAuthInfo(command.getAuthInfo())
.setFullyQualifiedDomainName(targetId)
.setNameservers(command.getNameservers())
.setContacts(command.getContacts())
.addGracePeriod(GracePeriod.forBillingEvent(GracePeriodStatus.ADD, createBillingEvent))
.build();
handleExtraFlowLogic(registry.getTldStr(), years, historyEntry, newDomain);

View file

@ -59,7 +59,6 @@ import google.registry.flows.EppException.UnimplementedOptionException;
import google.registry.flows.domain.TldSpecificLogicProxy.EppCommandOperations;
import google.registry.flows.exceptions.ResourceAlreadyExistsException;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.flows.exceptions.StatusNotClientSettableException;
import google.registry.model.EppResource;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
@ -829,18 +828,6 @@ public class DomainFlowUtils {
return ImmutableSet.copyOf(union(difference(oldDsData, toRemove), toAdd));
}
/** Check that all of the status values added or removed in an update are client-settable. */
static void verifyStatusChangesAreClientSettable(Update command)
throws StatusNotClientSettableException {
for (StatusValue statusValue : union(
command.getInnerAdd().getStatusValues(),
command.getInnerRemove().getStatusValues())) {
if (!statusValue.isClientSettable()) {
throw new StatusNotClientSettableException(statusValue.getXmlName());
}
}
}
/** If a domain or application has "clientUpdateProhibited" set, updates must clear it or fail. */
static void verifyClientUpdateNotProhibited(Update command, DomainBase existingResource)
throws ResourceHasClientUpdateProhibitedException {

View file

@ -14,9 +14,13 @@
package google.registry.flows.domain;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.Sets.symmetricDifference;
import static com.google.common.collect.Sets.union;
import static google.registry.flows.FlowUtils.validateClientIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.checkSameValuesNotAddedAndRemoved;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyAllStatusesAreClientSettable;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
@ -33,7 +37,6 @@ import static google.registry.flows.domain.DomainFlowUtils.validateRegistrantAll
import static google.registry.flows.domain.DomainFlowUtils.validateRequiredContactsPresent;
import static google.registry.flows.domain.DomainFlowUtils.verifyClientUpdateNotProhibited;
import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPendingDelete;
import static google.registry.flows.domain.DomainFlowUtils.verifyStatusChangesAreClientSettable;
import static google.registry.model.domain.fee.Fee.FEE_UPDATE_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.ofy.ObjectifyService.ofy;
@ -52,11 +55,12 @@ import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.domain.DomainFlowUtils.FeesRequiredForNonFreeUpdateException;
import google.registry.flows.domain.TldSpecificLogicProxy.EppCommandOperations;
import google.registry.flows.exceptions.AddRemoveSameValueEppException;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DomainCommand.Update;
import google.registry.model.domain.DomainCommand.Update.AddRemove;
import google.registry.model.domain.DomainCommand.Update.Change;
import google.registry.model.domain.DomainResource;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.fee.FeeTransformCommandExtension;
@ -67,7 +71,6 @@ import google.registry.model.domain.secdns.SecDnsUpdateExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppinput.ResourceCommand.AddRemoveSameValueException;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.registry.Registry;
import google.registry.model.reporting.HistoryEntry;
@ -92,13 +95,13 @@ import org.joda.time.DateTime;
* accordingly.
*
* @error {@link google.registry.flows.EppException.UnimplementedExtensionException}
* @error {@link google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.exceptions.AddRemoveSameValueEppException}
* @error {@link google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException}
* @error {@link google.registry.flows.exceptions.OnlyToolCanPassMetadataException}
* @error {@link google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link google.registry.flows.exceptions.StatusNotClientSettableException}
* @error {@link DomainFlowUtils.DuplicateContactForRoleException}
* @error {@link DomainFlowUtils.EmptySecDnsUpdateException}
* @error {@link DomainFlowUtils.FeesMismatchException}
@ -181,10 +184,12 @@ public final class DomainUpdateFlow extends Flow implements TransactionalFlow {
throws EppException {
verifyNoDisallowedStatuses(existingDomain, UPDATE_DISALLOWED_STATUSES);
verifyOptionalAuthInfoForResource(authInfo, existingDomain);
AddRemove add = command.getInnerAdd();
AddRemove remove = command.getInnerRemove();
if (!isSuperuser) {
verifyResourceOwnership(clientId, existingDomain);
verifyClientUpdateNotProhibited(command, existingDomain);
verifyStatusChangesAreClientSettable(command);
verifyAllStatusesAreClientSettable(union(add.getStatusValues(), remove.getStatusValues()));
}
String tld = existingDomain.getTld();
checkAllowedAccessToTld(clientId, tld);
@ -204,14 +209,14 @@ public final class DomainUpdateFlow extends Flow implements TransactionalFlow {
throw new FeesRequiredForNonFreeUpdateException();
}
verifyNotInPendingDelete(
command.getInnerAdd().getContacts(),
add.getContacts(),
command.getInnerChange().getRegistrant(),
command.getInnerAdd().getNameservers());
validateContactsHaveTypes(command.getInnerAdd().getContacts());
validateContactsHaveTypes(command.getInnerRemove().getContacts());
add.getNameservers());
validateContactsHaveTypes(add.getContacts());
validateContactsHaveTypes(remove.getContacts());
validateRegistrantAllowedOnTld(tld, command.getInnerChange().getRegistrantContactId());
validateNameserversAllowedOnTld(
tld, command.getInnerAdd().getNameserverFullyQualifiedHostNames());
tld, add.getNameserverFullyQualifiedHostNames());
}
private HistoryEntry buildHistoryEntry(DomainResource existingDomain) {
@ -222,22 +227,31 @@ public final class DomainUpdateFlow extends Flow implements TransactionalFlow {
.build();
}
private DomainResource performUpdate(Update command, DomainResource existingDomain)
private DomainResource performUpdate(Update command, DomainResource domain)
throws EppException {
DomainResource.Builder builder = existingDomain.asBuilder()
.setLastEppUpdateTime(now)
.setLastEppUpdateClientId(clientId);
try {
command.applyTo(builder);
} catch (AddRemoveSameValueException e) {
throw new AddRemoveSameValueEppException();
}
// Handle the secDNS extension.
AddRemove add = command.getInnerAdd();
AddRemove remove = command.getInnerRemove();
checkSameValuesNotAddedAndRemoved(add.getNameservers(), remove.getNameservers());
checkSameValuesNotAddedAndRemoved(add.getContacts(), remove.getContacts());
checkSameValuesNotAddedAndRemoved(add.getStatusValues(), remove.getStatusValues());
Change change = command.getInnerChange();
SecDnsUpdateExtension secDnsUpdate = eppInput.getSingleExtension(SecDnsUpdateExtension.class);
if (secDnsUpdate != null) {
builder.setDsData(updateDsData(existingDomain.getDsData(), secDnsUpdate));
}
return builder.build();
return domain.asBuilder()
// Handle the secDNS extension.
.setDsData(secDnsUpdate != null
? updateDsData(domain.getDsData(), secDnsUpdate)
: domain.getDsData())
.setLastEppUpdateTime(now)
.setLastEppUpdateClientId(clientId)
.addStatusValues(add.getStatusValues())
.removeStatusValues(remove.getStatusValues())
.addNameservers(add.getNameservers())
.removeNameservers(remove.getNameservers())
.addContacts(add.getContacts())
.removeContacts(remove.getContacts())
.setRegistrant(firstNonNull(change.getRegistrant(), domain.getRegistrant()))
.setAuthInfo(firstNonNull(change.getAuthInfo(), domain.getAuthInfo()))
.build();
}
private DomainResource convertSunrushAddToAdd(

View file

@ -1,24 +0,0 @@
// Copyright 2016 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows.exceptions;
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
/** Cannot add and remove the same value. */
public class AddRemoveSameValueEppException extends ParameterValuePolicyErrorException {
public AddRemoveSameValueEppException() {
super("Cannot add and remove the same value");
}
}

View file

@ -1,24 +0,0 @@
// Copyright 2016 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows.exceptions;
import google.registry.flows.EppException.ParameterValueRangeErrorException;
/** The specified status value cannot be set by clients. */
public class StatusNotClientSettableException extends ParameterValueRangeErrorException {
public StatusNotClientSettableException(String statusValue) {
super(String.format("Status value %s cannot be set by clients", statusValue));
}
}

View file

@ -101,11 +101,11 @@ public final class HostCreateFlow extends Flow implements TransactionalFlow {
? new SubordinateHostMustHaveIpException()
: new UnexpectedExternalHostIpException();
}
Builder builder = new Builder();
command.applyTo(builder);
HostResource newHost = builder
HostResource newHost = new Builder()
.setCreationClientId(clientId)
.setCurrentSponsorClientId(clientId)
.setFullyQualifiedHostName(targetId)
.setInetAddresses(command.getInetAddresses())
.setRepoId(createContactHostRoid(ObjectifyService.allocateId()))
.setSuperordinateDomain(
superordinateDomain.isPresent() ? Key.create(superordinateDomain.get()) : null)

View file

@ -15,8 +15,11 @@
package google.registry.flows.host;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.Sets.union;
import static google.registry.flows.FlowUtils.validateClientIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.checkSameValuesNotAddedAndRemoved;
import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyAllStatusesAreClientSettable;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
@ -30,7 +33,6 @@ import static google.registry.util.CollectionUtils.isNullOrEmpty;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.googlecode.objectify.Key;
import google.registry.dns.DnsQueue;
import google.registry.flows.EppException;
@ -44,20 +46,18 @@ import google.registry.flows.FlowModule.ClientId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.async.AsyncFlowEnqueuer;
import google.registry.flows.exceptions.AddRemoveSameValueEppException;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.flows.exceptions.StatusNotClientSettableException;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainResource;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppinput.ResourceCommand.AddRemoveSameValueException;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.host.HostCommand.Update;
import google.registry.model.host.HostCommand.Update.AddRemove;
import google.registry.model.host.HostCommand.Update.Change;
import google.registry.model.host.HostResource;
import google.registry.model.host.HostResource.Builder;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.reporting.HistoryEntry;
import java.util.Objects;
@ -76,11 +76,12 @@ import javax.inject.Inject;
* when it is renamed from external to internal at least one must be added. If the host is renamed
* or IP addresses are added, tasks are enqueued to update DNS accordingly.
*
* @error {@link google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException}
* @error {@link google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link google.registry.flows.exceptions.StatusNotClientSettableException}
* @error {@link HostFlowUtils.HostNameTooShallowException}
* @error {@link HostFlowUtils.InvalidHostNameException}
* @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException}
@ -117,7 +118,8 @@ public final class HostUpdateFlow extends Flow implements TransactionalFlow {
extensionManager.validate();
validateClientIsLoggedIn(clientId);
Update command = (Update) resourceCommand;
String suppliedNewHostName = command.getInnerChange().getFullyQualifiedHostName();
Change change = command.getInnerChange();
String suppliedNewHostName = change.getFullyQualifiedHostName();
HostResource existingHost = loadAndVerifyExistence(HostResource.class, targetId, now);
boolean isHostRename = suppliedNewHostName != null;
String oldHostName = targetId;
@ -128,13 +130,16 @@ public final class HostUpdateFlow extends Flow implements TransactionalFlow {
if (isHostRename && loadAndGetKey(HostResource.class, newHostName, now) != null) {
throw new HostAlreadyExistsException(newHostName);
}
Builder builder = existingHost.asBuilder();
try {
command.applyTo(builder);
} catch (AddRemoveSameValueException e) {
throw new AddRemoveSameValueEppException();
}
builder
AddRemove add = command.getInnerAdd();
AddRemove remove = command.getInnerRemove();
checkSameValuesNotAddedAndRemoved(add.getStatusValues(), remove.getStatusValues());
checkSameValuesNotAddedAndRemoved(add.getInetAddresses(), remove.getInetAddresses());
HostResource newHost = existingHost.asBuilder()
.setFullyQualifiedHostName(newHostName)
.addStatusValues(add.getStatusValues())
.removeStatusValues(remove.getStatusValues())
.addInetAddresses(add.getInetAddresses())
.removeInetAddresses(remove.getInetAddresses())
.setLastEppUpdateTime(now)
.setLastEppUpdateClientId(clientId)
// The superordinateDomain can be missing if the new name is external.
@ -142,9 +147,10 @@ public final class HostUpdateFlow extends Flow implements TransactionalFlow {
// the lookupSuperordinateDomain(...) call above, so that it will never be stale.
.setSuperordinateDomain(
superordinateDomain.isPresent() ? Key.create(superordinateDomain.get()) : null)
.setLastSuperordinateChange(superordinateDomain == null ? null : now);
// Rely on the host's cloneProjectedAtTime() method to handle setting of transfer data.
HostResource newHost = builder.build().cloneProjectedAtTime(now);
.setLastSuperordinateChange(superordinateDomain == null ? null : now)
.build()
// Rely on the host's cloneProjectedAtTime() method to handle setting of transfer data.
.cloneProjectedAtTime(now);
verifyHasIpsIffIsExternal(command, existingHost, newHost);
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
entitiesToSave.add(newHost);
@ -172,21 +178,16 @@ public final class HostUpdateFlow extends Flow implements TransactionalFlow {
verifyOptionalAuthInfoForResource(authInfo, existingResource);
if (!isSuperuser) {
verifyResourceOwnership(clientId, existingResource);
ImmutableSet<StatusValue> statusesToAdd = command.getInnerAdd().getStatusValues();
ImmutableSet<StatusValue> statusesToRemove = command.getInnerRemove().getStatusValues();
// If the resource is marked with clientUpdateProhibited, and this update does not clear that
// status, then the update must be disallowed (unless a superuser is requesting the change).
if (!isSuperuser
&& existingResource.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)
&& !command.getInnerRemove().getStatusValues()
.contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) {
&& !statusesToRemove.contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) {
throw new ResourceHasClientUpdateProhibitedException();
}
}
for (StatusValue statusValue : Sets.union(
command.getInnerAdd().getStatusValues(),
command.getInnerRemove().getStatusValues())) {
if (!isSuperuser && !statusValue.isClientSettable()) { // The superuser can set any status.
throw new StatusNotClientSettableException(statusValue.getXmlName());
}
verifyAllStatusesAreClientSettable(union(statusesToAdd, statusesToRemove));
}
verifyDomainIsSameRegistrar(superordinateDomain, clientId);
verifyNoDisallowedStatuses(existingResource, DISALLOWED_STATUSES);

View file

@ -20,7 +20,6 @@ import static google.registry.util.CollectionUtils.nullToEmpty;
import com.google.common.base.Function;
import com.google.common.collect.Maps;
import google.registry.model.ImmutableObject;
import google.registry.model.contact.ContactResource.Builder;
import google.registry.model.contact.PostalInfo.Type;
import google.registry.model.eppinput.ResourceCommand.AbstractSingleResourceCommand;
import google.registry.model.eppinput.ResourceCommand.ResourceCheck;
@ -75,23 +74,32 @@ public class ContactCommand {
}});
}
@Override
public void applyTo(Builder builder) {
if (authInfo != null) {
builder.setAuthInfo(authInfo);
}
if (disclose != null) {
builder.setDisclose(disclose);
}
if (email != null) {
builder.setEmailAddress(email);
}
if (fax != null) {
builder.setFaxNumber(fax);
}
if (voice != null) {
builder.setVoiceNumber(voice);
}
public ContactPhoneNumber getVoice() {
return voice;
}
public ContactPhoneNumber getFax() {
return fax;
}
public String getEmail() {
return email;
}
public ContactAuthInfo getAuthInfo() {
return authInfo;
}
public Disclose getDisclose() {
return disclose;
}
public PostalInfo getInternationalizedPostalInfo() {
return getPostalInfosAsMap().get(Type.INTERNATIONALIZED);
}
public PostalInfo getLocalizedPostalInfo() {
return getPostalInfosAsMap().get(Type.LOCALIZED);
}
}
@ -134,21 +142,6 @@ public class ContactCommand {
public ContactAuthInfo getAuthInfo() {
return authInfo;
}
@Override
public void applyTo(ContactResource.Builder builder) {
super.applyTo(builder);
if (contactId != null) {
builder.setContactId(contactId);
}
Map<Type, PostalInfo> postalInfosAsMap = getPostalInfosAsMap();
if (postalInfosAsMap.containsKey(Type.INTERNATIONALIZED)) {
builder.setInternationalizedPostalInfo(postalInfosAsMap.get(Type.INTERNATIONALIZED));
}
if (postalInfosAsMap.containsKey(Type.LOCALIZED)) {
builder.setLocalizedPostalInfo(postalInfosAsMap.get(Type.LOCALIZED));
}
}
}
/** A delete command for a {@link ContactResource}. */
@ -204,34 +197,6 @@ public class ContactCommand {
/** The inner change type on a contact update command. */
@XmlType(propOrder = {"postalInfo", "voice", "fax", "email", "authInfo", "disclose"})
public static class Change extends ContactCreateOrChange {
/**
* The spec requires the following behaviors:
* <ul>
* <li>If you update part of a postal info, the fields that you didn't update are unchanged.
* <li>If you update one postal info but not the other, the other is deleted.
* </ul>
* Therefore, if you want to preserve one postal info and update another you need to send the
* update and also something that technically updates the preserved one, even if it only
* "updates" it by setting just one field to the same value.
*/
@Override
public void applyTo(ContactResource.Builder builder) {
super.applyTo(builder);
Map<Type, PostalInfo> postalInfosAsMap = getPostalInfosAsMap();
if (postalInfosAsMap.containsKey(Type.INTERNATIONALIZED)) {
builder.overlayInternationalizedPostalInfo(postalInfosAsMap.get(Type.INTERNATIONALIZED));
if (postalInfosAsMap.size() == 1) {
builder.setLocalizedPostalInfo(null);
}
}
if (postalInfosAsMap.containsKey(Type.LOCALIZED)) {
builder.overlayLocalizedPostalInfo(postalInfosAsMap.get(Type.LOCALIZED));
if (postalInfosAsMap.size() == 1) {
builder.setInternationalizedPostalInfo(null);
}
}
}
}
public static class Change extends ContactCreateOrChange {}
}
}

View file

@ -30,7 +30,6 @@ import google.registry.model.EppResource;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.contact.PostalInfo.Type;
import google.registry.model.eppcommon.AuthInfo;
import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@ -147,7 +146,7 @@ public class ContactResource extends EppResource implements ForeignKeyedEppResou
return email;
}
public AuthInfo getAuthInfo() {
public ContactAuthInfo getAuthInfo() {
return authInfo;
}

View file

@ -47,7 +47,6 @@ import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.launch.LaunchNotice;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.host.HostResource;
import java.util.Set;
import javax.xml.bind.annotation.XmlElement;
@ -221,7 +220,7 @@ public abstract class DomainBase extends EppResource {
.toSet();
}
public AuthInfo getAuthInfo() {
public DomainAuthInfo getAuthInfo() {
return authInfo;
}

View file

@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.collect.Maps.transformValues;
import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.intersection;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.util.CollectionUtils.difference;
import static google.registry.util.CollectionUtils.forceEmptyToNull;
@ -37,7 +36,6 @@ import com.googlecode.objectify.Work;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.contact.ContactResource;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppinput.ResourceCommand.AbstractSingleResourceCommand;
import google.registry.model.eppinput.ResourceCommand.ResourceCheck;
import google.registry.model.eppinput.ResourceCommand.ResourceCreateOrChange;
@ -98,14 +96,8 @@ public class DomainCommand {
return registrant;
}
@Override
public void applyTo(B builder) {
if (registrant != null) {
builder.setRegistrant(registrant);
}
if (authInfo != null) {
builder.setAuthInfo(authInfo);
}
public DomainAuthInfo getAuthInfo() {
return authInfo;
}
}
@ -175,24 +167,10 @@ public class DomainCommand {
}
@Override
public AuthInfo getAuthInfo() {
public DomainAuthInfo getAuthInfo() {
return authInfo;
}
@Override
public void applyTo(DomainBase.Builder<?, ?> builder) {
super.applyTo(builder);
if (fullyQualifiedDomainName != null) {
builder.setFullyQualifiedDomainName(fullyQualifiedDomainName);
}
if (nameservers != null) {
builder.setNameservers(getNameservers());
}
if (contacts != null) {
builder.setContacts(getContacts());
}
}
/** Creates a copy of this {@link Create} with hard links to hosts and contacts. */
@Override
public Create cloneAndLinkReferences(DateTime now) throws InvalidReferencesException {
@ -422,24 +400,6 @@ public class DomainCommand {
}
}
@Override
public void applyTo(DomainBase.Builder<?, ?> builder) throws AddRemoveSameValueException {
super.applyTo(builder);
getInnerChange().applyTo(builder);
AddRemove add = getInnerAdd();
AddRemove remove = getInnerRemove();
if (!intersection(add.getNameservers(), remove.getNameservers()).isEmpty()) {
throw new AddRemoveSameValueException();
}
builder.addNameservers(add.getNameservers());
builder.removeNameservers(remove.getNameservers());
if (!intersection(add.getContacts(), remove.getContacts()).isEmpty()) {
throw new AddRemoveSameValueException();
}
builder.addContacts(add.getContacts());
builder.removeContacts(remove.getContacts());
}
/**
* Creates a copy of this {@link Update} with hard links to hosts and contacts.
*

View file

@ -14,7 +14,6 @@
package google.registry.model.eppinput;
import static com.google.common.collect.Sets.intersection;
import static google.registry.util.CollectionUtils.nullSafeImmutableCopy;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
@ -83,9 +82,7 @@ public interface ResourceCommand {
}
/** A create command, or the inner change (as opposed to add or remove) part of an update. */
public interface ResourceCreateOrChange<B extends Builder<?>> {
public abstract void applyTo(B builder);
}
public interface ResourceCreateOrChange<B extends Builder<?>> {}
/**
* An update command for an {@link EppResource}.
@ -133,18 +130,5 @@ public interface ResourceCommand {
A remove = getNullableInnerRemove();
return remove == null ? new TypeInstantiator<A>(getClass()){}.instantiate() : remove;
}
public void applyTo(B builder) throws AddRemoveSameValueException {
getInnerChange().applyTo(builder);
if (!intersection(getInnerAdd().getStatusValues(), getInnerRemove().getStatusValues())
.isEmpty()) {
throw new AddRemoveSameValueException();
}
builder.addStatusValues(getInnerAdd().getStatusValues());
builder.removeStatusValues(getInnerRemove().getStatusValues());
}
}
/** Exception for adding and removing the same value in {@link ResourceUpdate#applyTo}. */
public static class AddRemoveSameValueException extends Exception {}
}

View file

@ -14,7 +14,6 @@
package google.registry.model.host;
import static com.google.common.collect.Sets.intersection;
import static google.registry.util.CollectionUtils.nullSafeImmutableCopy;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
@ -37,27 +36,9 @@ public class HostCommand {
@XmlTransient
abstract static class HostCreateOrChange extends AbstractSingleResourceCommand
implements ResourceCreateOrChange<HostResource.Builder> {
/** IP Addresses for this host. Can be null if this is an external host. */
@XmlElement(name = "addr")
Set<InetAddress> inetAddresses;
public ImmutableSet<InetAddress> getInetAddresses() {
return nullSafeImmutableCopy(inetAddresses);
}
public String getFullyQualifiedHostName() {
return getTargetId();
}
@Override
public void applyTo(HostResource.Builder builder) {
if (getFullyQualifiedHostName() != null) {
builder.setFullyQualifiedHostName(getFullyQualifiedHostName());
}
if (getInetAddresses() != null) {
builder.setInetAddresses(getInetAddresses());
}
}
}
/**
@ -67,7 +48,15 @@ public class HostCommand {
@XmlType(propOrder = {"targetId", "inetAddresses" })
@XmlRootElement
public static class Create
extends HostCreateOrChange implements ResourceCreateOrChange<HostResource.Builder> {}
extends HostCreateOrChange implements ResourceCreateOrChange<HostResource.Builder> {
/** IP Addresses for this host. Can be null if this is an external host. */
@XmlElement(name = "addr")
Set<InetAddress> inetAddresses;
public ImmutableSet<InetAddress> getInetAddresses() {
return nullSafeImmutableCopy(inetAddresses);
}
}
/** A delete command for a {@link HostResource}. */
@XmlRootElement
@ -124,18 +113,6 @@ public class HostCommand {
}
/** The inner change type on a host update command. */
@XmlType(propOrder = {"targetId", "inetAddresses" })
public static class Change extends HostCreateOrChange {}
@Override
public void applyTo(HostResource.Builder builder) throws AddRemoveSameValueException {
super.applyTo(builder);
if (!intersection(getInnerAdd().getInetAddresses(), getInnerRemove().getInetAddresses())
.isEmpty()) {
throw new AddRemoveSameValueException();
}
builder.addInetAddresses(getInnerAdd().getInetAddresses());
builder.removeInetAddresses(getInnerRemove().getInetAddresses());
}
}
}

View file

@ -24,14 +24,14 @@ import static google.registry.testing.DatastoreHelper.persistResource;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException;
import google.registry.flows.contact.ContactFlowUtils.BadInternationalizedPostalInfoException;
import google.registry.flows.contact.ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException;
import google.registry.flows.exceptions.AddRemoveSameValueEppException;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.flows.exceptions.StatusNotClientSettableException;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.PostalInfo;
@ -84,7 +84,7 @@ public class ContactUpdateFlowTest
}
@Test
public void testSuccess_updatingOnePostalInfoDeletesTheOther() throws Exception {
public void testSuccess_updatingInternationalizedPostalInfoDeletesLocalized() throws Exception {
ContactResource contact =
persistResource(
newContactResource(getUniqueIdFromCommand()).asBuilder()
@ -104,12 +104,60 @@ public class ContactUpdateFlowTest
// the localized one since they are treated as a pair for update purposes.
assertAboutContacts().that(contact)
.hasNonNullLocalizedPostalInfo().and()
.hasInternationalizedPostalInfo(null);
.hasNullInternationalizedPostalInfo();
runFlowAssertResponse(readFile("contact_update_response.xml"));
assertAboutContacts().that(reloadResourceByForeignKey())
.hasLocalizedPostalInfo(null).and()
.hasNonNullInternationalizedPostalInfo();
.hasNullLocalizedPostalInfo().and()
.hasInternationalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("124 Example Dr.", "Suite 200"))
.setCity("Dulles")
.setState("VA")
.setZip("20166-6503")
.setCountryCode("US")
.build())
.build());
}
@Test
public void testSuccess_updatingLocalizedPostalInfoDeletesInternationalized() throws Exception {
setEppInput("contact_update_localized.xml");
ContactResource contact =
persistResource(
newContactResource(getUniqueIdFromCommand()).asBuilder()
.setInternationalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("111 8th Ave", "4th Floor"))
.setCity("New York")
.setState("NY")
.setZip("10011")
.setCountryCode("US")
.build())
.build())
.build());
clock.advanceOneMilli();
// The test xml updates the localized postal info and should therefore implicitly delete
// the internationalized one since they are treated as a pair for update purposes.
assertAboutContacts().that(contact)
.hasNonNullInternationalizedPostalInfo().and()
.hasNullLocalizedPostalInfo();
runFlowAssertResponse(readFile("contact_update_response.xml"));
assertAboutContacts().that(reloadResourceByForeignKey())
.hasNullInternationalizedPostalInfo().and()
.hasLocalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("124 Example Dr.", "Suite 200"))
.setCity("Dulles")
.setState("VA")
.setZip("20166-6503")
.setCountryCode("US")
.build())
.build());
}
@Test
@ -148,6 +196,73 @@ public class ContactUpdateFlowTest
.build());
}
@Test
public void testSuccess_updateOnePostalInfo_touchOtherPostalInfoPreservesIt() throws Exception {
setEppInput("contact_update_partial_postalinfo_preserve_int.xml");
persistResource(
newContactResource(getUniqueIdFromCommand()).asBuilder()
.setLocalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setName("A. Person")
.setOrg("Company Inc.")
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 4th st", "5th Floor"))
.setCity("City")
.setState("AB")
.setZip("12345")
.setCountryCode("US")
.build())
.build())
.setInternationalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setName("B. Person")
.setOrg("Company Co.")
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("100 200th Dr.", "6th Floor"))
.setCity("Town")
.setState("CD")
.setZip("67890")
.setCountryCode("US")
.build())
.build())
.build());
clock.advanceOneMilli();
// The test xml updates the address of the localized postal info. It also sets the name of the
// internationalized postal info to the same value it previously had, which causes it to be
// preserved. If the xml had not mentioned the internationalized one at all it would have been
// deleted.
runFlowAssertResponse(readFile("contact_update_response.xml"));
assertAboutContacts().that(reloadResourceByForeignKey())
.hasLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setName("A. Person")
.setOrg("Company Inc.")
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("456 5th st"))
.setCity("Place")
.setState("CD")
.setZip("54321")
.setCountryCode("US")
.build())
.build())
.and()
.hasInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setName("B. Person")
.setOrg("Company Co.")
.setAddress(new ContactAddress.Builder()
.setStreet(ImmutableList.of("100 200th Dr.", "6th Floor"))
.setCity("Town")
.setState("CD")
.setZip("67890")
.setCountryCode("US")
.build())
.build());
}
@Test
public void testFailure_neverExisted() throws Exception {
thrown.expect(
@ -264,7 +379,7 @@ public class ContactUpdateFlowTest
public void testFailure_addRemoveSameValue() throws Exception {
setEppInput("contact_update_add_remove_same.xml");
persistActiveContact(getUniqueIdFromCommand());
thrown.expect(AddRemoveSameValueEppException.class);
thrown.expect(AddRemoveSameValueException.class);
runFlow();
}
}

View file

@ -0,0 +1,36 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<update>
<contact:update
xmlns:contact="urn:ietf:params:xml:ns:contact-1.0">
<contact:id>sh8013</contact:id>
<contact:add>
<contact:status s="clientDeleteProhibited"/>
</contact:add>
<contact:chg>
<contact:postalInfo type="loc">
<contact:org/>
<contact:addr>
<contact:street>124 Example Dr.</contact:street>
<contact:street>Suite 200</contact:street>
<contact:city>Dulles</contact:city>
<contact:sp>VA</contact:sp>
<contact:pc>20166-6503</contact:pc>
<contact:cc>US</contact:cc>
</contact:addr>
</contact:postalInfo>
<contact:voice>+1.7034444444</contact:voice>
<contact:fax/>
<contact:authInfo>
<contact:pw>2fooBAR</contact:pw>
</contact:authInfo>
<contact:disclose flag="1">
<contact:voice/>
<contact:email/>
</contact:disclose>
</contact:chg>
</contact:update>
</update>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View file

@ -0,0 +1,25 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<update>
<contact:update
xmlns:contact="urn:ietf:params:xml:ns:contact-1.0">
<contact:id>sh8013</contact:id>
<contact:chg>
<contact:postalInfo type="loc">
<contact:addr>
<contact:street>456 5th st</contact:street>
<contact:city>Place</contact:city>
<contact:sp>CD</contact:sp>
<contact:pc>54321</contact:pc>
<contact:cc>US</contact:cc>
</contact:addr>
</contact:postalInfo>
<contact:postalInfo type="int">
<contact:org>Company Co.</contact:org>
</contact:postalInfo>
</contact:chg>
</contact:update>
</update>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View file

@ -32,8 +32,10 @@ import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.flows.EppException.UnimplementedExtensionException;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException;
import google.registry.flows.domain.DomainApplicationUpdateFlow.ApplicationStatusProhibitsUpdateException;
import google.registry.flows.domain.DomainFlowUtils.ApplicationDomainNameMismatchException;
import google.registry.flows.domain.DomainFlowUtils.DuplicateContactForRoleException;
@ -50,10 +52,8 @@ import google.registry.flows.domain.DomainFlowUtils.SecDnsAllUsageException;
import google.registry.flows.domain.DomainFlowUtils.TooManyDsRecordsException;
import google.registry.flows.domain.DomainFlowUtils.TooManyNameserversException;
import google.registry.flows.domain.DomainFlowUtils.UrgentAttributeNotSupportedException;
import google.registry.flows.exceptions.AddRemoveSameValueEppException;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.flows.exceptions.StatusNotClientSettableException;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
@ -587,7 +587,7 @@ public class DomainApplicationUpdateFlowTest
.setNameservers(ImmutableSet.of(Key.create(
loadByForeignKey(HostResource.class, "ns1.example.tld", clock.nowUtc()))))
.build());
thrown.expect(AddRemoveSameValueEppException.class);
thrown.expect(AddRemoveSameValueException.class);
runFlow();
}
@ -601,7 +601,7 @@ public class DomainApplicationUpdateFlowTest
Key.create(
loadByForeignKey(ContactResource.class, "sh8013", clock.nowUtc())))))
.build());
thrown.expect(AddRemoveSameValueEppException.class);
thrown.expect(AddRemoveSameValueException.class);
runFlow();
}

View file

@ -44,8 +44,10 @@ import com.googlecode.objectify.Key;
import google.registry.flows.EppException.UnimplementedExtensionException;
import google.registry.flows.EppRequestSource;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException;
import google.registry.flows.domain.DomainFlowUtils.DuplicateContactForRoleException;
import google.registry.flows.domain.DomainFlowUtils.EmptySecDnsUpdateException;
import google.registry.flows.domain.DomainFlowUtils.FeesMismatchException;
@ -64,11 +66,9 @@ import google.registry.flows.domain.DomainFlowUtils.SecDnsAllUsageException;
import google.registry.flows.domain.DomainFlowUtils.TooManyDsRecordsException;
import google.registry.flows.domain.DomainFlowUtils.TooManyNameserversException;
import google.registry.flows.domain.DomainFlowUtils.UrgentAttributeNotSupportedException;
import google.registry.flows.exceptions.AddRemoveSameValueEppException;
import google.registry.flows.exceptions.OnlyToolCanPassMetadataException;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.flows.exceptions.StatusNotClientSettableException;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.contact.ContactResource;
@ -1003,7 +1003,7 @@ public class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow,
.setNameservers(ImmutableSet.of(Key.create(
loadByForeignKey(HostResource.class, "ns1.example.foo", clock.nowUtc()))))
.build());
thrown.expect(AddRemoveSameValueEppException.class);
thrown.expect(AddRemoveSameValueException.class);
runFlow();
}
@ -1018,7 +1018,7 @@ public class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow,
Key.create(
loadByForeignKey(ContactResource.class, "sh8013", clock.nowUtc())))))
.build());
thrown.expect(AddRemoveSameValueEppException.class);
thrown.expect(AddRemoveSameValueException.class);
runFlow();
}

View file

@ -14,6 +14,7 @@
package google.registry.flows.host;
import static com.google.common.base.Strings.nullToEmpty;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.flows.async.RefreshDnsOnHostRenameAction.QUEUE_ASYNC_HOST_RENAME;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
@ -41,11 +42,12 @@ import com.google.common.net.InetAddresses;
import com.googlecode.objectify.Key;
import google.registry.flows.EppRequestSource;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.flows.exceptions.StatusNotClientSettableException;
import google.registry.flows.host.HostFlowUtils.HostNameTooShallowException;
import google.registry.flows.host.HostFlowUtils.InvalidHostNameException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainDoesNotExistException;
@ -69,14 +71,14 @@ import org.junit.Test;
public class HostUpdateFlowTest extends ResourceFlowTestCase<HostUpdateFlow, HostResource> {
private void setEppHostUpdateInput(
String oldHostName, String newHostName, String addHostAddrs, String remHostAddrs) {
String oldHostName, String newHostName, String ipOrStatusToAdd, String ipOrStatusToRem) {
setEppInput(
"host_update.xml",
ImmutableMap.of(
"OLD-HOSTNAME", oldHostName,
"NEW-HOSTNAME", newHostName,
"ADD-HOSTADDRS", (addHostAddrs == null) ? "" : addHostAddrs,
"REM-HOSTADDRS", (remHostAddrs == null) ? "" : remHostAddrs));
"ADD-HOSTADDRSORSTATUS", nullToEmpty(ipOrStatusToAdd),
"REM-HOSTADDRSORSTATUS", nullToEmpty(ipOrStatusToRem)));
}
public HostUpdateFlowTest() {
@ -793,6 +795,34 @@ public class HostUpdateFlowTest extends ResourceFlowTestCase<HostUpdateFlow, Hos
runFlow();
}
@Test
public void testFailure_addRemoveSameStatusValues() throws Exception {
createTld("tld");
persistActiveDomain("example.tld");
setEppHostUpdateInput(
"ns1.example.tld",
"ns2.example.tld",
"<host:status s=\"clientUpdateProhibited\"/>",
"<host:status s=\"clientUpdateProhibited\"/>");
persistActiveHost(oldHostName());
thrown.expect(AddRemoveSameValueException.class);
runFlow();
}
@Test
public void testFailure_addRemoveSameInetAddresses() throws Exception {
createTld("tld");
persistActiveDomain("example.tld");
setEppHostUpdateInput(
"ns1.example.tld",
"ns2.example.tld",
"<host:addr ip=\"v4\">192.0.2.22</host:addr>",
"<host:addr ip=\"v4\">192.0.2.22</host:addr>");
persistActiveHost(oldHostName());
thrown.expect(AddRemoveSameValueException.class);
runFlow();
}
@Test
public void testFailure_clientProhibitedStatusValue() throws Exception {
createTld("tld");

View file

@ -5,11 +5,11 @@
xmlns:host="urn:ietf:params:xml:ns:host-1.0">
<host:name>%OLD-HOSTNAME%</host:name>
<host:add>
%ADD-HOSTADDRS%
%ADD-HOSTADDRSORSTATUS%
<host:status s="clientUpdateProhibited"/>
</host:add>
<host:rem>
%REM-HOSTADDRS%
%REM-HOSTADDRSORSTATUS%
</host:rem>
<host:chg>
<host:name>%NEW-HOSTNAME%</host:name>

View file

@ -16,14 +16,10 @@ package google.registry.model.contact;
import static google.registry.flows.EppXmlTransformer.marshalInput;
import static google.registry.flows.EppXmlTransformer.validateInput;
import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
import static google.registry.testing.DatastoreHelper.createTld;
import static google.registry.xml.ValidationMode.LENIENT;
import static google.registry.xml.XmlTestUtils.assertXmlEquals;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
import google.registry.model.contact.PostalInfo.Type;
import google.registry.testing.AppEngineRule;
import google.registry.testing.EppLoader;
import org.junit.Rule;
@ -103,75 +99,4 @@ public class ContactCommandTest {
public void testTransferRequest() throws Exception {
doXmlRoundtripTest("contact_transfer_request.xml");
}
@Test
public void testPostalInfoOverlay() {
createTld("foo");
ContactResource contact = new ContactResource.Builder()
.setLocalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setName("loc name")
.build())
.setInternationalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setName("int name")
.build())
.build();
ContactCommand.Update.Change change = new ContactCommand.Update.Change();
// Updating one field of the loc should delete the int and leave the loc otherwise untouched.
change.postalInfo = ImmutableList.of(new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setOrg("org")
.build());
ContactResource.Builder builder = contact.asBuilder();
change.applyTo(builder);
ContactResource changed = builder.build();
assertAboutContacts().that(changed)
.hasNullInternationalizedPostalInfo().and()
.hasLocalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setName("loc name")
.setOrg("org")
.build());
// Updating one field of the int should delete the loc and leave the int otherwise untouched.
change.postalInfo = ImmutableList.of(new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setOrg("org")
.build());
builder = contact.asBuilder();
change.applyTo(builder);
changed = builder.build();
assertAboutContacts().that(changed)
.hasNullLocalizedPostalInfo().and()
.hasInternationalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setName("int name")
.setOrg("org")
.build());
// Updating one field of the int and touching the loc with no changes should preserve both.
change.postalInfo = ImmutableList.of(
new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setName("new int name")
.build(),
new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.build());
builder = contact.asBuilder();
change.applyTo(builder);
changed = builder.build();
assertAboutContacts().that(changed)
.hasLocalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.LOCALIZED)
.setName("loc name")
.build()).and()
.hasInternationalizedPostalInfo(new PostalInfo.Builder()
.setType(Type.INTERNATIONALIZED)
.setName("new int name")
.build());
}
}