diff --git a/java/google/registry/rdap/RdapDomainSearchAction.java b/java/google/registry/rdap/RdapDomainSearchAction.java index a12953679..6152c9651 100644 --- a/java/google/registry/rdap/RdapDomainSearchAction.java +++ b/java/google/registry/rdap/RdapDomainSearchAction.java @@ -52,6 +52,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.stream.Stream; import javax.inject.Inject; import org.joda.time.DateTime; @@ -120,6 +121,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { throw new BadRequestException( "You must specify either name=XXXX, nsLdhName=YYYY or nsIp=ZZZZ"); } + decodeCursorToken(); RdapSearchResults results; if (nameParam.isPresent()) { metricInformationBuilder.setSearchType(SearchType.BY_DOMAIN_NAME); @@ -163,7 +165,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { rdapJsonFormatter.addTopLevelEntries( builder, BoilerplateType.DOMAIN, - results.getIncompletenessWarnings(), + getNotices(results), ImmutableList.of(), fullServletPath); return builder.build(); @@ -241,11 +243,14 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { .load() .type(DomainResource.class) .filter("fullyQualifiedDomainName <", partialStringQuery.getNextInitialString()) - .filter("fullyQualifiedDomainName >=", partialStringQuery.getInitialString()) - .limit(querySizeLimit); + .filter("fullyQualifiedDomainName >=", partialStringQuery.getInitialString()); + if (cursorString.isPresent()) { + query = query.filter("fullyQualifiedDomainName >", cursorString.get()); + } if (partialStringQuery.getSuffix() != null) { query = query.filter("tld", partialStringQuery.getSuffix()); } + query = query.limit(querySizeLimit); // Always check for visibility, because we couldn't look at the deletionTime in the query. return makeSearchResults(getMatchingResources(query, true, now, querySizeLimit), now); } @@ -261,9 +266,11 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { ofy() .load() .type(DomainResource.class) - .filter("tld", tld) - .order("fullyQualifiedDomainName") - .limit(querySizeLimit); + .filter("tld", tld); + if (cursorString.isPresent()) { + query = query.filter("fullyQualifiedDomainName >", cursorString.get()); + } + query = query.order("fullyQualifiedDomainName").limit(querySizeLimit); return makeSearchResults(getMatchingResources(query, true, now, querySizeLimit), now); } @@ -459,10 +466,19 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { .filter("nsHosts in", chunk); if (!shouldIncludeDeleted()) { query = query.filter("deletionTime >", now); + // If we are not performing an inequality query, we can filter on the cursor in the query. + // Otherwise, we will need to filter the results afterward. + } else if (cursorString.isPresent()) { + query = query.filter("fullyQualifiedDomainName >", cursorString.get()); } - Streams.stream(query) - .filter(domain -> isAuthorized(domain, now)) - .forEach(domainSetBuilder::add); + Stream stream = + Streams.stream(query).filter(domain -> isAuthorized(domain, now)); + if (cursorString.isPresent()) { + stream = + stream.filter( + domain -> (domain.getFullyQualifiedDomainName().compareTo(cursorString.get()) > 0)); + } + stream.forEach(domainSetBuilder::add); } List domains = domainSetBuilder.build().asList(); metricInformationBuilder.setNumHostsRetrieved(numHostKeysSearched); @@ -519,7 +535,9 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { (domains.size() > 1) ? OutputDataType.SUMMARY : OutputDataType.FULL; RdapAuthorization authorization = getAuthorization(); List> jsonList = new ArrayList<>(); + Optional newCursor = Optional.empty(); for (DomainResource domain : domains) { + newCursor = Optional.of(domain.getFullyQualifiedDomainName()); jsonList.add( rdapJsonFormatter.makeRdapJsonForDomain( domain, false, fullServletPath, rdapWhoisServer, now, outputDataType, authorization)); @@ -533,6 +551,10 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { : incompletenessWarningType; metricInformationBuilder.setIncompletenessWarningType(finalIncompletenessWarningType); return RdapSearchResults.create( - ImmutableList.copyOf(jsonList), finalIncompletenessWarningType, Optional.empty()); + ImmutableList.copyOf(jsonList), + finalIncompletenessWarningType, + (finalIncompletenessWarningType == IncompletenessWarningType.TRUNCATED) + ? newCursor + : Optional.empty()); } } diff --git a/javatests/google/registry/rdap/RdapDomainSearchActionTest.java b/javatests/google/registry/rdap/RdapDomainSearchActionTest.java index 1b0c8eee9..951c112f9 100644 --- a/javatests/google/registry/rdap/RdapDomainSearchActionTest.java +++ b/javatests/google/registry/rdap/RdapDomainSearchActionTest.java @@ -36,6 +36,7 @@ import static org.mockito.Mockito.when; import com.google.appengine.api.users.User; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Range; @@ -64,6 +65,7 @@ import google.registry.testing.InjectRule; import google.registry.ui.server.registrar.SessionUtils; import google.registry.util.Idn; import java.net.IDN; +import java.net.URLDecoder; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -71,6 +73,7 @@ import java.util.Optional; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; import org.joda.time.DateTime; +import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.JSONValue; import org.junit.Before; @@ -93,7 +96,6 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { public final InjectRule inject = new InjectRule(); private final HttpServletRequest request = mock(HttpServletRequest.class); - private final FakeResponse response = new FakeResponse(); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01T00:00:00Z")); private final SessionUtils sessionUtils = mock(SessionUtils.class); private final User user = new User("rdap.user@example.com", "gmail.com", "12345"); @@ -101,6 +103,8 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { private final UserAuthInfo adminUserAuthInfo = UserAuthInfo.create(user, true); private final RdapDomainSearchAction action = new RdapDomainSearchAction(); + private FakeResponse response = new FakeResponse(); + private Registrar registrar; private DomainResource domainCatLol; private DomainResource domainCatLol2; @@ -118,30 +122,49 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { enum RequestType { NONE, NAME, NS_LDH_NAME, NS_IP } private Object generateActualJson(RequestType requestType, String paramValue) { + return generateActualJson(requestType, paramValue, null); + } + + private Object generateActualJson( + RequestType requestType, String paramValue, String cursor) { action.requestPath = RdapDomainSearchAction.PATH; + String requestTypeParam = null; switch (requestType) { case NAME: action.nameParam = Optional.of(paramValue); action.nsLdhNameParam = Optional.empty(); action.nsIpParam = Optional.empty(); + requestTypeParam = "name"; break; case NS_LDH_NAME: action.nameParam = Optional.empty(); action.nsLdhNameParam = Optional.of(paramValue); action.nsIpParam = Optional.empty(); + requestTypeParam = "nsLdhName"; break; case NS_IP: action.nameParam = Optional.empty(); action.nsLdhNameParam = Optional.empty(); action.nsIpParam = Optional.of(paramValue); + requestTypeParam = "nsIp"; break; default: action.nameParam = Optional.empty(); action.nsLdhNameParam = Optional.empty(); action.nsIpParam = Optional.empty(); + requestTypeParam = ""; break; } - action.rdapResultSetMaxSize = 4; + if (paramValue != null) { + if (cursor == null) { + action.parameterMap = ImmutableListMultimap.of(requestTypeParam, paramValue); + action.cursorTokenParam = Optional.empty(); + } else { + action.parameterMap = + ImmutableListMultimap.of(requestTypeParam, paramValue, "cursor", cursor); + action.cursorTokenParam = Optional.of(cursor); + } + } action.run(); return JSONValue.parse(response.getPayload()); } @@ -371,6 +394,8 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { action.request = request; action.requestMethod = Action.Method.GET; action.fullServletPath = "https://example.com/rdap"; + action.requestUrl = "https://example.com/rdap/domains"; + action.parameterMap = ImmutableListMultimap.of(); action.requestMethod = POST; action.response = response; action.registrarParam = Optional.empty(); @@ -381,6 +406,8 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { action.sessionUtils = sessionUtils; action.authResult = AuthResult.create(AuthLevel.USER, userAuthInfo); action.rdapMetrics = rdapMetrics; + action.cursorTokenParam = Optional.empty(); + action.rdapResultSetMaxSize = 4; } private void login(String clientId) { @@ -426,6 +453,30 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { String domain4Name, String domain4Handle, String expectedOutputFile) { + return generateExpectedJsonForFourDomains( + domain1Name, + domain1Handle, + domain2Name, + domain2Handle, + domain3Name, + domain3Handle, + domain4Name, + domain4Handle, + "none", + expectedOutputFile); + } + + private Object generateExpectedJsonForFourDomains( + String domain1Name, + String domain1Handle, + String domain2Name, + String domain2Handle, + String domain3Name, + String domain3Handle, + String domain4Name, + String domain4Handle, + String nextQuery, + String expectedOutputFile) { return JSONValue.parse( loadFile( this.getClass(), @@ -444,6 +495,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { .put("DOMAINPUNYCODENAME4", domain4Name) .put("DOMAINNAME4", IDN.toUnicode(domain4Name)) .put("DOMAINHANDLE4", domain4Handle) + .put("NEXT_QUERY", nextQuery) .build())); } @@ -567,6 +619,30 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { String domainHandle3, String domainName4, String domainHandle4) { + return readMultiDomainFile( + fileName, + domainName1, + domainHandle1, + domainName2, + domainHandle2, + domainName3, + domainHandle3, + domainName4, + domainHandle4, + "none"); + } + + private Object readMultiDomainFile( + String fileName, + String domainName1, + String domainHandle1, + String domainName2, + String domainHandle2, + String domainName3, + String domainHandle3, + String domainName4, + String domainHandle4, + String nextQuery) { return JSONValue.parse(loadFile( this.getClass(), fileName, @@ -579,6 +655,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { .put("DOMAINHANDLE3", domainHandle3) .put("DOMAINNAME4", domainName4) .put("DOMAINHANDLE4", domainHandle4) + .put("NEXT_QUERY", nextQuery) .build())); } @@ -645,6 +722,26 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { String domainRoid3, String domainRoid4, String fileName) { + runSuccessfulTestWithFourDomains( + requestType, + queryString, + domainRoid1, + domainRoid2, + domainRoid3, + domainRoid4, + "none", + fileName); + } + + private void runSuccessfulTestWithFourDomains( + RequestType requestType, + String queryString, + String domainRoid1, + String domainRoid2, + String domainRoid3, + String domainRoid4, + String nextQuery, + String fileName) { rememberWildcardType(queryString); assertThat(generateActualJson(requestType, queryString)) .isEqualTo( @@ -657,7 +754,8 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { "domain3.lol", domainRoid3, "domain4.lol", - domainRoid4)); + domainRoid4, + nextQuery)); assertThat(response.getStatus()).isEqualTo(200); } @@ -731,6 +829,50 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { verifyMetrics(searchType, numDomainsRetrieved, numHostsRetrieved); } + /** + * Checks multi-page result set navigation using the cursor. + * + *

If there are more results than the max result set size, the RDAP code returns a cursor token + * which can be used in a subsequent call to get the next chunk of results. + * + * @param requestType the type of query (name, nameserver name or nameserver address) + * @param paramValue the query string + * @param expectedNames an immutable list of the domain names we expect to retrieve + */ + private void checkCursorNavigation( + RequestType requestType, String paramValue, ImmutableList expectedNames) + throws Exception { + String cursor = null; + int expectedNameOffset = 0; + int expectedPageCount = + (expectedNames.size() + action.rdapResultSetMaxSize - 1) / action.rdapResultSetMaxSize; + for (int pageNum = 0; pageNum < expectedPageCount; pageNum++) { + Object results = generateActualJson(requestType, paramValue, cursor); + assertThat(response.getStatus()).isEqualTo(200); + String linkToNext = RdapTestHelper.getLinkToNext(results); + if (pageNum == expectedPageCount - 1) { + assertThat(linkToNext).isNull(); + } else { + assertThat(linkToNext).isNotNull(); + int pos = linkToNext.indexOf("cursor="); + assertThat(pos).isAtLeast(0); + cursor = URLDecoder.decode(linkToNext.substring(pos + 7), "UTF-8"); + Object searchResults = ((JSONObject) results).get("domainSearchResults"); + assertThat(searchResults).isInstanceOf(JSONArray.class); + assertThat(((JSONArray) searchResults)).hasSize(action.rdapResultSetMaxSize); + for (Object item : ((JSONArray) searchResults)) { + assertThat(item).isInstanceOf(JSONObject.class); + Object name = ((JSONObject) item).get("ldhName"); + assertThat(name).isNotNull(); + assertThat(name).isInstanceOf(String.class); + assertThat(name).isEqualTo(expectedNames.get(expectedNameOffset++)); + } + response = new FakeResponse(); + action.response = response; + } + } + } + @Test public void testInvalidPath_rejected() throws Exception { action.requestPath = RdapDomainSearchAction.PATH + "/path"; @@ -1032,6 +1174,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { "cat.example", "21-EXAMPLE", "cat.lol", "C-LOL", "cat.xn--q9jyb4c", "2D-Q9JYB4C", + "name=cat*&cursor=Y2F0LnhuLS1xOWp5YjRj", "rdap_domains_four_with_one_unicode_truncated.json")); assertThat(response.getStatus()).isEqualTo(200); verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED); @@ -1192,6 +1335,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { "46-LOL", "45-LOL", "44-LOL", + "name=domain*.lol&cursor=ZG9tYWluNC5sb2w%3D", "rdap_domains_four_truncated.json"); verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED); } @@ -1210,7 +1354,8 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { "domain1.lol", "46-LOL", "domain2.lol", - "45-LOL")); + "45-LOL", + "name=*.lol&cursor=ZG9tYWluMi5sb2w%3D")); verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED); } @@ -1226,6 +1371,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { "4A-LOL", "49-LOL", "48-LOL", + "name=domain*.lol&cursor=ZG9tYWluNC5sb2w%3D", "rdap_domains_four_truncated.json"); verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED); } @@ -1244,11 +1390,54 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { "domain24.lol", "49-LOL", "domain30.lol", - "43-LOL")); + "43-LOL", + "name=domain*.lol&cursor=ZG9tYWluMzAubG9s")); assertThat(response.getStatus()).isEqualTo(200); verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(27L), IncompletenessWarningType.TRUNCATED); } + @Test + public void testDomainMatch_cursorNavigationWithInitialString() throws Exception { + createManyDomainsAndHosts(11, 1, 2); + checkCursorNavigation( + RequestType.NAME, + "domain*.lol", + ImmutableList.of( + "domain1.lol", + "domain10.lol", + "domain11.lol", + "domain2.lol", + "domain3.lol", + "domain4.lol", + "domain5.lol", + "domain6.lol", + "domain7.lol", + "domain8.lol", + "domain9.lol")); + } + + @Test + public void testDomainMatch_cursorNavigationWithTldSuffix() throws Exception { + createManyDomainsAndHosts(11, 1, 2); + checkCursorNavigation( + RequestType.NAME, + "*.lol", + ImmutableList.of( + "cat.lol", + "cat2.lol", + "domain1.lol", + "domain10.lol", + "domain11.lol", + "domain2.lol", + "domain3.lol", + "domain4.lol", + "domain5.lol", + "domain6.lol", + "domain7.lol", + "domain8.lol", + "domain9.lol")); + } + @Test public void testNameserverMatch_foundMultiple() throws Exception { rememberWildcardType("ns1.cat.lol"); @@ -1595,6 +1784,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { "46-LOL", "45-LOL", "44-LOL", + "nsLdhName=ns1.domain1.lol&cursor=ZG9tYWluNC5sb2w%3D", "rdap_domains_four_truncated.json"); verifyMetrics( SearchType.BY_NAMESERVER_NAME, @@ -1613,6 +1803,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { "4A-LOL", "49-LOL", "48-LOL", + "nsLdhName=ns1.domain1.lol&cursor=ZG9tYWluNC5sb2w%3D", "rdap_domains_four_truncated.json"); verifyMetrics( SearchType.BY_NAMESERVER_NAME, @@ -1666,6 +1857,23 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { IncompletenessWarningType.MIGHT_BE_INCOMPLETE); } + @Test + public void testNameserverMatch_cursorNavigation() throws Exception { + createManyDomainsAndHosts(8, 1, 2); + checkCursorNavigation( + RequestType.NS_LDH_NAME, + "ns*.domain1.lol", + ImmutableList.of( + "domain1.lol", + "domain2.lol", + "domain3.lol", + "domain4.lol", + "domain5.lol", + "domain6.lol", + "domain7.lol", + "domain8.lol")); + } + @Test public void testAddressMatchV4Address_invalidAddress() throws Exception { rememberWildcardType("1.2.3.4.5.6.7.8.9"); @@ -1819,6 +2027,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { "46-LOL", "45-LOL", "44-LOL", + "nsIp=5.5.5.1&cursor=ZG9tYWluNC5sb2w%3D", "rdap_domains_four_truncated.json"); verifyMetrics( SearchType.BY_NAMESERVER_ADDRESS, @@ -1837,6 +2046,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { "4A-LOL", "49-LOL", "48-LOL", + "nsIp=5.5.5.1&cursor=ZG9tYWluNC5sb2w%3D", "rdap_domains_four_truncated.json"); verifyMetrics( SearchType.BY_NAMESERVER_ADDRESS, @@ -1844,4 +2054,21 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase { Optional.of(1L), IncompletenessWarningType.TRUNCATED); } + + @Test + public void testAddressMatch_cursorNavigation() throws Exception { + createManyDomainsAndHosts(7, 1, 2); + checkCursorNavigation( + RequestType.NS_IP, + "5.5.5.1", + ImmutableList.of( + "domain1.lol", + "domain2.lol", + "domain3.lol", + "domain4.lol", + "domain5.lol", + "domain6.lol", + "domain7.lol", + "domain8.lol")); + } } diff --git a/javatests/google/registry/rdap/RdapEntitySearchActionTest.java b/javatests/google/registry/rdap/RdapEntitySearchActionTest.java index c0386d86b..6f486eeb2 100644 --- a/javatests/google/registry/rdap/RdapEntitySearchActionTest.java +++ b/javatests/google/registry/rdap/RdapEntitySearchActionTest.java @@ -419,35 +419,49 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase { * Checks multi-page result set navigation using the cursor. * *

If there are more results than the max result set size, the RDAP code returns a cursor token - * which can be used in a subsequent call to get the next chunk of results. This method starts by - * making the query without a cursor, then follows the chain of pages using each returned cursor - * to ask for the next one, and makes sure that the expected number of pages are fetched. + * which can be used in a subsequent call to get the next chunk of results. * * @param queryType type of query being run - * @param queryString the full name or handle query string - * @param expectedPageCount how many pages we expect to retrieve; all but the last will have a - * cursor + * @param paramValue the query string + * @param expectedNames an immutable list of the entity names we expect to retrieve */ - private void checkCursorNavigation(QueryType queryType, String queryString, int expectedPageCount) + private void checkCursorNavigation( + QueryType queryType, String paramValue, ImmutableList expectedNames) throws Exception { String cursor = null; - for (int i = 0; i < expectedPageCount; i++) { + int expectedNameOffset = 0; + int expectedPageCount = + (expectedNames.size() + action.rdapResultSetMaxSize - 1) / action.rdapResultSetMaxSize; + for (int pageNum = 0; pageNum < expectedPageCount; pageNum++) { Object results = (queryType == QueryType.FULL_NAME) - ? generateActualJsonWithFullName(queryString, cursor) - : generateActualJsonWithHandle(queryString, cursor); + ? generateActualJsonWithFullName(paramValue, cursor) + : generateActualJsonWithHandle(paramValue, cursor); assertThat(response.getStatus()).isEqualTo(200); String linkToNext = RdapTestHelper.getLinkToNext(results); - if (i == expectedPageCount - 1) { + if (pageNum == expectedPageCount - 1) { assertThat(linkToNext).isNull(); } else { assertThat(linkToNext).isNotNull(); int pos = linkToNext.indexOf("cursor="); assertThat(pos).isAtLeast(0); cursor = URLDecoder.decode(linkToNext.substring(pos + 7), "UTF-8"); - Object nameserverSearchResults = ((JSONObject) results).get("entitySearchResults"); - assertThat(nameserverSearchResults).isInstanceOf(JSONArray.class); - assertThat(((JSONArray) nameserverSearchResults)).hasSize(action.rdapResultSetMaxSize); + Object searchResults = ((JSONObject) results).get("entitySearchResults"); + assertThat(searchResults).isInstanceOf(JSONArray.class); + assertThat(((JSONArray) searchResults)).hasSize(action.rdapResultSetMaxSize); + for (Object item : ((JSONArray) searchResults)) { + assertThat(item).isInstanceOf(JSONObject.class); + Object vcardArray = ((JSONObject) item).get("vcardArray"); + assertThat(vcardArray).isInstanceOf(JSONArray.class); + Object vcardData = ((JSONArray) vcardArray).get(1); + assertThat(vcardData).isInstanceOf(JSONArray.class); + Object vcardFn = ((JSONArray) vcardData).get(1); + assertThat(vcardFn).isInstanceOf(JSONArray.class); + Object name = ((JSONArray) vcardFn).get(3); + assertThat(name).isNotNull(); + assertThat(name).isInstanceOf(String.class); + assertThat(name).isEqualTo(expectedNames.get(expectedNameOffset++)); + } response = new FakeResponse(); action.response = response; } @@ -696,7 +710,19 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase { public void testNameMatchContacts_cursorNavigation() throws Exception { login("2-RegistrarTest"); createManyContactsAndRegistrars(9, 0, registrarTest); - checkCursorNavigation(QueryType.FULL_NAME, "Entity *", 3); + checkCursorNavigation( + QueryType.FULL_NAME, + "Entity *", + ImmutableList.of( + "Entity 1", + "Entity 2", + "Entity 3", + "Entity 4", + "Entity 5", + "Entity 6", + "Entity 7", + "Entity 8", + "Entity 9")); } @Test @@ -736,7 +762,23 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase { @Test public void testNameMatchRegistrars_cursorNavigation() throws Exception { createManyContactsAndRegistrars(0, 13, registrarTest); - checkCursorNavigation(QueryType.FULL_NAME, "Entity *", 4); + checkCursorNavigation( + QueryType.FULL_NAME, + "Entity *", + ImmutableList.of( + "Entity 1", + "Entity 10", + "Entity 11", + "Entity 12", + "Entity 13", + "Entity 2", + "Entity 3", + "Entity 4", + "Entity 5", + "Entity 6", + "Entity 7", + "Entity 8", + "Entity 9")); } @Test @@ -756,7 +798,16 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase { public void testNameMatchMix_cursorNavigation() throws Exception { login("2-RegistrarTest"); createManyContactsAndRegistrars(3, 3, registrarTest); - checkCursorNavigation(QueryType.FULL_NAME, "Entity *", 2); + checkCursorNavigation( + QueryType.FULL_NAME, + "Entity *", + ImmutableList.of( + "Entity 1", + "Entity 2", + "Entity 3", + "Entity 4", + "Entity 5", + "Entity 6")); } @Test @@ -1002,14 +1053,49 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase { @Test public void testHandleMatchContact_cursorNavigationWithFullLastPage() throws Exception { + login("2-RegistrarTest"); createManyContactsAndRegistrars(12, 0, registrarTest); - checkCursorNavigation(QueryType.HANDLE, "00*", 3); + checkCursorNavigation( + QueryType.HANDLE, + "00*", + // Contacts are returned in ROID order, not name order, by handle searches. + ImmutableList.of( + "Entity 1", + "Entity 2", + "Entity 3", + "Entity 4", + "Entity 5", + "Entity 6", + "Entity 7", + "Entity 8", + "Entity 9", + "Entity 10", + "Entity 11", + "Entity 12")); } @Test public void testHandleMatchContact_cursorNavigationWithPartialLastPage() throws Exception { + login("2-RegistrarTest"); createManyContactsAndRegistrars(13, 0, registrarTest); - checkCursorNavigation(QueryType.HANDLE, "00*", 4); + checkCursorNavigation( + QueryType.HANDLE, + "00*", + // Contacts are returned in ROID order, not name order, by handle searches. + ImmutableList.of( + "Entity 1", + "Entity 2", + "Entity 3", + "Entity 4", + "Entity 5", + "Entity 6", + "Entity 7", + "Entity 8", + "Entity 9", + "Entity 10", + "Entity 11", + "Entity 12", + "Entity 13")); } @Test diff --git a/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java b/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java index eb837a6b8..fb8f6ecc0 100644 --- a/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java +++ b/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java @@ -272,7 +272,7 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase { ImmutableList.Builder hostsBuilder = new ImmutableList.Builder<>(); ImmutableSet.Builder subordinateHostsBuilder = new ImmutableSet.Builder<>(); for (int i = 1; i <= numHosts; i++) { - String hostName = String.format("ns%d.cat.lol", i); + String hostName = String.format("nsx%d.cat.lol", i); subordinateHostsBuilder.add(hostName); hostsBuilder.add(makeHostResource(hostName, "5.5.5.1", "5.5.5.2")); } @@ -575,7 +575,7 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase { @Test public void testNameMatch_nontruncatedResultSet() throws Exception { createManyHosts(4); - assertThat(generateActualJsonWithName("ns*.cat.lol")) + assertThat(generateActualJsonWithName("nsx*.cat.lol")) .isEqualTo(generateExpectedJson("rdap_nontruncated_hosts.json")); assertThat(response.getStatus()).isEqualTo(200); verifyMetrics(4); @@ -584,10 +584,10 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase { @Test public void testNameMatch_truncatedResultSet() throws Exception { createManyHosts(5); - assertThat(generateActualJsonWithName("ns*.cat.lol")) + assertThat(generateActualJsonWithName("nsx*.cat.lol")) .isEqualTo( generateExpectedJson( - "name=ns*.cat.lol&cursor=bnM0LmNhdC5sb2w%3D", "rdap_truncated_hosts.json")); + "name=nsx*.cat.lol&cursor=bnN4NC5jYXQubG9s", "rdap_truncated_hosts.json")); assertThat(response.getStatus()).isEqualTo(200); verifyMetrics(5); } @@ -595,10 +595,10 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase { @Test public void testNameMatch_reallyTruncatedResultSet() throws Exception { createManyHosts(9); - assertThat(generateActualJsonWithName("ns*.cat.lol")) + assertThat(generateActualJsonWithName("nsx*.cat.lol")) .isEqualTo( generateExpectedJson( - "name=ns*.cat.lol&cursor=bnM0LmNhdC5sb2w%3D", "rdap_truncated_hosts.json")); + "name=nsx*.cat.lol&cursor=bnN4NC5jYXQubG9s", "rdap_truncated_hosts.json")); assertThat(response.getStatus()).isEqualTo(200); // When searching names, we look for additional matches, in case some are not visible. verifyMetrics(9); @@ -716,30 +716,40 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase { * which can be used in a subsequent call to get the next chunk of results. * * @param byName true if we are searching by name; false if we are searching by address - * @param queryString the name or address query string - * @param expectedPageCount how many pages we expect to retrieve; all but the last will have a - * cursor + * @param paramValue the query string + * @param expectedNames an immutable list of the host names we expect to retrieve */ - private void checkCursorNavigation(boolean byName, String queryString, int expectedPageCount) + private void checkCursorNavigation( + boolean byName, String paramValue, ImmutableList expectedNames) throws Exception { String cursor = null; - for (int i = 0; i < expectedPageCount; i++) { + int expectedNameOffset = 0; + int expectedPageCount = + (expectedNames.size() + action.rdapResultSetMaxSize - 1) / action.rdapResultSetMaxSize; + for (int pageNum = 0; pageNum < expectedPageCount; pageNum++) { Object results = byName - ? generateActualJsonWithName(queryString, cursor) - : generateActualJsonWithIp(queryString, cursor); + ? generateActualJsonWithName(paramValue, cursor) + : generateActualJsonWithIp(paramValue, cursor); assertThat(response.getStatus()).isEqualTo(200); String linkToNext = RdapTestHelper.getLinkToNext(results); - if (i == expectedPageCount - 1) { + if (pageNum == expectedPageCount - 1) { assertThat(linkToNext).isNull(); } else { assertThat(linkToNext).isNotNull(); int pos = linkToNext.indexOf("cursor="); assertThat(pos).isAtLeast(0); cursor = URLDecoder.decode(linkToNext.substring(pos + 7), "UTF-8"); - Object nameserverSearchResults = ((JSONObject) results).get("nameserverSearchResults"); - assertThat(nameserverSearchResults).isInstanceOf(JSONArray.class); - assertThat(((JSONArray) nameserverSearchResults)).hasSize(action.rdapResultSetMaxSize); + Object searchResults = ((JSONObject) results).get("nameserverSearchResults"); + assertThat(searchResults).isInstanceOf(JSONArray.class); + assertThat(((JSONArray) searchResults)).hasSize(action.rdapResultSetMaxSize); + for (Object item : ((JSONArray) searchResults)) { + assertThat(item).isInstanceOf(JSONObject.class); + Object name = ((JSONObject) item).get("ldhName"); + assertThat(name).isNotNull(); + assertThat(name).isInstanceOf(String.class); + assertThat(name).isEqualTo(expectedNames.get(expectedNameOffset++)); + } response = new FakeResponse(); action.response = response; } @@ -749,13 +759,43 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase { @Test public void testNameMatch_cursorNavigationWithSuperordinateDomain() throws Exception { createManyHosts(9); - checkCursorNavigation(true, "ns*.cat.lol", 3); + checkCursorNavigation( + true, + "ns*.cat.lol", + ImmutableList.of( + "nsx1.cat.lol", + "nsx2.cat.lol", + "nsx3.cat.lol", + "nsx4.cat.lol", + "nsx5.cat.lol", + "nsx6.cat.lol", + "nsx7.cat.lol", + "nsx8.cat.lol", + "nsx9.cat.lol")); } @Test public void testNameMatch_cursorNavigationWithPrefix() throws Exception { createManyHosts(9); - checkCursorNavigation(true, "ns*", 4); + checkCursorNavigation( + true, + "ns*", + ImmutableList.of( + "ns1.cat.1.test", + "ns1.cat.external", + "ns1.cat.lol", + "ns1.cat.xn--q9jyb4c", + "ns1.cat2.lol", + "ns2.cat.lol", + "nsx1.cat.lol", + "nsx2.cat.lol", + "nsx3.cat.lol", + "nsx4.cat.lol", + "nsx5.cat.lol", + "nsx6.cat.lol", + "nsx7.cat.lol", + "nsx8.cat.lol", + "nsx9.cat.lol")); } @Test @@ -921,6 +961,18 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase { @Test public void testAddressMatch_cursorNavigation() throws Exception { createManyHosts(9); - checkCursorNavigation(false, "5.5.5.1", 3); + checkCursorNavigation( + false, + "5.5.5.1", + ImmutableList.of( + "nsx1.cat.lol", + "nsx2.cat.lol", + "nsx3.cat.lol", + "nsx4.cat.lol", + "nsx5.cat.lol", + "nsx6.cat.lol", + "nsx7.cat.lol", + "nsx8.cat.lol", + "nsx9.cat.lol")); } } diff --git a/javatests/google/registry/rdap/testdata/rdap_domains_four_truncated.json b/javatests/google/registry/rdap/testdata/rdap_domains_four_truncated.json index e689f7faf..c8207faf6 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domains_four_truncated.json +++ b/javatests/google/registry/rdap/testdata/rdap_domains_four_truncated.json @@ -126,6 +126,18 @@ "Search results per query are limited." ] }, + { + "title" : "Navigation Links", + "description" : [ "Links to related pages." ], + "links" : + [ + { + "type" : "application/rdap+json", + "rel" : "next", + "href" : "https://example.com/rdap/domains?%NEXT_QUERY%" + } + ] + }, { "title" : "RDAP Terms of Service", "description" : diff --git a/javatests/google/registry/rdap/testdata/rdap_domains_four_with_one_unicode_truncated.json b/javatests/google/registry/rdap/testdata/rdap_domains_four_with_one_unicode_truncated.json index 3a462a6db..2f2c8a193 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domains_four_with_one_unicode_truncated.json +++ b/javatests/google/registry/rdap/testdata/rdap_domains_four_with_one_unicode_truncated.json @@ -127,6 +127,18 @@ "Search results per query are limited." ] }, + { + "title" : "Navigation Links", + "description" : [ "Links to related pages." ], + "links" : + [ + { + "type" : "application/rdap+json", + "rel" : "next", + "href" : "https://example.com/rdap/domains?%NEXT_QUERY%" + } + ] + }, { "title" : "RDAP Terms of Service", "description" : diff --git a/javatests/google/registry/rdap/testdata/rdap_nontruncated_hosts.json b/javatests/google/registry/rdap/testdata/rdap_nontruncated_hosts.json index ded648b0b..8665ff5f7 100644 --- a/javatests/google/registry/rdap/testdata/rdap_nontruncated_hosts.json +++ b/javatests/google/registry/rdap/testdata/rdap_nontruncated_hosts.json @@ -5,14 +5,14 @@ "objectClassName" : "nameserver", "handle" : "14-ROID", "status" : ["active"], - "ldhName" : "ns1.cat.lol", + "ldhName" : "nsx1.cat.lol", "links" : [ { - "value" : "https://example.tld/rdap/nameserver/ns1.cat.lol", + "value" : "https://example.tld/rdap/nameserver/nsx1.cat.lol", "rel" : "self", "type" : "application/rdap+json", - "href" : "https://example.tld/rdap/nameserver/ns1.cat.lol" + "href" : "https://example.tld/rdap/nameserver/nsx1.cat.lol" } ], "ipAddresses" : @@ -33,14 +33,14 @@ "objectClassName" : "nameserver", "handle" : "15-ROID", "status" : ["active"], - "ldhName" : "ns2.cat.lol", + "ldhName" : "nsx2.cat.lol", "links" : [ { - "value" : "https://example.tld/rdap/nameserver/ns2.cat.lol", + "value" : "https://example.tld/rdap/nameserver/nsx2.cat.lol", "rel" : "self", "type" : "application/rdap+json", - "href" : "https://example.tld/rdap/nameserver/ns2.cat.lol" + "href" : "https://example.tld/rdap/nameserver/nsx2.cat.lol" } ], "ipAddresses" : @@ -61,14 +61,14 @@ "objectClassName" : "nameserver", "handle" : "16-ROID", "status" : ["active"], - "ldhName" : "ns3.cat.lol", + "ldhName" : "nsx3.cat.lol", "links" : [ { - "value" : "https://example.tld/rdap/nameserver/ns3.cat.lol", + "value" : "https://example.tld/rdap/nameserver/nsx3.cat.lol", "rel" : "self", "type" : "application/rdap+json", - "href" : "https://example.tld/rdap/nameserver/ns3.cat.lol" + "href" : "https://example.tld/rdap/nameserver/nsx3.cat.lol" } ], "ipAddresses" : @@ -89,14 +89,14 @@ "objectClassName" : "nameserver", "handle" : "17-ROID", "status" : ["active"], - "ldhName" : "ns4.cat.lol", + "ldhName" : "nsx4.cat.lol", "links" : [ { - "value" : "https://example.tld/rdap/nameserver/ns4.cat.lol", + "value" : "https://example.tld/rdap/nameserver/nsx4.cat.lol", "rel" : "self", "type" : "application/rdap+json", - "href" : "https://example.tld/rdap/nameserver/ns4.cat.lol" + "href" : "https://example.tld/rdap/nameserver/nsx4.cat.lol" } ], "ipAddresses" : diff --git a/javatests/google/registry/rdap/testdata/rdap_truncated_hosts.json b/javatests/google/registry/rdap/testdata/rdap_truncated_hosts.json index 65d9ebb42..cb355ad09 100644 --- a/javatests/google/registry/rdap/testdata/rdap_truncated_hosts.json +++ b/javatests/google/registry/rdap/testdata/rdap_truncated_hosts.json @@ -5,14 +5,14 @@ "objectClassName" : "nameserver", "handle" : "14-ROID", "status" : ["active"], - "ldhName" : "ns1.cat.lol", + "ldhName" : "nsx1.cat.lol", "links" : [ { - "value" : "https://example.tld/rdap/nameserver/ns1.cat.lol", + "value" : "https://example.tld/rdap/nameserver/nsx1.cat.lol", "rel" : "self", "type" : "application/rdap+json", - "href" : "https://example.tld/rdap/nameserver/ns1.cat.lol" + "href" : "https://example.tld/rdap/nameserver/nsx1.cat.lol" } ], "ipAddresses" : @@ -33,14 +33,14 @@ "objectClassName" : "nameserver", "handle" : "15-ROID", "status" : ["active"], - "ldhName" : "ns2.cat.lol", + "ldhName" : "nsx2.cat.lol", "links" : [ { - "value" : "https://example.tld/rdap/nameserver/ns2.cat.lol", + "value" : "https://example.tld/rdap/nameserver/nsx2.cat.lol", "rel" : "self", "type" : "application/rdap+json", - "href" : "https://example.tld/rdap/nameserver/ns2.cat.lol" + "href" : "https://example.tld/rdap/nameserver/nsx2.cat.lol" } ], "ipAddresses" : @@ -61,14 +61,14 @@ "objectClassName" : "nameserver", "handle" : "16-ROID", "status" : ["active"], - "ldhName" : "ns3.cat.lol", + "ldhName" : "nsx3.cat.lol", "links" : [ { - "value" : "https://example.tld/rdap/nameserver/ns3.cat.lol", + "value" : "https://example.tld/rdap/nameserver/nsx3.cat.lol", "rel" : "self", "type" : "application/rdap+json", - "href" : "https://example.tld/rdap/nameserver/ns3.cat.lol" + "href" : "https://example.tld/rdap/nameserver/nsx3.cat.lol" } ], "ipAddresses" : @@ -89,14 +89,14 @@ "objectClassName" : "nameserver", "handle" : "17-ROID", "status" : ["active"], - "ldhName" : "ns4.cat.lol", + "ldhName" : "nsx4.cat.lol", "links" : [ { - "value" : "https://example.tld/rdap/nameserver/ns4.cat.lol", + "value" : "https://example.tld/rdap/nameserver/nsx4.cat.lol", "rel" : "self", "type" : "application/rdap+json", - "href" : "https://example.tld/rdap/nameserver/ns4.cat.lol" + "href" : "https://example.tld/rdap/nameserver/nsx4.cat.lol" } ], "ipAddresses" :