Add next page navigation for RDAP domain searches

In addition, while adding the tests, I became discontented with the thoroughness of the cursor navigation tests, which checked only the number of items returned, not their proper ordering. So I updated them to be more careful, and backported the changes to the nameserver and entity search tests as well.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=179442118
This commit is contained in:
mountford 2017-12-18 10:44:14 -08:00 committed by Ben McIlwain
parent 46aa638b74
commit 42795074a8
8 changed files with 489 additions and 78 deletions

View file

@ -52,6 +52,7 @@ import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream;
import javax.inject.Inject; import javax.inject.Inject;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -120,6 +121,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
throw new BadRequestException( throw new BadRequestException(
"You must specify either name=XXXX, nsLdhName=YYYY or nsIp=ZZZZ"); "You must specify either name=XXXX, nsLdhName=YYYY or nsIp=ZZZZ");
} }
decodeCursorToken();
RdapSearchResults results; RdapSearchResults results;
if (nameParam.isPresent()) { if (nameParam.isPresent()) {
metricInformationBuilder.setSearchType(SearchType.BY_DOMAIN_NAME); metricInformationBuilder.setSearchType(SearchType.BY_DOMAIN_NAME);
@ -163,7 +165,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
rdapJsonFormatter.addTopLevelEntries( rdapJsonFormatter.addTopLevelEntries(
builder, builder,
BoilerplateType.DOMAIN, BoilerplateType.DOMAIN,
results.getIncompletenessWarnings(), getNotices(results),
ImmutableList.of(), ImmutableList.of(),
fullServletPath); fullServletPath);
return builder.build(); return builder.build();
@ -241,11 +243,14 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
.load() .load()
.type(DomainResource.class) .type(DomainResource.class)
.filter("fullyQualifiedDomainName <", partialStringQuery.getNextInitialString()) .filter("fullyQualifiedDomainName <", partialStringQuery.getNextInitialString())
.filter("fullyQualifiedDomainName >=", partialStringQuery.getInitialString()) .filter("fullyQualifiedDomainName >=", partialStringQuery.getInitialString());
.limit(querySizeLimit); if (cursorString.isPresent()) {
query = query.filter("fullyQualifiedDomainName >", cursorString.get());
}
if (partialStringQuery.getSuffix() != null) { if (partialStringQuery.getSuffix() != null) {
query = query.filter("tld", partialStringQuery.getSuffix()); 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. // Always check for visibility, because we couldn't look at the deletionTime in the query.
return makeSearchResults(getMatchingResources(query, true, now, querySizeLimit), now); return makeSearchResults(getMatchingResources(query, true, now, querySizeLimit), now);
} }
@ -261,9 +266,11 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
ofy() ofy()
.load() .load()
.type(DomainResource.class) .type(DomainResource.class)
.filter("tld", tld) .filter("tld", tld);
.order("fullyQualifiedDomainName") if (cursorString.isPresent()) {
.limit(querySizeLimit); query = query.filter("fullyQualifiedDomainName >", cursorString.get());
}
query = query.order("fullyQualifiedDomainName").limit(querySizeLimit);
return makeSearchResults(getMatchingResources(query, true, now, querySizeLimit), now); return makeSearchResults(getMatchingResources(query, true, now, querySizeLimit), now);
} }
@ -459,10 +466,19 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
.filter("nsHosts in", chunk); .filter("nsHosts in", chunk);
if (!shouldIncludeDeleted()) { if (!shouldIncludeDeleted()) {
query = query.filter("deletionTime >", now); 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) Stream<DomainResource> stream =
.filter(domain -> isAuthorized(domain, now)) Streams.stream(query).filter(domain -> isAuthorized(domain, now));
.forEach(domainSetBuilder::add); if (cursorString.isPresent()) {
stream =
stream.filter(
domain -> (domain.getFullyQualifiedDomainName().compareTo(cursorString.get()) > 0));
}
stream.forEach(domainSetBuilder::add);
} }
List<DomainResource> domains = domainSetBuilder.build().asList(); List<DomainResource> domains = domainSetBuilder.build().asList();
metricInformationBuilder.setNumHostsRetrieved(numHostKeysSearched); metricInformationBuilder.setNumHostsRetrieved(numHostKeysSearched);
@ -519,7 +535,9 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
(domains.size() > 1) ? OutputDataType.SUMMARY : OutputDataType.FULL; (domains.size() > 1) ? OutputDataType.SUMMARY : OutputDataType.FULL;
RdapAuthorization authorization = getAuthorization(); RdapAuthorization authorization = getAuthorization();
List<ImmutableMap<String, Object>> jsonList = new ArrayList<>(); List<ImmutableMap<String, Object>> jsonList = new ArrayList<>();
Optional<String> newCursor = Optional.empty();
for (DomainResource domain : domains) { for (DomainResource domain : domains) {
newCursor = Optional.of(domain.getFullyQualifiedDomainName());
jsonList.add( jsonList.add(
rdapJsonFormatter.makeRdapJsonForDomain( rdapJsonFormatter.makeRdapJsonForDomain(
domain, false, fullServletPath, rdapWhoisServer, now, outputDataType, authorization)); domain, false, fullServletPath, rdapWhoisServer, now, outputDataType, authorization));
@ -533,6 +551,10 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
: incompletenessWarningType; : incompletenessWarningType;
metricInformationBuilder.setIncompletenessWarningType(finalIncompletenessWarningType); metricInformationBuilder.setIncompletenessWarningType(finalIncompletenessWarningType);
return RdapSearchResults.create( return RdapSearchResults.create(
ImmutableList.copyOf(jsonList), finalIncompletenessWarningType, Optional.empty()); ImmutableList.copyOf(jsonList),
finalIncompletenessWarningType,
(finalIncompletenessWarningType == IncompletenessWarningType.TRUNCATED)
? newCursor
: Optional.empty());
} }
} }

View file

@ -36,6 +36,7 @@ import static org.mockito.Mockito.when;
import com.google.appengine.api.users.User; import com.google.appengine.api.users.User;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Range; 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.ui.server.registrar.SessionUtils;
import google.registry.util.Idn; import google.registry.util.Idn;
import java.net.IDN; import java.net.IDN;
import java.net.URLDecoder;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -71,6 +73,7 @@ import java.util.Optional;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject; import org.json.simple.JSONObject;
import org.json.simple.JSONValue; import org.json.simple.JSONValue;
import org.junit.Before; import org.junit.Before;
@ -93,7 +96,6 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
public final InjectRule inject = new InjectRule(); public final InjectRule inject = new InjectRule();
private final HttpServletRequest request = mock(HttpServletRequest.class); 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 FakeClock clock = new FakeClock(DateTime.parse("2000-01-01T00:00:00Z"));
private final SessionUtils sessionUtils = mock(SessionUtils.class); private final SessionUtils sessionUtils = mock(SessionUtils.class);
private final User user = new User("rdap.user@example.com", "gmail.com", "12345"); 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 UserAuthInfo adminUserAuthInfo = UserAuthInfo.create(user, true);
private final RdapDomainSearchAction action = new RdapDomainSearchAction(); private final RdapDomainSearchAction action = new RdapDomainSearchAction();
private FakeResponse response = new FakeResponse();
private Registrar registrar; private Registrar registrar;
private DomainResource domainCatLol; private DomainResource domainCatLol;
private DomainResource domainCatLol2; private DomainResource domainCatLol2;
@ -118,30 +122,49 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
enum RequestType { NONE, NAME, NS_LDH_NAME, NS_IP } enum RequestType { NONE, NAME, NS_LDH_NAME, NS_IP }
private Object generateActualJson(RequestType requestType, String paramValue) { 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; action.requestPath = RdapDomainSearchAction.PATH;
String requestTypeParam = null;
switch (requestType) { switch (requestType) {
case NAME: case NAME:
action.nameParam = Optional.of(paramValue); action.nameParam = Optional.of(paramValue);
action.nsLdhNameParam = Optional.empty(); action.nsLdhNameParam = Optional.empty();
action.nsIpParam = Optional.empty(); action.nsIpParam = Optional.empty();
requestTypeParam = "name";
break; break;
case NS_LDH_NAME: case NS_LDH_NAME:
action.nameParam = Optional.empty(); action.nameParam = Optional.empty();
action.nsLdhNameParam = Optional.of(paramValue); action.nsLdhNameParam = Optional.of(paramValue);
action.nsIpParam = Optional.empty(); action.nsIpParam = Optional.empty();
requestTypeParam = "nsLdhName";
break; break;
case NS_IP: case NS_IP:
action.nameParam = Optional.empty(); action.nameParam = Optional.empty();
action.nsLdhNameParam = Optional.empty(); action.nsLdhNameParam = Optional.empty();
action.nsIpParam = Optional.of(paramValue); action.nsIpParam = Optional.of(paramValue);
requestTypeParam = "nsIp";
break; break;
default: default:
action.nameParam = Optional.empty(); action.nameParam = Optional.empty();
action.nsLdhNameParam = Optional.empty(); action.nsLdhNameParam = Optional.empty();
action.nsIpParam = Optional.empty(); action.nsIpParam = Optional.empty();
requestTypeParam = "";
break; 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(); action.run();
return JSONValue.parse(response.getPayload()); return JSONValue.parse(response.getPayload());
} }
@ -371,6 +394,8 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
action.request = request; action.request = request;
action.requestMethod = Action.Method.GET; action.requestMethod = Action.Method.GET;
action.fullServletPath = "https://example.com/rdap"; action.fullServletPath = "https://example.com/rdap";
action.requestUrl = "https://example.com/rdap/domains";
action.parameterMap = ImmutableListMultimap.of();
action.requestMethod = POST; action.requestMethod = POST;
action.response = response; action.response = response;
action.registrarParam = Optional.empty(); action.registrarParam = Optional.empty();
@ -381,6 +406,8 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
action.sessionUtils = sessionUtils; action.sessionUtils = sessionUtils;
action.authResult = AuthResult.create(AuthLevel.USER, userAuthInfo); action.authResult = AuthResult.create(AuthLevel.USER, userAuthInfo);
action.rdapMetrics = rdapMetrics; action.rdapMetrics = rdapMetrics;
action.cursorTokenParam = Optional.empty();
action.rdapResultSetMaxSize = 4;
} }
private void login(String clientId) { private void login(String clientId) {
@ -426,6 +453,30 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
String domain4Name, String domain4Name,
String domain4Handle, String domain4Handle,
String expectedOutputFile) { 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( return JSONValue.parse(
loadFile( loadFile(
this.getClass(), this.getClass(),
@ -444,6 +495,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
.put("DOMAINPUNYCODENAME4", domain4Name) .put("DOMAINPUNYCODENAME4", domain4Name)
.put("DOMAINNAME4", IDN.toUnicode(domain4Name)) .put("DOMAINNAME4", IDN.toUnicode(domain4Name))
.put("DOMAINHANDLE4", domain4Handle) .put("DOMAINHANDLE4", domain4Handle)
.put("NEXT_QUERY", nextQuery)
.build())); .build()));
} }
@ -567,6 +619,30 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
String domainHandle3, String domainHandle3,
String domainName4, String domainName4,
String domainHandle4) { 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( return JSONValue.parse(loadFile(
this.getClass(), this.getClass(),
fileName, fileName,
@ -579,6 +655,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
.put("DOMAINHANDLE3", domainHandle3) .put("DOMAINHANDLE3", domainHandle3)
.put("DOMAINNAME4", domainName4) .put("DOMAINNAME4", domainName4)
.put("DOMAINHANDLE4", domainHandle4) .put("DOMAINHANDLE4", domainHandle4)
.put("NEXT_QUERY", nextQuery)
.build())); .build()));
} }
@ -645,6 +722,26 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
String domainRoid3, String domainRoid3,
String domainRoid4, String domainRoid4,
String fileName) { 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); rememberWildcardType(queryString);
assertThat(generateActualJson(requestType, queryString)) assertThat(generateActualJson(requestType, queryString))
.isEqualTo( .isEqualTo(
@ -657,7 +754,8 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
"domain3.lol", "domain3.lol",
domainRoid3, domainRoid3,
"domain4.lol", "domain4.lol",
domainRoid4)); domainRoid4,
nextQuery));
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
} }
@ -731,6 +829,50 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
verifyMetrics(searchType, numDomainsRetrieved, numHostsRetrieved); verifyMetrics(searchType, numDomainsRetrieved, numHostsRetrieved);
} }
/**
* Checks multi-page result set navigation using the cursor.
*
* <p>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<String> 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 @Test
public void testInvalidPath_rejected() throws Exception { public void testInvalidPath_rejected() throws Exception {
action.requestPath = RdapDomainSearchAction.PATH + "/path"; action.requestPath = RdapDomainSearchAction.PATH + "/path";
@ -1032,6 +1174,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
"cat.example", "21-EXAMPLE", "cat.example", "21-EXAMPLE",
"cat.lol", "C-LOL", "cat.lol", "C-LOL",
"cat.xn--q9jyb4c", "2D-Q9JYB4C", "cat.xn--q9jyb4c", "2D-Q9JYB4C",
"name=cat*&cursor=Y2F0LnhuLS1xOWp5YjRj",
"rdap_domains_four_with_one_unicode_truncated.json")); "rdap_domains_four_with_one_unicode_truncated.json"));
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED); verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED);
@ -1192,6 +1335,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
"46-LOL", "46-LOL",
"45-LOL", "45-LOL",
"44-LOL", "44-LOL",
"name=domain*.lol&cursor=ZG9tYWluNC5sb2w%3D",
"rdap_domains_four_truncated.json"); "rdap_domains_four_truncated.json");
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED); verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED);
} }
@ -1210,7 +1354,8 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
"domain1.lol", "domain1.lol",
"46-LOL", "46-LOL",
"domain2.lol", "domain2.lol",
"45-LOL")); "45-LOL",
"name=*.lol&cursor=ZG9tYWluMi5sb2w%3D"));
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED); verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED);
} }
@ -1226,6 +1371,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
"4A-LOL", "4A-LOL",
"49-LOL", "49-LOL",
"48-LOL", "48-LOL",
"name=domain*.lol&cursor=ZG9tYWluNC5sb2w%3D",
"rdap_domains_four_truncated.json"); "rdap_domains_four_truncated.json");
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED); verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(5L), IncompletenessWarningType.TRUNCATED);
} }
@ -1244,11 +1390,54 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
"domain24.lol", "domain24.lol",
"49-LOL", "49-LOL",
"domain30.lol", "domain30.lol",
"43-LOL")); "43-LOL",
"name=domain*.lol&cursor=ZG9tYWluMzAubG9s"));
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(SearchType.BY_DOMAIN_NAME, Optional.of(27L), IncompletenessWarningType.TRUNCATED); 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 @Test
public void testNameserverMatch_foundMultiple() throws Exception { public void testNameserverMatch_foundMultiple() throws Exception {
rememberWildcardType("ns1.cat.lol"); rememberWildcardType("ns1.cat.lol");
@ -1595,6 +1784,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
"46-LOL", "46-LOL",
"45-LOL", "45-LOL",
"44-LOL", "44-LOL",
"nsLdhName=ns1.domain1.lol&cursor=ZG9tYWluNC5sb2w%3D",
"rdap_domains_four_truncated.json"); "rdap_domains_four_truncated.json");
verifyMetrics( verifyMetrics(
SearchType.BY_NAMESERVER_NAME, SearchType.BY_NAMESERVER_NAME,
@ -1613,6 +1803,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
"4A-LOL", "4A-LOL",
"49-LOL", "49-LOL",
"48-LOL", "48-LOL",
"nsLdhName=ns1.domain1.lol&cursor=ZG9tYWluNC5sb2w%3D",
"rdap_domains_four_truncated.json"); "rdap_domains_four_truncated.json");
verifyMetrics( verifyMetrics(
SearchType.BY_NAMESERVER_NAME, SearchType.BY_NAMESERVER_NAME,
@ -1666,6 +1857,23 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
IncompletenessWarningType.MIGHT_BE_INCOMPLETE); 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 @Test
public void testAddressMatchV4Address_invalidAddress() throws Exception { public void testAddressMatchV4Address_invalidAddress() throws Exception {
rememberWildcardType("1.2.3.4.5.6.7.8.9"); rememberWildcardType("1.2.3.4.5.6.7.8.9");
@ -1819,6 +2027,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
"46-LOL", "46-LOL",
"45-LOL", "45-LOL",
"44-LOL", "44-LOL",
"nsIp=5.5.5.1&cursor=ZG9tYWluNC5sb2w%3D",
"rdap_domains_four_truncated.json"); "rdap_domains_four_truncated.json");
verifyMetrics( verifyMetrics(
SearchType.BY_NAMESERVER_ADDRESS, SearchType.BY_NAMESERVER_ADDRESS,
@ -1837,6 +2046,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
"4A-LOL", "4A-LOL",
"49-LOL", "49-LOL",
"48-LOL", "48-LOL",
"nsIp=5.5.5.1&cursor=ZG9tYWluNC5sb2w%3D",
"rdap_domains_four_truncated.json"); "rdap_domains_four_truncated.json");
verifyMetrics( verifyMetrics(
SearchType.BY_NAMESERVER_ADDRESS, SearchType.BY_NAMESERVER_ADDRESS,
@ -1844,4 +2054,21 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase {
Optional.of(1L), Optional.of(1L),
IncompletenessWarningType.TRUNCATED); 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"));
}
} }

View file

@ -419,35 +419,49 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
* Checks multi-page result set navigation using the cursor. * Checks multi-page result set navigation using the cursor.
* *
* <p>If there are more results than the max result set size, the RDAP code returns a cursor token * <p>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 * which can be used in a subsequent call to get the next chunk of results.
* 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.
* *
* @param queryType type of query being run * @param queryType type of query being run
* @param queryString the full name or handle query string * @param paramValue the query string
* @param expectedPageCount how many pages we expect to retrieve; all but the last will have a * @param expectedNames an immutable list of the entity names we expect to retrieve
* cursor
*/ */
private void checkCursorNavigation(QueryType queryType, String queryString, int expectedPageCount) private void checkCursorNavigation(
QueryType queryType, String paramValue, ImmutableList<String> expectedNames)
throws Exception { throws Exception {
String cursor = null; 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 = Object results =
(queryType == QueryType.FULL_NAME) (queryType == QueryType.FULL_NAME)
? generateActualJsonWithFullName(queryString, cursor) ? generateActualJsonWithFullName(paramValue, cursor)
: generateActualJsonWithHandle(queryString, cursor); : generateActualJsonWithHandle(paramValue, cursor);
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
String linkToNext = RdapTestHelper.getLinkToNext(results); String linkToNext = RdapTestHelper.getLinkToNext(results);
if (i == expectedPageCount - 1) { if (pageNum == expectedPageCount - 1) {
assertThat(linkToNext).isNull(); assertThat(linkToNext).isNull();
} else { } else {
assertThat(linkToNext).isNotNull(); assertThat(linkToNext).isNotNull();
int pos = linkToNext.indexOf("cursor="); int pos = linkToNext.indexOf("cursor=");
assertThat(pos).isAtLeast(0); assertThat(pos).isAtLeast(0);
cursor = URLDecoder.decode(linkToNext.substring(pos + 7), "UTF-8"); cursor = URLDecoder.decode(linkToNext.substring(pos + 7), "UTF-8");
Object nameserverSearchResults = ((JSONObject) results).get("entitySearchResults"); Object searchResults = ((JSONObject) results).get("entitySearchResults");
assertThat(nameserverSearchResults).isInstanceOf(JSONArray.class); assertThat(searchResults).isInstanceOf(JSONArray.class);
assertThat(((JSONArray) nameserverSearchResults)).hasSize(action.rdapResultSetMaxSize); 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(); response = new FakeResponse();
action.response = response; action.response = response;
} }
@ -696,7 +710,19 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
public void testNameMatchContacts_cursorNavigation() throws Exception { public void testNameMatchContacts_cursorNavigation() throws Exception {
login("2-RegistrarTest"); login("2-RegistrarTest");
createManyContactsAndRegistrars(9, 0, 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 @Test
@ -736,7 +762,23 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
@Test @Test
public void testNameMatchRegistrars_cursorNavigation() throws Exception { public void testNameMatchRegistrars_cursorNavigation() throws Exception {
createManyContactsAndRegistrars(0, 13, registrarTest); 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 @Test
@ -756,7 +798,16 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
public void testNameMatchMix_cursorNavigation() throws Exception { public void testNameMatchMix_cursorNavigation() throws Exception {
login("2-RegistrarTest"); login("2-RegistrarTest");
createManyContactsAndRegistrars(3, 3, 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 @Test
@ -1002,14 +1053,49 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
@Test @Test
public void testHandleMatchContact_cursorNavigationWithFullLastPage() throws Exception { public void testHandleMatchContact_cursorNavigationWithFullLastPage() throws Exception {
login("2-RegistrarTest");
createManyContactsAndRegistrars(12, 0, 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 @Test
public void testHandleMatchContact_cursorNavigationWithPartialLastPage() throws Exception { public void testHandleMatchContact_cursorNavigationWithPartialLastPage() throws Exception {
login("2-RegistrarTest");
createManyContactsAndRegistrars(13, 0, 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 @Test

View file

@ -272,7 +272,7 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase {
ImmutableList.Builder<HostResource> hostsBuilder = new ImmutableList.Builder<>(); ImmutableList.Builder<HostResource> hostsBuilder = new ImmutableList.Builder<>();
ImmutableSet.Builder<String> subordinateHostsBuilder = new ImmutableSet.Builder<>(); ImmutableSet.Builder<String> subordinateHostsBuilder = new ImmutableSet.Builder<>();
for (int i = 1; i <= numHosts; i++) { 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); subordinateHostsBuilder.add(hostName);
hostsBuilder.add(makeHostResource(hostName, "5.5.5.1", "5.5.5.2")); hostsBuilder.add(makeHostResource(hostName, "5.5.5.1", "5.5.5.2"));
} }
@ -575,7 +575,7 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase {
@Test @Test
public void testNameMatch_nontruncatedResultSet() throws Exception { public void testNameMatch_nontruncatedResultSet() throws Exception {
createManyHosts(4); createManyHosts(4);
assertThat(generateActualJsonWithName("ns*.cat.lol")) assertThat(generateActualJsonWithName("nsx*.cat.lol"))
.isEqualTo(generateExpectedJson("rdap_nontruncated_hosts.json")); .isEqualTo(generateExpectedJson("rdap_nontruncated_hosts.json"));
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(4); verifyMetrics(4);
@ -584,10 +584,10 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase {
@Test @Test
public void testNameMatch_truncatedResultSet() throws Exception { public void testNameMatch_truncatedResultSet() throws Exception {
createManyHosts(5); createManyHosts(5);
assertThat(generateActualJsonWithName("ns*.cat.lol")) assertThat(generateActualJsonWithName("nsx*.cat.lol"))
.isEqualTo( .isEqualTo(
generateExpectedJson( 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); assertThat(response.getStatus()).isEqualTo(200);
verifyMetrics(5); verifyMetrics(5);
} }
@ -595,10 +595,10 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase {
@Test @Test
public void testNameMatch_reallyTruncatedResultSet() throws Exception { public void testNameMatch_reallyTruncatedResultSet() throws Exception {
createManyHosts(9); createManyHosts(9);
assertThat(generateActualJsonWithName("ns*.cat.lol")) assertThat(generateActualJsonWithName("nsx*.cat.lol"))
.isEqualTo( .isEqualTo(
generateExpectedJson( 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); assertThat(response.getStatus()).isEqualTo(200);
// When searching names, we look for additional matches, in case some are not visible. // When searching names, we look for additional matches, in case some are not visible.
verifyMetrics(9); 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. * 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 byName true if we are searching by name; false if we are searching by address
* @param queryString the name or address query string * @param paramValue the query string
* @param expectedPageCount how many pages we expect to retrieve; all but the last will have a * @param expectedNames an immutable list of the host names we expect to retrieve
* cursor
*/ */
private void checkCursorNavigation(boolean byName, String queryString, int expectedPageCount) private void checkCursorNavigation(
boolean byName, String paramValue, ImmutableList<String> expectedNames)
throws Exception { throws Exception {
String cursor = null; 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 = Object results =
byName byName
? generateActualJsonWithName(queryString, cursor) ? generateActualJsonWithName(paramValue, cursor)
: generateActualJsonWithIp(queryString, cursor); : generateActualJsonWithIp(paramValue, cursor);
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
String linkToNext = RdapTestHelper.getLinkToNext(results); String linkToNext = RdapTestHelper.getLinkToNext(results);
if (i == expectedPageCount - 1) { if (pageNum == expectedPageCount - 1) {
assertThat(linkToNext).isNull(); assertThat(linkToNext).isNull();
} else { } else {
assertThat(linkToNext).isNotNull(); assertThat(linkToNext).isNotNull();
int pos = linkToNext.indexOf("cursor="); int pos = linkToNext.indexOf("cursor=");
assertThat(pos).isAtLeast(0); assertThat(pos).isAtLeast(0);
cursor = URLDecoder.decode(linkToNext.substring(pos + 7), "UTF-8"); cursor = URLDecoder.decode(linkToNext.substring(pos + 7), "UTF-8");
Object nameserverSearchResults = ((JSONObject) results).get("nameserverSearchResults"); Object searchResults = ((JSONObject) results).get("nameserverSearchResults");
assertThat(nameserverSearchResults).isInstanceOf(JSONArray.class); assertThat(searchResults).isInstanceOf(JSONArray.class);
assertThat(((JSONArray) nameserverSearchResults)).hasSize(action.rdapResultSetMaxSize); 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(); response = new FakeResponse();
action.response = response; action.response = response;
} }
@ -749,13 +759,43 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase {
@Test @Test
public void testNameMatch_cursorNavigationWithSuperordinateDomain() throws Exception { public void testNameMatch_cursorNavigationWithSuperordinateDomain() throws Exception {
createManyHosts(9); 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 @Test
public void testNameMatch_cursorNavigationWithPrefix() throws Exception { public void testNameMatch_cursorNavigationWithPrefix() throws Exception {
createManyHosts(9); 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 @Test
@ -921,6 +961,18 @@ public class RdapNameserverSearchActionTest extends RdapSearchActionTestCase {
@Test @Test
public void testAddressMatch_cursorNavigation() throws Exception { public void testAddressMatch_cursorNavigation() throws Exception {
createManyHosts(9); 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"));
} }
} }

View file

@ -126,6 +126,18 @@
"Search results per query are limited." "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", "title" : "RDAP Terms of Service",
"description" : "description" :

View file

@ -127,6 +127,18 @@
"Search results per query are limited." "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", "title" : "RDAP Terms of Service",
"description" : "description" :

View file

@ -5,14 +5,14 @@
"objectClassName" : "nameserver", "objectClassName" : "nameserver",
"handle" : "14-ROID", "handle" : "14-ROID",
"status" : ["active"], "status" : ["active"],
"ldhName" : "ns1.cat.lol", "ldhName" : "nsx1.cat.lol",
"links" : "links" :
[ [
{ {
"value" : "https://example.tld/rdap/nameserver/ns1.cat.lol", "value" : "https://example.tld/rdap/nameserver/nsx1.cat.lol",
"rel" : "self", "rel" : "self",
"type" : "application/rdap+json", "type" : "application/rdap+json",
"href" : "https://example.tld/rdap/nameserver/ns1.cat.lol" "href" : "https://example.tld/rdap/nameserver/nsx1.cat.lol"
} }
], ],
"ipAddresses" : "ipAddresses" :
@ -33,14 +33,14 @@
"objectClassName" : "nameserver", "objectClassName" : "nameserver",
"handle" : "15-ROID", "handle" : "15-ROID",
"status" : ["active"], "status" : ["active"],
"ldhName" : "ns2.cat.lol", "ldhName" : "nsx2.cat.lol",
"links" : "links" :
[ [
{ {
"value" : "https://example.tld/rdap/nameserver/ns2.cat.lol", "value" : "https://example.tld/rdap/nameserver/nsx2.cat.lol",
"rel" : "self", "rel" : "self",
"type" : "application/rdap+json", "type" : "application/rdap+json",
"href" : "https://example.tld/rdap/nameserver/ns2.cat.lol" "href" : "https://example.tld/rdap/nameserver/nsx2.cat.lol"
} }
], ],
"ipAddresses" : "ipAddresses" :
@ -61,14 +61,14 @@
"objectClassName" : "nameserver", "objectClassName" : "nameserver",
"handle" : "16-ROID", "handle" : "16-ROID",
"status" : ["active"], "status" : ["active"],
"ldhName" : "ns3.cat.lol", "ldhName" : "nsx3.cat.lol",
"links" : "links" :
[ [
{ {
"value" : "https://example.tld/rdap/nameserver/ns3.cat.lol", "value" : "https://example.tld/rdap/nameserver/nsx3.cat.lol",
"rel" : "self", "rel" : "self",
"type" : "application/rdap+json", "type" : "application/rdap+json",
"href" : "https://example.tld/rdap/nameserver/ns3.cat.lol" "href" : "https://example.tld/rdap/nameserver/nsx3.cat.lol"
} }
], ],
"ipAddresses" : "ipAddresses" :
@ -89,14 +89,14 @@
"objectClassName" : "nameserver", "objectClassName" : "nameserver",
"handle" : "17-ROID", "handle" : "17-ROID",
"status" : ["active"], "status" : ["active"],
"ldhName" : "ns4.cat.lol", "ldhName" : "nsx4.cat.lol",
"links" : "links" :
[ [
{ {
"value" : "https://example.tld/rdap/nameserver/ns4.cat.lol", "value" : "https://example.tld/rdap/nameserver/nsx4.cat.lol",
"rel" : "self", "rel" : "self",
"type" : "application/rdap+json", "type" : "application/rdap+json",
"href" : "https://example.tld/rdap/nameserver/ns4.cat.lol" "href" : "https://example.tld/rdap/nameserver/nsx4.cat.lol"
} }
], ],
"ipAddresses" : "ipAddresses" :

View file

@ -5,14 +5,14 @@
"objectClassName" : "nameserver", "objectClassName" : "nameserver",
"handle" : "14-ROID", "handle" : "14-ROID",
"status" : ["active"], "status" : ["active"],
"ldhName" : "ns1.cat.lol", "ldhName" : "nsx1.cat.lol",
"links" : "links" :
[ [
{ {
"value" : "https://example.tld/rdap/nameserver/ns1.cat.lol", "value" : "https://example.tld/rdap/nameserver/nsx1.cat.lol",
"rel" : "self", "rel" : "self",
"type" : "application/rdap+json", "type" : "application/rdap+json",
"href" : "https://example.tld/rdap/nameserver/ns1.cat.lol" "href" : "https://example.tld/rdap/nameserver/nsx1.cat.lol"
} }
], ],
"ipAddresses" : "ipAddresses" :
@ -33,14 +33,14 @@
"objectClassName" : "nameserver", "objectClassName" : "nameserver",
"handle" : "15-ROID", "handle" : "15-ROID",
"status" : ["active"], "status" : ["active"],
"ldhName" : "ns2.cat.lol", "ldhName" : "nsx2.cat.lol",
"links" : "links" :
[ [
{ {
"value" : "https://example.tld/rdap/nameserver/ns2.cat.lol", "value" : "https://example.tld/rdap/nameserver/nsx2.cat.lol",
"rel" : "self", "rel" : "self",
"type" : "application/rdap+json", "type" : "application/rdap+json",
"href" : "https://example.tld/rdap/nameserver/ns2.cat.lol" "href" : "https://example.tld/rdap/nameserver/nsx2.cat.lol"
} }
], ],
"ipAddresses" : "ipAddresses" :
@ -61,14 +61,14 @@
"objectClassName" : "nameserver", "objectClassName" : "nameserver",
"handle" : "16-ROID", "handle" : "16-ROID",
"status" : ["active"], "status" : ["active"],
"ldhName" : "ns3.cat.lol", "ldhName" : "nsx3.cat.lol",
"links" : "links" :
[ [
{ {
"value" : "https://example.tld/rdap/nameserver/ns3.cat.lol", "value" : "https://example.tld/rdap/nameserver/nsx3.cat.lol",
"rel" : "self", "rel" : "self",
"type" : "application/rdap+json", "type" : "application/rdap+json",
"href" : "https://example.tld/rdap/nameserver/ns3.cat.lol" "href" : "https://example.tld/rdap/nameserver/nsx3.cat.lol"
} }
], ],
"ipAddresses" : "ipAddresses" :
@ -89,14 +89,14 @@
"objectClassName" : "nameserver", "objectClassName" : "nameserver",
"handle" : "17-ROID", "handle" : "17-ROID",
"status" : ["active"], "status" : ["active"],
"ldhName" : "ns4.cat.lol", "ldhName" : "nsx4.cat.lol",
"links" : "links" :
[ [
{ {
"value" : "https://example.tld/rdap/nameserver/ns4.cat.lol", "value" : "https://example.tld/rdap/nameserver/nsx4.cat.lol",
"rel" : "self", "rel" : "self",
"type" : "application/rdap+json", "type" : "application/rdap+json",
"href" : "https://example.tld/rdap/nameserver/ns4.cat.lol" "href" : "https://example.tld/rdap/nameserver/nsx4.cat.lol"
} }
], ],
"ipAddresses" : "ipAddresses" :