google-nomulus/javatests/google/registry/flows/FlowTestCase.java
mcilwain ed910455b0 Add more absent clTrid unit tests
In RFC 5730, clTrid is specified as optional. We ran into an error earlier this
year in which a registrar was not passing a client transaction id and we didn't
handle it correctly. So, this CL adds some tests of common EPP operations verify
that they work correctly when the clTrid is not specified.

This also slightly improves some flow logic to make it more obvious at first
glance that clTrid is indeed optional.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=202000845
2018-06-27 15:28:52 -04:00

370 lines
15 KiB
Java

// Copyright 2017 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;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Sets.difference;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.flows.EppXmlTransformer.marshal;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.testing.DatastoreHelper.POLL_MESSAGE_ID_STRIPPER;
import static google.registry.testing.DatastoreHelper.getPollMessages;
import static google.registry.testing.DatastoreHelper.stripBillingEventId;
import static google.registry.xml.XmlTestUtils.assertXmlEquals;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.ObjectArrays;
import com.google.common.collect.Streams;
import google.registry.config.RegistryConfig.ConfigModule.TmchCaMode;
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
import google.registry.flows.picker.FlowPicker;
import google.registry.model.billing.BillingEvent;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.ofy.Ofy;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tmch.ClaimsListShard.ClaimsListSingleton;
import google.registry.monitoring.whitebox.EppMetric;
import google.registry.testing.AppEngineRule;
import google.registry.testing.EppLoader;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeHttpSession;
import google.registry.testing.InjectRule;
import google.registry.testing.ShardableTestCase;
import google.registry.testing.TestDataHelper;
import google.registry.tmch.TmchCertificateAuthority;
import google.registry.tmch.TmchXmlSignature;
import google.registry.util.TypeUtils.TypeInstantiator;
import google.registry.xml.ValidationMode;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Rule;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Base class for resource flow unit tests.
*
* @param <F> the flow type
*/
@RunWith(JUnit4.class)
public abstract class FlowTestCase<F extends Flow> extends ShardableTestCase {
/** Whether to actually write to Datastore or just simulate. */
public enum CommitMode { LIVE, DRY_RUN }
/** Whether to run in normal or superuser mode. */
public enum UserPrivileges { NORMAL, SUPERUSER }
@Rule
public final AppEngineRule appEngine = AppEngineRule.builder()
.withDatastore()
.withTaskQueue()
.build();
@Rule
public final InjectRule inject = new InjectRule();
protected EppLoader eppLoader;
protected SessionMetadata sessionMetadata;
protected FakeClock clock = new FakeClock(DateTime.now(UTC));
protected TransportCredentials credentials = new PasswordOnlyTransportCredentials();
protected EppRequestSource eppRequestSource = EppRequestSource.UNIT_TEST;
protected TmchXmlSignature testTmchXmlSignature = null;
private EppMetric.Builder eppMetricBuilder;
@Before
public void init() {
sessionMetadata = new HttpSessionMetadata(new FakeHttpSession());
sessionMetadata.setClientId("TheRegistrar");
sessionMetadata.setServiceExtensionUris(ProtocolDefinition.getVisibleServiceExtensionUris());
ofy().saveWithoutBackup().entity(new ClaimsListSingleton()).now();
inject.setStaticField(Ofy.class, "clock", clock); // For transactional flows.
}
protected void removeServiceExtensionUri(String uri) {
sessionMetadata.setServiceExtensionUris(
difference(sessionMetadata.getServiceExtensionUris(), ImmutableSet.of(uri)));
}
protected void setEppInput(String inputFilename) {
eppLoader = new EppLoader(this, inputFilename, ImmutableMap.of());
}
protected void setEppInput(String inputFilename, Map<String, String> substitutions) {
eppLoader = new EppLoader(this, inputFilename, substitutions);
}
/** Returns the EPP data loaded by a previous call to setEppInput. */
protected EppInput getEppInput() throws EppException {
return eppLoader.getEpp();
}
protected EppMetric getEppMetric() {
checkNotNull(eppMetricBuilder, "Run the flow first before checking EPP metrics");
return eppMetricBuilder.build();
}
protected String loadFile(String filename) {
return TestDataHelper.loadFile(getClass(), filename);
}
protected String loadFile(String filename, Map<String, String> substitutions) {
return TestDataHelper.loadFile(getClass(), filename, substitutions);
}
@Nullable
protected String getClientTrid() throws Exception {
return eppLoader.getEpp().getCommandWrapper().getClTrid().orElse(null);
}
/** Gets the client ID that the flow will run as. */
protected String getClientIdForFlow() {
return sessionMetadata.getClientId();
}
/** Sets the client ID that the flow will run as. */
protected void setClientIdForFlow(String clientId) {
sessionMetadata.setClientId(clientId);
}
public void assertTransactionalFlow(boolean isTransactional) throws Exception {
Class<? extends Flow> flowClass = FlowPicker.getFlowClass(eppLoader.getEpp());
if (isTransactional) {
assertThat(flowClass).isAssignableTo(TransactionalFlow.class);
} else {
// There's no "isNotAssignableTo" in Truth.
assertThat(TransactionalFlow.class.isAssignableFrom(flowClass))
.named(flowClass.getSimpleName() + " implements TransactionalFlow")
.isFalse();
}
}
protected void assertNoHistory() {
assertThat(ofy().load().type(HistoryEntry.class)).isEmpty();
}
public <T> T getOnlyGlobalResource(Class<T> clazz) {
return Iterables.getOnlyElement(ofy().load().type(clazz));
}
/** Helper to remove the grace period's billing event key to facilitate comparison. */
/** A helper class that sets the billing event parent history entry to facilitate comparison. */
public static class BillingEventParentSetter implements Function<BillingEvent, BillingEvent> {
private HistoryEntry historyEntry;
public static BillingEventParentSetter withParent(HistoryEntry historyEntry) {
BillingEventParentSetter instance = new BillingEventParentSetter();
instance.historyEntry = historyEntry;
return instance;
}
@Override
public BillingEvent apply(BillingEvent billingEvent) {
return billingEvent.asBuilder().setParent(historyEntry).build();
}
private BillingEventParentSetter() {}
}
/**
* Helper to facilitate comparison of maps of GracePeriods to BillingEvents. This takes a map of
* GracePeriods to BillingEvents and returns a map of the same entries that ignores the keys
* on the grace periods and the IDs on the billing events (by setting them all to the same dummy
* values), since they will vary between instantiations even when the other data is the same.
*/
private ImmutableMap<GracePeriod, BillingEvent>
canonicalizeGracePeriods(ImmutableMap<GracePeriod, ? extends BillingEvent> gracePeriods) {
ImmutableMap.Builder<GracePeriod, BillingEvent> builder = new ImmutableMap.Builder<>();
for (Map.Entry<GracePeriod, ? extends BillingEvent> entry : gracePeriods.entrySet()) {
builder.put(
((Function<GracePeriod, GracePeriod>)
gracePeriod ->
GracePeriod.create(
gracePeriod.isSunrushAddGracePeriod()
? GracePeriodStatus.SUNRUSH_ADD
: gracePeriod.getType(),
gracePeriod.getExpirationTime(),
gracePeriod.getClientId(),
null))
.apply(entry.getKey()),
stripBillingEventId(entry.getValue()));
}
return builder.build();
}
private static BillingEvent expandGracePeriod(GracePeriod gracePeriod) {
assertThat(gracePeriod.hasBillingEvent())
.named("Billing event is present for grace period: " + gracePeriod)
.isTrue();
return ofy()
.load()
.key(
firstNonNull(
gracePeriod.getOneTimeBillingEvent(), gracePeriod.getRecurringBillingEvent()))
.now();
}
/**
* Assert that the actual grace periods and the corresponding billing events referenced from
* their keys match the expected map of grace periods to billing events. For the expected map,
* the keys on the grace periods and IDs on the billing events are ignored.
*/
public void assertGracePeriods(
Iterable<GracePeriod> actual,
ImmutableMap<GracePeriod, ? extends BillingEvent> expected) {
assertThat(canonicalizeGracePeriods(Maps.toMap(actual, FlowTestCase::expandGracePeriod)))
.isEqualTo(canonicalizeGracePeriods(expected));
}
public void assertPollMessages(
String clientId,
PollMessage... expected) {
assertPollMessagesHelper(getPollMessages(clientId), expected);
}
public void assertPollMessages(
String clientId,
DateTime now,
PollMessage... expected) {
assertPollMessagesHelper(getPollMessages(clientId, now), expected);
}
public void assertPollMessages(PollMessage... expected) {
assertPollMessagesHelper(getPollMessages(), expected);
}
/** Assert that the list matches all the poll messages in the fake Datastore. */
public void assertPollMessagesHelper(
Iterable<PollMessage> pollMessages, PollMessage... expected) {
// Ordering is irrelevant but duplicates should be considered independently.
assertThat(
Streams.stream(pollMessages).map(POLL_MESSAGE_ID_STRIPPER).collect(toImmutableList()))
.containsExactlyElementsIn(
Arrays.stream(expected).map(POLL_MESSAGE_ID_STRIPPER).collect(toImmutableList()));
}
private EppOutput runFlowInternal(CommitMode commitMode, UserPrivileges userPrivileges)
throws Exception {
eppMetricBuilder = EppMetric.builderForRequest("request-id-1", clock);
// Assert that the xml triggers the flow we expect.
assertThat(FlowPicker.getFlowClass(eppLoader.getEpp()))
.isEqualTo(new TypeInstantiator<F>(getClass()){}.getExactType());
// Run the flow.
TmchXmlSignature tmchXmlSignature =
testTmchXmlSignature != null
? testTmchXmlSignature
: new TmchXmlSignature(new TmchCertificateAuthority(tmchCaMode));
return DaggerEppTestComponent.builder()
.fakesAndMocksModule(FakesAndMocksModule.create(clock, eppMetricBuilder, tmchXmlSignature))
.build()
.startRequest()
.flowComponentBuilder()
.flowModule(
new FlowModule.Builder()
.setSessionMetadata(sessionMetadata)
.setCredentials(credentials)
.setEppRequestSource(eppRequestSource)
.setIsDryRun(commitMode.equals(CommitMode.DRY_RUN))
.setIsSuperuser(userPrivileges.equals(UserPrivileges.SUPERUSER))
.setInputXmlBytes(eppLoader.getEppXml().getBytes(UTF_8))
.setEppInput(eppLoader.getEpp())
.build())
.build()
.flowRunner()
.run(eppMetricBuilder);
}
/** Run a flow and marshal the result to EPP, or throw if it doesn't validate. */
public EppOutput runFlow(CommitMode commitMode, UserPrivileges userPrivileges) throws Exception {
EppOutput output = runFlowInternal(commitMode, userPrivileges);
marshal(output, ValidationMode.STRICT);
return output;
}
/** Shortcut to call {@link #runFlow(CommitMode, UserPrivileges)} as normal user and live run. */
public EppOutput runFlow() throws Exception {
return runFlow(CommitMode.LIVE, UserPrivileges.NORMAL);
}
/** Shortcut to call {@link #runFlow(CommitMode, UserPrivileges)} as super user and live run. */
public EppOutput runFlowAsSuperuser() throws Exception {
return runFlow(CommitMode.LIVE, UserPrivileges.SUPERUSER);
}
/** Run a flow, marshal the result to EPP, and assert that the output is as expected. */
public EppOutput runFlowAssertResponse(
CommitMode commitMode, UserPrivileges userPrivileges, String xml, String... ignoredPaths)
throws Exception {
// Always ignore the server trid, since it's generated and meaningless to flow correctness.
String[] ignoredPathsPlusTrid = ObjectArrays.concat(ignoredPaths, "epp.response.trID.svTRID");
EppOutput output = runFlowInternal(commitMode, userPrivileges);
if (output.isResponse()) {
assertThat(output.isSuccess()).isTrue();
}
try {
assertXmlEquals(
xml, new String(marshal(output, ValidationMode.STRICT), UTF_8), ignoredPathsPlusTrid);
} catch (Throwable e) {
assertXmlEquals(
xml, new String(marshal(output, ValidationMode.LENIENT), UTF_8), ignoredPathsPlusTrid);
// If it was a marshaling error, augment the output.
throw new Exception(
String.format(
"Invalid xml.\nExpected:\n%s\n\nActual:\n%s\n",
xml,
marshal(output, ValidationMode.LENIENT)),
e);
}
// Clear the cache so that we don't see stale results in tests.
ofy().clearSessionCache();
return output;
}
private TmchCaMode tmchCaMode = TmchCaMode.PILOT;
protected void useTmchProdCert() {
tmchCaMode = TmchCaMode.PRODUCTION;
}
public EppOutput dryRunFlowAssertResponse(String xml, String... ignoredPaths) throws Exception {
List<Object> beforeEntities = ofy().load().list();
EppOutput output =
runFlowAssertResponse(CommitMode.DRY_RUN, UserPrivileges.NORMAL, xml, ignoredPaths);
assertThat(ofy().load()).containsExactlyElementsIn(beforeEntities);
return output;
}
public EppOutput runFlowAssertResponse(String xml, String... ignoredPaths) throws Exception {
return runFlowAssertResponse(CommitMode.LIVE, UserPrivileges.NORMAL, xml, ignoredPaths);
}
}