// 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.truth.Truth.assertThat;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.testing.DatastoreHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatastoreHelper.stripBillingEventId;
import static google.registry.testing.TestDataHelper.loadFile;
import static google.registry.xml.XmlTestUtils.assertXmlEqualsWithMessage;
import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.MediaType;
import com.google.common.truth.Truth8;
import com.googlecode.objectify.Key;
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.OneTime;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DomainResource;
import google.registry.model.eppcommon.EppXmlTransformer;
import google.registry.model.ofy.Ofy;
import google.registry.model.registry.Registry;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntry.Type;
import google.registry.monitoring.whitebox.EppMetric;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeHttpSession;
import google.registry.testing.FakeResponse;
import google.registry.testing.InjectRule;
import google.registry.testing.ShardableTestCase;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nullable;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Rule;
public class EppTestCase extends ShardableTestCase {
private static final MediaType APPLICATION_EPP_XML_UTF8 =
MediaType.create("application", "epp+xml").withCharset(UTF_8);
@Rule
public final InjectRule inject = new InjectRule();
protected final FakeClock clock = new FakeClock();
private SessionMetadata sessionMetadata;
private TransportCredentials credentials = new PasswordOnlyTransportCredentials();
private EppMetric.Builder eppMetricBuilder;
private boolean isSuperuser;
@Before
public void initTestCase() {
// For transactional flows
inject.setStaticField(Ofy.class, "clock", clock);
}
/**
* Set the transport credentials.
*
*
When the credentials are null, the login flow still checks the EPP password from the xml,
* which is sufficient for all tests that aren't explicitly testing a form of login credentials
* such as {@link EppLoginUserTest}, {@link EppLoginAdminUserTest} and {@link EppLoginTlsTest}.
* Therefore, only those tests should call this method.
*/
protected void setTransportCredentials(TransportCredentials credentials) {
this.credentials = credentials;
}
protected void setIsSuperuser(boolean isSuperuser) {
this.isSuperuser = isSuperuser;
}
public class CommandAsserter {
private final String inputFilename;
private @Nullable final Map inputSubstitutions;
private DateTime now;
private CommandAsserter(
String inputFilename, @Nullable Map inputSubstitutions) {
this.inputFilename = inputFilename;
this.inputSubstitutions = inputSubstitutions;
this.now = DateTime.now(UTC);
}
public CommandAsserter atTime(DateTime now) {
this.now = now;
return this;
}
public CommandAsserter atTime(String now) {
return atTime(DateTime.parse(now));
}
public String hasResponse(String outputFilename) throws Exception {
return hasResponse(outputFilename, null);
}
public String hasResponse(
String outputFilename, @Nullable Map outputSubstitutions) throws Exception {
return assertCommandAndResponse(
inputFilename, inputSubstitutions, outputFilename, outputSubstitutions, now);
}
}
protected CommandAsserter assertThatCommand(String inputFilename) {
return assertThatCommand(inputFilename, null);
}
protected CommandAsserter assertThatCommand(
String inputFilename, @Nullable Map inputSubstitutions) {
return new CommandAsserter(inputFilename, inputSubstitutions);
}
protected CommandAsserter assertThatLogin(String clientId, String password) {
return assertThatCommand("login.xml", ImmutableMap.of("CLID", clientId, "PW", password));
}
protected void assertThatLoginSucceeds(String clientId, String password) throws Exception {
assertThatLogin(clientId, password).hasResponse("generic_success_response.xml");
}
protected void assertThatLogoutSucceeds() throws Exception {
assertThatCommand("logout.xml").hasResponse("logout_response.xml");
}
private String assertCommandAndResponse(
String inputFilename,
@Nullable Map inputSubstitutions,
String outputFilename,
@Nullable Map outputSubstitutions,
DateTime now)
throws Exception {
clock.setTo(now);
String input = loadFile(EppTestCase.class, inputFilename, inputSubstitutions);
String expectedOutput = loadFile(EppTestCase.class, outputFilename, outputSubstitutions);
if (sessionMetadata == null) {
sessionMetadata =
new HttpSessionMetadata(new FakeHttpSession()) {
@Override
public void invalidate() {
// When a session is invalidated, reset the sessionMetadata field.
super.invalidate();
EppTestCase.this.sessionMetadata = null;
}
};
}
String actualOutput = executeXmlCommand(input);
assertXmlEqualsWithMessage(
expectedOutput,
actualOutput,
"Running " + inputFilename + " => " + outputFilename,
"epp.response.resData.infData.roid",
"epp.response.trID.svTRID");
ofy().clearSessionCache(); // Clear the cache like OfyFilter would.
return actualOutput;
}
private String executeXmlCommand(String inputXml) throws Exception {
EppRequestHandler handler = new EppRequestHandler();
FakeResponse response = new FakeResponse();
handler.response = response;
eppMetricBuilder = EppMetric.builderForRequest(clock);
handler.eppController = DaggerEppTestComponent.builder()
.fakesAndMocksModule(FakesAndMocksModule.create(clock, eppMetricBuilder))
.build()
.startRequest()
.eppController();
handler.executeEpp(
sessionMetadata,
credentials,
EppRequestSource.UNIT_TEST,
false, // Not dryRun.
isSuperuser,
inputXml.getBytes(UTF_8));
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(response.getContentType()).isEqualTo(APPLICATION_EPP_XML_UTF8);
String result = response.getPayload();
// Run the resulting xml through the unmarshaller to verify that it was valid.
EppXmlTransformer.validateOutput(result);
return result;
}
protected EppMetric getRecordedEppMetric() {
return eppMetricBuilder.build();
}
/** Create the two administrative contacts and two hosts. */
protected void createContactsAndHosts() throws Exception {
DateTime createTime = DateTime.parse("2000-06-01T00:00:00Z");
createContacts(createTime);
assertThatCommand("host_create.xml", ImmutableMap.of("HOSTNAME", "ns1.example.external"))
.atTime(createTime.plusMinutes(2))
.hasResponse(
"host_create_response.xml",
ImmutableMap.of(
"HOSTNAME", "ns1.example.external",
"CRDATE", createTime.plusMinutes(2).toString()));
assertThatCommand("host_create.xml", ImmutableMap.of("HOSTNAME", "ns2.example.external"))
.atTime(createTime.plusMinutes(3))
.hasResponse(
"host_create_response.xml",
ImmutableMap.of(
"HOSTNAME", "ns2.example.external",
"CRDATE", createTime.plusMinutes(3).toString()));
}
protected void createContacts(DateTime createTime) throws Exception {
assertThatCommand("contact_create_sh8013.xml")
.atTime(createTime)
.hasResponse(
"contact_create_response_sh8013.xml", ImmutableMap.of("CRDATE", createTime.toString()));
assertThatCommand("contact_create_jd1234.xml")
.atTime(createTime.plusMinutes(1))
.hasResponse(
"contact_create_response_jd1234.xml",
ImmutableMap.of("CRDATE", createTime.plusMinutes(1).toString()));
}
/** Creates the domain fakesite.example with two nameservers on it. */
protected void createFakesite() throws Exception {
createContactsAndHosts();
assertThatCommand("domain_create_fakesite.xml")
.atTime("2000-06-01T00:04:00Z")
.hasResponse(
"domain_create_response.xml",
ImmutableMap.of(
"DOMAIN", "fakesite.example",
"CRDATE", "2000-06-01T00:04:00.0Z",
"EXDATE", "2002-06-01T00:04:00.0Z"));
assertThatCommand("domain_info_fakesite.xml")
.atTime("2000-06-06T00:00:00Z")
.hasResponse("domain_info_response_fakesite_ok.xml");
}
/** Creates ns3.fakesite.example as a host, then adds it to fakesite. */
protected void createSubordinateHost() throws Exception {
// Add the fakesite nameserver (requires that domain is already created).
assertThatCommand("host_create_fakesite.xml")
.atTime("2000-06-06T00:01:00Z")
.hasResponse("host_create_response_fakesite.xml");
// Add new nameserver to domain.
assertThatCommand("domain_update_add_nameserver_fakesite.xml")
.atTime("2000-06-08T00:00:00Z")
.hasResponse("generic_success_response.xml");
// Verify new nameserver was added.
assertThatCommand("domain_info_fakesite.xml")
.atTime("2000-06-08T00:01:00Z")
.hasResponse("domain_info_response_fakesite_3_nameservers.xml");
// Verify that nameserver's data was set correctly.
assertThatCommand("host_info_fakesite.xml")
.atTime("2000-06-08T00:02:00Z")
.hasResponse("host_info_response_fakesite_linked.xml");
}
/** Makes a one-time billing event corresponding to the given domain's creation. */
protected static BillingEvent.OneTime makeOneTimeCreateBillingEvent(
DomainResource domain, DateTime createTime) {
return new BillingEvent.OneTime.Builder()
.setReason(Reason.CREATE)
.setTargetId(domain.getFullyQualifiedDomainName())
.setClientId(domain.getCurrentSponsorClientId())
.setCost(Money.parse("USD 26.00"))
.setPeriodYears(2)
.setEventTime(createTime)
.setBillingTime(createTime.plus(Registry.get(domain.getTld()).getRenewGracePeriodLength()))
.setParent(getOnlyHistoryEntryOfType(domain, Type.DOMAIN_CREATE))
.build();
}
/** Makes a recurring billing event corresponding to the given domain's creation. */
protected static BillingEvent.Recurring makeRecurringCreateBillingEvent(
DomainResource domain, DateTime eventTime, DateTime endTime) {
return makeRecurringCreateBillingEvent(
domain, getOnlyHistoryEntryOfType(domain, Type.DOMAIN_CREATE), eventTime, endTime);
}
/** Makes a recurring billing event corresponding to the given history entry. */
protected static BillingEvent.Recurring makeRecurringCreateBillingEvent(
DomainResource domain, HistoryEntry historyEntry, DateTime eventTime, DateTime endTime) {
return new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(domain.getFullyQualifiedDomainName())
.setClientId(domain.getCurrentSponsorClientId())
.setEventTime(eventTime)
.setRecurrenceEndTime(endTime)
.setParent(historyEntry)
.build();
}
/** Makes a cancellation billing event cancelling out the given domain create billing event. */
protected static BillingEvent.Cancellation makeCancellationBillingEventFor(
DomainResource domain,
OneTime billingEventToCancel,
DateTime createTime,
DateTime deleteTime) {
return new BillingEvent.Cancellation.Builder()
.setTargetId(domain.getFullyQualifiedDomainName())
.setClientId(domain.getCurrentSponsorClientId())
.setEventTime(deleteTime)
.setOneTimeEventKey(findKeyToActualOneTimeBillingEvent(billingEventToCancel))
.setBillingTime(createTime.plus(Registry.get(domain.getTld()).getRenewGracePeriodLength()))
.setReason(Reason.CREATE)
.setParent(getOnlyHistoryEntryOfType(domain, Type.DOMAIN_DELETE))
.build();
}
/**
* Finds the Key to the actual one-time create billing event associated with a domain's creation.
*
* This is used in the situation where we have created an expected billing event associated
* with the domain's creation (which is passed as the parameter here), then need to locate the key
* to the actual billing event in Datastore that would be seen on a Cancellation billing event.
* This is necessary because the ID will be different even though all the rest of the fields are
* the same.
*/
protected static Key findKeyToActualOneTimeBillingEvent(OneTime expectedBillingEvent) {
Optional actualCreateBillingEvent =
ofy()
.load()
.type(BillingEvent.OneTime.class)
.list()
.stream()
.filter(
b ->
Objects.equals(
stripBillingEventId(b), stripBillingEventId(expectedBillingEvent)))
.findFirst();
Truth8.assertThat(actualCreateBillingEvent).isPresent();
return Key.create(actualCreateBillingEvent.get());
}
}