mirror of
https://github.com/google/nomulus
synced 2026-07-08 09:06:58 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ab612ab23 | |||
| 403c7ad275 | |||
| 11d625b837 | |||
| 6a47287da7 | |||
| cdc0ffe831 | |||
| 7c23413d83 | |||
| fe222bbdcf |
@@ -94,7 +94,7 @@ Do not wait for the user to tell you to improve the skills; it is your responsib
|
||||
|
||||
# Gemini Engineering Guide: Nomulus Codebase
|
||||
|
||||
This document captures high-level architectural patterns, lessons learned from large-scale refactorings (like the Joda-Time to `java.time` migration), and specific instructions to avoid common pitfalls in this environment.
|
||||
This document captures high-level architectural patterns, lessons learned from large-scale refactorings, and specific instructions to avoid common pitfalls in this environment.
|
||||
|
||||
## 🏛 Architecture Overview
|
||||
|
||||
|
||||
@@ -48,7 +48,11 @@ interface DomainData {
|
||||
selector: 'app-response-dialog',
|
||||
template: `
|
||||
<h2 mat-dialog-title>{{ data.title }}</h2>
|
||||
<mat-dialog-content [innerHTML]="data.content" />
|
||||
<mat-dialog-content>
|
||||
@for (line of data.content; track line) {
|
||||
<div>{{ line }}</div>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onClose()">Close</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -59,7 +63,7 @@ export class ResponseDialogComponent {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ReasonDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA)
|
||||
public data: { title: string; content: string }
|
||||
public data: { title: string; content: string[] }
|
||||
) {}
|
||||
|
||||
onClose(): void {
|
||||
@@ -312,11 +316,13 @@ export class DomainListComponent {
|
||||
this.dialog.open(ResponseDialogComponent, {
|
||||
data: {
|
||||
title: 'Domain Deletion Results',
|
||||
content: `Successfully deleted - ${successCount} domain(s)<br/>Failed to delete - ${failureCount} domain(s)<br/>${
|
||||
content: [
|
||||
`Successfully deleted - ${successCount} domain(s)`,
|
||||
`Failed to delete - ${failureCount} domain(s)`,
|
||||
failureCount
|
||||
? 'Some domains could not be deleted due to ongoing processes or server errors. '
|
||||
: ''
|
||||
}Please check the table for more information.`,
|
||||
? 'Some domains could not be deleted due to ongoing processes or server errors. Please check the table for more information.'
|
||||
: 'Please check the table for more information.',
|
||||
],
|
||||
},
|
||||
});
|
||||
this.selection.clear();
|
||||
|
||||
@@ -97,10 +97,9 @@
|
||||
@for (column of columns; track column.columnDef) {
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">{{ column.header }} </span>
|
||||
<span
|
||||
class="console-app__list-value"
|
||||
[innerHTML]="column.cell(registrarInEdit).replace('<br/>', ' ')"
|
||||
></span>
|
||||
<span class="console-app__list-value">{{
|
||||
column.cell(registrarInEdit)
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
}
|
||||
|
||||
@@ -49,10 +49,9 @@
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
{{ column.header }}
|
||||
</mat-header-cell>
|
||||
<mat-cell
|
||||
*matCellDef="let row"
|
||||
[innerHTML]="column.cell(row)"
|
||||
></mat-cell>
|
||||
<mat-cell *matCellDef="let row" style="white-space: pre-wrap">{{
|
||||
column.cell(row)
|
||||
}}</mat-cell>
|
||||
</ng-container>
|
||||
}
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
|
||||
@@ -56,7 +56,7 @@ export const columns = [
|
||||
cell: (record: Registrar) =>
|
||||
`${Object.entries(record.billingAccountMap || {}).reduce(
|
||||
(acc, [key, val]) => {
|
||||
return `${acc}${key}=${val}<br/>`;
|
||||
return `${acc}${key}=${val}\n`;
|
||||
},
|
||||
''
|
||||
)}`,
|
||||
|
||||
@@ -25,7 +25,18 @@
|
||||
@for (column of columns; track column) {
|
||||
<ng-container [matColumnDef]="column.columnDef">
|
||||
<mat-header-cell *matHeaderCellDef> {{ column.header }} </mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
@if (column.columnDef === 'name') {
|
||||
<div class="contact__name-column">
|
||||
<div class="contact__name-column-title">{{ row.name }}</div>
|
||||
<div class="contact__name-column-roles">
|
||||
{{ row.userFriendlyTypes.join(" • ") }}
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
{{ column.cell(row) }}
|
||||
}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
}
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
|
||||
@@ -34,14 +34,7 @@ export default class ContactComponent {
|
||||
{
|
||||
columnDef: 'name',
|
||||
header: 'Name',
|
||||
cell: (contact: ViewReadyContact) => `
|
||||
<div class="contact__name-column">
|
||||
<div class="contact__name-column-title">${contact.name}</div>
|
||||
<div class="contact__name-column-roles">${contact.userFriendlyTypes.join(
|
||||
' • '
|
||||
)}</div>
|
||||
</div>
|
||||
`,
|
||||
cell: (contact: ViewReadyContact) => `${contact.name}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'emailAddress',
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
{{ column.header }}
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
|
||||
<mat-cell *matCellDef="let row">{{ column.cell(row) }}</mat-cell>
|
||||
</ng-container>
|
||||
}
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
|
||||
@@ -173,6 +173,18 @@ public final class RegistryConfig {
|
||||
return config.registrarConsole.supportEmailAddress;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("consoleHistoryQueryLimit")
|
||||
public static int provideConsoleHistoryQueryLimit(RegistryConfigSettings config) {
|
||||
return config.registrarConsole.historyQueryLimit;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("consoleBulkDomainActionLimit")
|
||||
public static int provideConsoleBulkDomainActionLimit(RegistryConfigSettings config) {
|
||||
return config.registrarConsole.bulkDomainActionLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* The DUM file name, used as a file name base for DUM csv file
|
||||
*
|
||||
|
||||
@@ -181,6 +181,8 @@ public class RegistryConfigSettings {
|
||||
public String supportPhoneNumber;
|
||||
public String supportEmailAddress;
|
||||
public String technicalDocsUrl;
|
||||
public int historyQueryLimit;
|
||||
public int bulkDomainActionLimit;
|
||||
}
|
||||
|
||||
/** Configuration for monitoring. */
|
||||
|
||||
@@ -380,6 +380,12 @@ registrarConsole:
|
||||
# URL linking to directory of technical support docs on the registry.
|
||||
technicalDocsUrl: http://example.com/your_support_docs/
|
||||
|
||||
# Maximum number of history records returned in a single query.
|
||||
historyQueryLimit: 500
|
||||
|
||||
# Maximum number of domains allowed in a single bulk action.
|
||||
bulkDomainActionLimit: 500
|
||||
|
||||
monitoring:
|
||||
# Max queries per second for the Google Cloud Monitoring V3 (aka Stackdriver)
|
||||
# API. The limit can be adjusted by contacting Cloud Support.
|
||||
|
||||
@@ -283,7 +283,7 @@ public final class DomainPricingLogic {
|
||||
return tld.getStandardRenewCost(dateTime).multipliedBy(years);
|
||||
}
|
||||
if (token.getRenewalPriceBehavior().equals(RenewalPriceBehavior.SPECIFIED)) {
|
||||
return token.getRenewalPrice().get();
|
||||
return token.getRenewalPrice().get().multipliedBy(years);
|
||||
}
|
||||
}
|
||||
return getDomainCostWithDiscount(
|
||||
@@ -328,12 +328,13 @@ public final class DomainPricingLogic {
|
||||
// Apply the allocation token discount, if applicable.
|
||||
if (token.getDiscountPrice().isPresent()
|
||||
&& tld.getCurrency().equals(token.getDiscountPrice().get().getCurrencyUnit())) {
|
||||
int nonDiscountedYears = Math.max(0, years - token.getDiscountYears());
|
||||
int discountedYears = Math.min(years, token.getDiscountYears());
|
||||
int nonDiscountedYears = years - discountedYears;
|
||||
totalDomainFlowCost =
|
||||
token
|
||||
.getDiscountPrice()
|
||||
.get()
|
||||
.multipliedBy(token.getDiscountYears())
|
||||
.multipliedBy(discountedYears)
|
||||
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(nonDiscountedYears));
|
||||
} else if (token.getDiscountFraction() > 0) {
|
||||
int discountedYears = Math.min(years, token.getDiscountYears());
|
||||
|
||||
@@ -116,15 +116,21 @@ public class HostFlowUtils {
|
||||
if (inetAddresses == null) {
|
||||
return;
|
||||
}
|
||||
if (inetAddresses.stream().anyMatch(InetAddress::isLoopbackAddress)) {
|
||||
throw new LoopbackIpNotValidForHostException();
|
||||
for (InetAddress inetAddress : inetAddresses) {
|
||||
if (inetAddress.isLoopbackAddress()
|
||||
|| inetAddress.isLinkLocalAddress()
|
||||
|| inetAddress.isSiteLocalAddress()
|
||||
|| inetAddress.isAnyLocalAddress()
|
||||
|| inetAddress.isMulticastAddress()) {
|
||||
throw new IpAddressNotRoutableException(inetAddress.getHostAddress());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Loopback IPs are not valid for hosts. */
|
||||
static class LoopbackIpNotValidForHostException extends ParameterValuePolicyErrorException {
|
||||
public LoopbackIpNotValidForHostException() {
|
||||
super("Loopback IPs are not valid for hosts");
|
||||
/** IP address is not a public, routable address. */
|
||||
static class IpAddressNotRoutableException extends ParameterValuePolicyErrorException {
|
||||
public IpAddressNotRoutableException(String ipAddress) {
|
||||
super(String.format("IP address %s is not a public, globally routable address", ipAddress));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package google.registry.flows.picker;
|
||||
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableTable;
|
||||
@@ -255,6 +256,17 @@ public class FlowPicker {
|
||||
if (innerCommand == null && !(eppInput.getCommandWrapper() instanceof Hello)) {
|
||||
throw new MissingCommandException();
|
||||
}
|
||||
if (innerCommand instanceof ResourceCommandWrapper resourceCommandWrapper) {
|
||||
ResourceCommand resourceCommand = resourceCommandWrapper.getResourceCommand();
|
||||
if (resourceCommand != null) {
|
||||
String wrapperName = innerCommand.getClass().getSimpleName();
|
||||
String commandName = resourceCommand.getClass().getSimpleName();
|
||||
if (!wrapperName.equals(commandName)) {
|
||||
throw new MismatchedCommandException(
|
||||
Ascii.toLowerCase(wrapperName), Ascii.toLowerCase(commandName));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try the FlowProviders until we find a match. The order matters because it's possible to
|
||||
// match multiple FlowProviders and so more specific matches are tried first.
|
||||
for (FlowProvider flowProvider : FLOW_PROVIDERS) {
|
||||
@@ -279,4 +291,14 @@ public class FlowPicker {
|
||||
super("Command missing");
|
||||
}
|
||||
}
|
||||
|
||||
/** Command wrapper and inner resource command do not match. */
|
||||
static class MismatchedCommandException extends SyntaxErrorException {
|
||||
public MismatchedCommandException(String wrapperName, String commandName) {
|
||||
super(
|
||||
String.format(
|
||||
"EPP command wrapper <%s> does not match resource command <%s>",
|
||||
wrapperName, commandName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,8 @@ import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
import com.google.cloud.storage.BlobId;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.net.MediaType;
|
||||
@@ -41,6 +39,8 @@ import jakarta.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** Copy all registrar detail reports in a given bucket's subdirectory from GCS to Drive. */
|
||||
@Action(
|
||||
@@ -54,6 +54,9 @@ public final class CopyDetailReportsAction implements Runnable {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private static final Pattern FILENAME_PATTERN =
|
||||
Pattern.compile("^invoice_details_[0-9]{4}-[0-9]{2}_(.+)_.+\\.csv$");
|
||||
|
||||
private final String billingBucket;
|
||||
private final String invoiceDirectoryPrefix;
|
||||
private final DriveConnection driveConnection;
|
||||
@@ -101,8 +104,13 @@ public final class CopyDetailReportsAction implements Runnable {
|
||||
new ImmutableMultimap.Builder<>();
|
||||
for (String detailReportName : detailReportObjectNames) {
|
||||
// The standard report format is "invoice_details_yyyy-MM_registrarId_tld.csv
|
||||
// TODO(larryruili): Determine a safer way of enforcing this.
|
||||
String registrarId = Iterables.get(Splitter.on('_').split(detailReportName), 3);
|
||||
Matcher matcher = FILENAME_PATTERN.matcher(detailReportName);
|
||||
if (!matcher.matches()) {
|
||||
logger.atWarning().log(
|
||||
"Detail report filename '%s' does not match the expected pattern.", detailReportName);
|
||||
continue;
|
||||
}
|
||||
String registrarId = matcher.group(1);
|
||||
Optional<Registrar> registrar = Registrar.loadByRegistrarId(registrarId);
|
||||
if (registrar.isEmpty()) {
|
||||
logger.atWarning().log(
|
||||
|
||||
@@ -105,7 +105,8 @@ public abstract class ConsoleApiAction implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
protected void checkPermission(User user, String registrarId, ConsolePermission permission) {
|
||||
protected static void checkPermission(
|
||||
User user, String registrarId, ConsolePermission permission) {
|
||||
if (!user.getUserRoles().hasPermission(registrarId, permission)) {
|
||||
throw new ConsolePermissionForbiddenException(
|
||||
String.format(
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableSet;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import google.registry.flows.EppException.AuthenticationErrorException;
|
||||
import google.registry.flows.PasswordOnlyTransportCredentials;
|
||||
import google.registry.model.console.ConsolePermission;
|
||||
import google.registry.model.console.ConsoleUpdateHistory;
|
||||
import google.registry.model.console.User;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
@@ -84,6 +85,8 @@ public class ConsoleEppPasswordAction extends ConsoleApiAction {
|
||||
eppRequestBody.newPassword().equals(eppRequestBody.newPasswordRepeat()),
|
||||
"New password fields don't match");
|
||||
|
||||
checkPermission(user, eppRequestBody.registrarId(), ConsolePermission.CONFIGURE_EPP_CONNECTION);
|
||||
|
||||
Registrar registrar;
|
||||
try {
|
||||
registrar = registrarAccessor.getRegistrar(eppRequestBody.registrarId());
|
||||
|
||||
@@ -62,17 +62,20 @@ public class ConsoleHistoryDataAction extends ConsoleApiAction {
|
||||
private final Optional<String> consoleUserEmail;
|
||||
|
||||
private final String supportEmail;
|
||||
private final int historyQueryLimit;
|
||||
|
||||
@Inject
|
||||
public ConsoleHistoryDataAction(
|
||||
ConsoleApiParams consoleApiParams,
|
||||
@Config("supportEmail") String supportEmail,
|
||||
@Config("consoleHistoryQueryLimit") int historyQueryLimit,
|
||||
@Parameter("registrarId") String registrarId,
|
||||
@Parameter("consoleUserEmail") Optional<String> consoleUserEmail) {
|
||||
super(consoleApiParams);
|
||||
this.registrarId = registrarId;
|
||||
this.consoleUserEmail = consoleUserEmail;
|
||||
this.supportEmail = supportEmail;
|
||||
this.historyQueryLimit = historyQueryLimit;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,6 +120,7 @@ public class ConsoleHistoryDataAction extends ConsoleApiAction {
|
||||
.createNativeQuery(SQL_REGISTRAR_HISTORY, ConsoleUpdateHistory.class)
|
||||
.setParameter("registrarId", registrarId)
|
||||
.setHint("org.hibernate.fetchSize", 1000)
|
||||
.setMaxResults(historyQueryLimit)
|
||||
.getResultList());
|
||||
|
||||
List<ConsoleUpdateHistory> formattedHistoryList =
|
||||
|
||||
+18
-7
@@ -86,7 +86,7 @@ public class PasswordResetRequestAction extends ConsoleApiAction {
|
||||
"Must provide registry lock email to reset");
|
||||
requiredPermission = ConsolePermission.MANAGE_USERS;
|
||||
destinationEmail = passwordResetRequestData.registryLockEmail;
|
||||
checkUserExistsWithRegistryLockEmail(destinationEmail);
|
||||
checkUserExistsWithRegistryLockEmail(destinationEmail, registrarId);
|
||||
emailSubject = "Registry lock password reset request";
|
||||
}
|
||||
default -> throw new IllegalArgumentException("Unknown type " + type);
|
||||
@@ -121,12 +121,23 @@ public class PasswordResetRequestAction extends ConsoleApiAction {
|
||||
.sendEmail(EmailMessage.create(emailSubject, body, destinationAddress));
|
||||
}
|
||||
|
||||
static User checkUserExistsWithRegistryLockEmail(String destinationEmail) {
|
||||
return tm().createQueryComposer(User.class)
|
||||
.where("registryLockEmailAddress", QueryComposer.Comparator.EQ, destinationEmail)
|
||||
.first()
|
||||
.orElseThrow(
|
||||
() -> new IllegalArgumentException("Unknown user with lock email " + destinationEmail));
|
||||
static User checkUserExistsWithRegistryLockEmail(String destinationEmail, String registrarId) {
|
||||
User targetUser =
|
||||
tm().createQueryComposer(User.class)
|
||||
.where("registryLockEmailAddress", QueryComposer.Comparator.EQ, destinationEmail)
|
||||
.first()
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new IllegalArgumentException(
|
||||
"Unknown user with lock email " + destinationEmail));
|
||||
|
||||
// Prevent IDOR: Ensure the resolved user has the right permission
|
||||
checkArgument(
|
||||
targetUser.getUserRoles().hasPermission(registrarId, ConsolePermission.REGISTRY_LOCK),
|
||||
"User %s does not have permission REGISTRY_LOCK on registrar %s",
|
||||
targetUser.getEmailAddress(),
|
||||
registrarId);
|
||||
return targetUser;
|
||||
}
|
||||
|
||||
private String getAdminPocEmail(String registrarId) {
|
||||
|
||||
+12
-2
@@ -98,7 +98,9 @@ public class PasswordResetVerifyAction extends ConsoleApiAction {
|
||||
}
|
||||
|
||||
private void handleRegistryLockPasswordReset(PasswordResetRequest request) {
|
||||
User affectedUser = checkUserExistsWithRegistryLockEmail(request.getDestinationEmail());
|
||||
User affectedUser =
|
||||
checkUserExistsWithRegistryLockEmail(
|
||||
request.getDestinationEmail(), request.getRegistrarId());
|
||||
tm().put(
|
||||
affectedUser
|
||||
.asBuilder()
|
||||
@@ -119,7 +121,15 @@ public class PasswordResetVerifyAction extends ConsoleApiAction {
|
||||
ConsolePermission requiredVerifyPermission =
|
||||
switch (request.getType()) {
|
||||
case EPP -> ConsolePermission.MANAGE_USERS;
|
||||
case REGISTRY_LOCK -> ConsolePermission.REGISTRY_LOCK;
|
||||
case REGISTRY_LOCK -> {
|
||||
checkArgument(
|
||||
user.getRegistryLockEmailAddress()
|
||||
.map(address -> address.equals(request.getDestinationEmail()))
|
||||
.orElse(false),
|
||||
"User %s has the wrong registry lock email address",
|
||||
user.getEmailAddress());
|
||||
yield ConsolePermission.REGISTRY_LOCK;
|
||||
}
|
||||
};
|
||||
checkPermission(user, request.getRegistrarId(), requiredVerifyPermission);
|
||||
if (plusHours(request.getRequestTime(), 1).isBefore(tm().getTxTime())) {
|
||||
|
||||
+12
@@ -14,6 +14,7 @@
|
||||
|
||||
package google.registry.ui.server.console.domains;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
@@ -22,6 +23,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.flows.EppController;
|
||||
import google.registry.flows.EppRequestSource;
|
||||
import google.registry.flows.PasswordOnlyTransportCredentials;
|
||||
@@ -64,11 +66,13 @@ public class ConsoleBulkDomainAction extends ConsoleApiAction {
|
||||
private final String registrarId;
|
||||
private final String bulkDomainAction;
|
||||
private final Optional<JsonElement> optionalJsonPayload;
|
||||
private final int bulkDomainActionLimit;
|
||||
|
||||
@Inject
|
||||
public ConsoleBulkDomainAction(
|
||||
ConsoleApiParams consoleApiParams,
|
||||
EppController eppController,
|
||||
@Config("consoleBulkDomainActionLimit") int bulkDomainActionLimit,
|
||||
@Parameter("registrarId") String registrarId,
|
||||
@Parameter("bulkDomainAction") String bulkDomainAction,
|
||||
@OptionalJsonPayload Optional<JsonElement> optionalJsonPayload) {
|
||||
@@ -77,6 +81,7 @@ public class ConsoleBulkDomainAction extends ConsoleApiAction {
|
||||
this.registrarId = registrarId;
|
||||
this.bulkDomainAction = bulkDomainAction;
|
||||
this.optionalJsonPayload = optionalJsonPayload;
|
||||
this.bulkDomainActionLimit = bulkDomainActionLimit;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -85,6 +90,13 @@ public class ConsoleBulkDomainAction extends ConsoleApiAction {
|
||||
optionalJsonPayload.orElseThrow(
|
||||
() -> new IllegalArgumentException("Bulk action payload must be present"));
|
||||
BulkDomainList domainList = consoleApiParams.gson().fromJson(jsonPayload, BulkDomainList.class);
|
||||
checkArgument(
|
||||
domainList.domainList != null && !domainList.domainList.isEmpty(),
|
||||
"Domain list cannot be empty");
|
||||
checkArgument(
|
||||
domainList.domainList.size() <= bulkDomainActionLimit,
|
||||
"Cannot process more than %s domains in a single bulk action",
|
||||
bulkDomainActionLimit);
|
||||
ConsoleDomainActionType actionType =
|
||||
ConsoleDomainActionType.parseActionType(bulkDomainAction, jsonPayload);
|
||||
|
||||
|
||||
@@ -203,6 +203,29 @@ public class DomainPricingLogicTest {
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDomainCreatePrice_discountPriceAllocationToken_oneYearCreate_moreDiscountYears()
|
||||
throws EppException {
|
||||
AllocationToken allocationToken =
|
||||
persistResource(
|
||||
new AllocationToken.Builder()
|
||||
.setToken("abc123_more_discount")
|
||||
.setTokenType(SINGLE_USE)
|
||||
.setDomainName("default.example")
|
||||
.setDiscountPrice(Money.of(USD, 5))
|
||||
.setDiscountYears(2)
|
||||
.setRegistrationBehavior(RegistrationBehavior.DEFAULT)
|
||||
.build());
|
||||
assertThat(
|
||||
domainPricingLogic.getCreatePrice(
|
||||
tld, "default.example", clock.now(), 1, false, false, Optional.of(allocationToken)))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
.addFeeOrCredit(Fee.create(new BigDecimal("5.00"), CREATE, false))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDomainRenewPrice_oneYear_standardDomain_noBilling_isStandardPrice()
|
||||
throws EppException {
|
||||
@@ -1093,4 +1116,23 @@ public class DomainPricingLogicTest {
|
||||
.getRenewCost())
|
||||
.isEqualTo(Money.of(USD, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDomainRenewPrice_specifiedToken_multiYear() throws Exception {
|
||||
AllocationToken allocationToken =
|
||||
persistResource(
|
||||
new AllocationToken.Builder()
|
||||
.setToken("abc123_multi")
|
||||
.setTokenType(SINGLE_USE)
|
||||
.setDomainName("premium.example")
|
||||
.setRenewalPriceBehavior(SPECIFIED)
|
||||
.setRenewalPrice(Money.of(USD, 5))
|
||||
.build());
|
||||
assertThat(
|
||||
domainPricingLogic
|
||||
.getRenewPrice(
|
||||
tld, "premium.example", clock.now(), 5, null, Optional.of(allocationToken))
|
||||
.getRenewCost())
|
||||
.isEqualTo(Money.of(USD, 25));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ import google.registry.flows.host.HostFlowUtils.HostNameNotPunyCodedException;
|
||||
import google.registry.flows.host.HostFlowUtils.HostNameTooLongException;
|
||||
import google.registry.flows.host.HostFlowUtils.HostNameTooShallowException;
|
||||
import google.registry.flows.host.HostFlowUtils.InvalidHostNameException;
|
||||
import google.registry.flows.host.HostFlowUtils.LoopbackIpNotValidForHostException;
|
||||
import google.registry.flows.host.HostFlowUtils.IpAddressNotRoutableException;
|
||||
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainDoesNotExistException;
|
||||
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainInPendingDeleteException;
|
||||
import google.registry.model.ForeignKeyUtils;
|
||||
@@ -354,22 +354,62 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, Host> {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_localhostInetAddress_ipv4() {
|
||||
void testFailure_loopbackInetAddress_ipv4() {
|
||||
createTld("tld");
|
||||
persistActiveDomain("example.tld");
|
||||
setEppHostCreateInput("ns1.example.tld", "<host:addr ip=\"v4\">127.0.0.1</host:addr>");
|
||||
assertAboutEppExceptions()
|
||||
.that(assertThrows(LoopbackIpNotValidForHostException.class, this::runFlow))
|
||||
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
|
||||
.marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_localhostInetAddress_ipv6() {
|
||||
void testFailure_loopbackInetAddress_ipv6() {
|
||||
createTld("tld");
|
||||
persistActiveDomain("example.tld");
|
||||
setEppHostCreateInput("ns1.example.tld", "<host:addr ip=\"v6\">::1</host:addr>");
|
||||
assertAboutEppExceptions()
|
||||
.that(assertThrows(LoopbackIpNotValidForHostException.class, this::runFlow))
|
||||
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
|
||||
.marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_linkLocalInetAddress_ipv4() {
|
||||
createTld("tld");
|
||||
persistActiveDomain("example.tld");
|
||||
setEppHostCreateInput("ns1.example.tld", "<host:addr ip=\"v4\">169.254.1.1</host:addr>");
|
||||
assertAboutEppExceptions()
|
||||
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
|
||||
.marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_linkLocalInetAddress_ipv6() {
|
||||
createTld("tld");
|
||||
persistActiveDomain("example.tld");
|
||||
setEppHostCreateInput("ns1.example.tld", "<host:addr ip=\"v6\">fe80::1</host:addr>");
|
||||
assertAboutEppExceptions()
|
||||
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
|
||||
.marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_privateInetAddress_ipv4() {
|
||||
createTld("tld");
|
||||
persistActiveDomain("example.tld");
|
||||
setEppHostCreateInput("ns1.example.tld", "<host:addr ip=\"v4\">192.168.1.1</host:addr>");
|
||||
assertAboutEppExceptions()
|
||||
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
|
||||
.marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_anyLocalInetAddress_ipv4() {
|
||||
createTld("tld");
|
||||
persistActiveDomain("example.tld");
|
||||
setEppHostCreateInput("ns1.example.tld", "<host:addr ip=\"v4\">0.0.0.0</host:addr>");
|
||||
assertAboutEppExceptions()
|
||||
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
|
||||
.marshalsToXml();
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
|
||||
import static google.registry.testing.HostSubject.assertAboutHosts;
|
||||
import static google.registry.util.DateTimeUtils.minusDays;
|
||||
import static google.registry.util.DateTimeUtils.plusDays;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
@@ -42,6 +43,8 @@ import google.registry.flows.host.HostFlowUtils.HostNameNotLowerCaseException;
|
||||
import google.registry.flows.host.HostFlowUtils.HostNameNotNormalizedException;
|
||||
import google.registry.flows.host.HostFlowUtils.HostNameNotPunyCodedException;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.GracePeriod;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.host.Host;
|
||||
import google.registry.model.reporting.HistoryEntry.Type;
|
||||
@@ -311,6 +314,30 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, Host> {
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_failfastWhenLinkedToDomainInRedemption() throws Exception {
|
||||
createTld("tld");
|
||||
Host host = persistActiveHost("ns1.example.tld");
|
||||
Domain domain = persistResource(DatabaseHelper.newDomain("example.tld"));
|
||||
persistResource(
|
||||
domain
|
||||
.asBuilder()
|
||||
.setNameservers(ImmutableSet.of(host.createVKey()))
|
||||
.setDeletionTime(plusDays(clock.now(), 35))
|
||||
.addGracePeriod(
|
||||
GracePeriod.create(
|
||||
GracePeriodStatus.REDEMPTION,
|
||||
domain.getRepoId(),
|
||||
plusDays(clock.now(), 1),
|
||||
"TheRegistrar",
|
||||
null))
|
||||
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
|
||||
.build());
|
||||
|
||||
EppException thrown = assertThrows(ResourceToDeleteIsReferencedException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_nonLowerCaseHostname() {
|
||||
setEppInput("host_delete.xml", ImmutableMap.of("HOSTNAME", "NS1.EXAMPLE.NET"));
|
||||
|
||||
@@ -65,7 +65,7 @@ import google.registry.flows.host.HostFlowUtils.HostNameNotNormalizedException;
|
||||
import google.registry.flows.host.HostFlowUtils.HostNameNotPunyCodedException;
|
||||
import google.registry.flows.host.HostFlowUtils.HostNameTooLongException;
|
||||
import google.registry.flows.host.HostFlowUtils.HostNameTooShallowException;
|
||||
import google.registry.flows.host.HostFlowUtils.LoopbackIpNotValidForHostException;
|
||||
import google.registry.flows.host.HostFlowUtils.IpAddressNotRoutableException;
|
||||
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainDoesNotExistException;
|
||||
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainInPendingDeleteException;
|
||||
import google.registry.flows.host.HostUpdateFlow.CannotAddIpToExternalHostException;
|
||||
@@ -1391,24 +1391,68 @@ class HostUpdateFlowTest extends ResourceFlowTestCase<HostUpdateFlow, Host> {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_localhostInetAddress_ipv4() throws Exception {
|
||||
void testFailure_loopbackInetAddress_ipv4() throws Exception {
|
||||
createTld("tld");
|
||||
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
|
||||
setEppHostUpdateInput(
|
||||
"ns1.example.tld", "ns2.example.tld", "<host:addr ip=\"v4\">127.0.0.1</host:addr>", null);
|
||||
assertAboutEppExceptions()
|
||||
.that(assertThrows(LoopbackIpNotValidForHostException.class, this::runFlow))
|
||||
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
|
||||
.marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_localhostInetAddress_ipv6() throws Exception {
|
||||
void testFailure_loopbackInetAddress_ipv6() throws Exception {
|
||||
createTld("tld");
|
||||
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
|
||||
setEppHostUpdateInput(
|
||||
"ns1.example.tld", "ns2.example.tld", "<host:addr ip=\"v6\">::1</host:addr>", null);
|
||||
assertAboutEppExceptions()
|
||||
.that(assertThrows(LoopbackIpNotValidForHostException.class, this::runFlow))
|
||||
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
|
||||
.marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_linkLocalInetAddress_ipv4() throws Exception {
|
||||
createTld("tld");
|
||||
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
|
||||
setEppHostUpdateInput(
|
||||
"ns1.example.tld", "ns2.example.tld", "<host:addr ip=\"v4\">169.254.1.1</host:addr>", null);
|
||||
assertAboutEppExceptions()
|
||||
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
|
||||
.marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_linkLocalInetAddress_ipv6() throws Exception {
|
||||
createTld("tld");
|
||||
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
|
||||
setEppHostUpdateInput(
|
||||
"ns1.example.tld", "ns2.example.tld", "<host:addr ip=\"v6\">fe80::1</host:addr>", null);
|
||||
assertAboutEppExceptions()
|
||||
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
|
||||
.marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_privateInetAddress_ipv4() throws Exception {
|
||||
createTld("tld");
|
||||
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
|
||||
setEppHostUpdateInput(
|
||||
"ns1.example.tld", "ns2.example.tld", "<host:addr ip=\"v4\">192.168.1.1</host:addr>", null);
|
||||
assertAboutEppExceptions()
|
||||
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
|
||||
.marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_anyLocalInetAddress_ipv4() throws Exception {
|
||||
createTld("tld");
|
||||
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
|
||||
setEppHostUpdateInput(
|
||||
"ns1.example.tld", "ns2.example.tld", "<host:addr ip=\"v4\">0.0.0.0</host:addr>", null);
|
||||
assertAboutEppExceptions()
|
||||
.that(assertThrows(IpAddressNotRoutableException.class, this::runFlow))
|
||||
.marshalsToXml();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2026 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.picker;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import google.registry.flows.Flow;
|
||||
import google.registry.flows.domain.DomainCheckFlow;
|
||||
import google.registry.flows.domain.DomainCreateFlow;
|
||||
import google.registry.model.domain.DomainCommand;
|
||||
import google.registry.model.eppcommon.EppXmlTransformer;
|
||||
import google.registry.model.eppinput.EppInput;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link FlowPicker}. */
|
||||
class FlowPickerTest {
|
||||
|
||||
@Test
|
||||
void testGetFlowClass_matchingWrapperAndCommand_returnsFlow() throws Exception {
|
||||
EppInput checkEppInput = EppInput.create(EppInput.Check.create(new DomainCommand.Check()));
|
||||
Class<? extends Flow> checkFlow = FlowPicker.getFlowClass(checkEppInput);
|
||||
assertThat(checkFlow).isEqualTo(DomainCheckFlow.class);
|
||||
|
||||
EppInput createEppInput = EppInput.create(EppInput.Create.create(new DomainCommand.Create()));
|
||||
Class<? extends Flow> createFlow = FlowPicker.getFlowClass(createEppInput);
|
||||
assertThat(createFlow).isEqualTo(DomainCreateFlow.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFlowClass_mismatchedWrapperAndCommand_throwsSyntaxErrorException() {
|
||||
EppInput mismatchedEppInput1 =
|
||||
EppInput.create(EppInput.Check.create(new DomainCommand.Create()));
|
||||
assertThat(
|
||||
assertThrows(
|
||||
FlowPicker.MismatchedCommandException.class,
|
||||
() -> FlowPicker.getFlowClass(mismatchedEppInput1)))
|
||||
.hasMessageThat()
|
||||
.isEqualTo("EPP command wrapper <check> does not match resource command <create>");
|
||||
|
||||
EppInput mismatchedEppInput2 =
|
||||
EppInput.create(EppInput.Create.create(new DomainCommand.Check()));
|
||||
assertThat(
|
||||
assertThrows(
|
||||
FlowPicker.MismatchedCommandException.class,
|
||||
() -> FlowPicker.getFlowClass(mismatchedEppInput2)))
|
||||
.hasMessageThat()
|
||||
.contains("EPP command wrapper <create> does not match resource command <check>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFlowClass_mismatchedXml_throwsMismatchedCommandException() throws Exception {
|
||||
String mismatchedXml =
|
||||
"""
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<check>
|
||||
<domain:create xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>example.com</domain:name>
|
||||
<domain:authInfo>
|
||||
<domain:pw>fooBAR123</domain:pw>
|
||||
</domain:authInfo>
|
||||
</domain:create>
|
||||
</check>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
""";
|
||||
|
||||
// Verify JAXB successfully unmarshals the mismatched XML without throwing any errors
|
||||
EppInput eppInput = EppXmlTransformer.unmarshal(EppInput.class, mismatchedXml.getBytes(UTF_8));
|
||||
assertThat(eppInput).isNotNull();
|
||||
|
||||
// Verify that FlowPicker intercepts the unmarshalled input and blocks it
|
||||
assertThat(
|
||||
assertThrows(
|
||||
FlowPicker.MismatchedCommandException.class,
|
||||
() -> FlowPicker.getFlowClass(eppInput)))
|
||||
.hasMessageThat()
|
||||
.contains("EPP command wrapper <check> does not match resource command <create>");
|
||||
}
|
||||
}
|
||||
@@ -248,4 +248,38 @@ class CopyDetailReportsActionTest {
|
||||
action.run();
|
||||
verifyNoInteractions(driveConnection);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_registrarIdWithUnderscores() throws IOException {
|
||||
persistResource(
|
||||
loadRegistrar("TheRegistrar")
|
||||
.asBuilder()
|
||||
.setRegistrarId("The_Registrar")
|
||||
.setDriveFolderId("0B-99999")
|
||||
.build());
|
||||
|
||||
gcsUtils.createFromBytes(
|
||||
BlobId.of("test-bucket", "results/invoice_details_2017-10_The_Registrar_test.csv"),
|
||||
"hello,world\n1,2".getBytes(UTF_8));
|
||||
|
||||
action.run();
|
||||
verify(driveConnection)
|
||||
.createOrUpdateFile(
|
||||
"invoice_details_2017-10_The_Registrar_test.csv",
|
||||
MediaType.CSV_UTF_8,
|
||||
"0B-99999",
|
||||
"hello,world\n1,2".getBytes(UTF_8));
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_invalidFilenamePattern_skipped() throws IOException {
|
||||
gcsUtils.createFromBytes(
|
||||
BlobId.of("test-bucket", "results/invoice_details_2017-10.csv"),
|
||||
"hello,world\n1,2".getBytes(UTF_8));
|
||||
|
||||
action.run();
|
||||
verifyNoInteractions(driveConnection);
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-1
@@ -23,6 +23,8 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonParser;
|
||||
import google.registry.model.console.ConsoleUpdateHistory;
|
||||
import google.registry.model.console.RegistrarRole;
|
||||
import google.registry.model.console.User;
|
||||
@@ -162,6 +164,27 @@ class ConsoleHistoryDataActionTest extends ConsoleActionBaseTestCase {
|
||||
assertThat(response.getPayload()).isEqualTo("[]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_limitsResults() {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
DatabaseHelper.persistResource(
|
||||
new ConsoleUpdateHistory.Builder()
|
||||
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
|
||||
.setDescription("TheRegistrar|some detail " + i)
|
||||
.setActingUser(fteUser)
|
||||
.setUrl("https://test.com")
|
||||
.setMethod("GET")
|
||||
.setModificationTime(clock.now())
|
||||
.build());
|
||||
}
|
||||
ConsoleHistoryDataAction action =
|
||||
createAction(AuthResult.createUser(fteUser), "TheRegistrar", Optional.empty(), 5);
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
JsonArray payload = JsonParser.parseString(response.getPayload()).getAsJsonArray();
|
||||
assertThat(payload.size()).isEqualTo(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_getByRegistrar_noPermission() {
|
||||
ConsoleHistoryDataAction action =
|
||||
@@ -192,10 +215,15 @@ class ConsoleHistoryDataActionTest extends ConsoleActionBaseTestCase {
|
||||
|
||||
private ConsoleHistoryDataAction createAction(
|
||||
AuthResult authResult, String registrarId, Optional<String> consoleUserEmail) {
|
||||
return createAction(authResult, registrarId, consoleUserEmail, 500);
|
||||
}
|
||||
|
||||
private ConsoleHistoryDataAction createAction(
|
||||
AuthResult authResult, String registrarId, Optional<String> consoleUserEmail, int limit) {
|
||||
consoleApiParams = ConsoleApiParamsUtils.createFake(authResult);
|
||||
when(consoleApiParams.request().getMethod()).thenReturn("GET");
|
||||
response = (FakeResponse) consoleApiParams.response();
|
||||
return new ConsoleHistoryDataAction(
|
||||
consoleApiParams, SUPPORT_EMAIL, registrarId, consoleUserEmail);
|
||||
consoleApiParams, SUPPORT_EMAIL, limit, registrarId, consoleUserEmail);
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -158,6 +158,31 @@ public class PasswordResetRequestActionTest extends ConsoleActionBaseTestCase {
|
||||
.isEqualTo("Unknown user with lock email nonexistent@email.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_registryLock_userNoLockPermission() throws Exception {
|
||||
DatabaseHelper.persistResource(
|
||||
new User.Builder()
|
||||
.setEmailAddress("email@registry.tld")
|
||||
.setUserRoles(
|
||||
new UserRoles.Builder()
|
||||
.setRegistrarRoles(
|
||||
ImmutableMap.of("TheRegistrar", RegistrarRole.ACCOUNT_MANAGER))
|
||||
.build())
|
||||
.setRegistryLockEmailAddress("registrylock@theregistrar.com")
|
||||
.build());
|
||||
PasswordResetRequestAction action =
|
||||
createAction(
|
||||
PasswordResetRequest.Type.REGISTRY_LOCK,
|
||||
"TheRegistrar",
|
||||
"registrylock@theregistrar.com");
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo(
|
||||
"User email@registry.tld does not have permission REGISTRY_LOCK on registrar"
|
||||
+ " TheRegistrar");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("Enable when testing is done in sandbox and isAdmin check is removed")
|
||||
void testFailure_epp_noPermission() throws Exception {
|
||||
|
||||
+42
@@ -157,6 +157,48 @@ public class PasswordResetVerifyActionTest extends ConsoleActionBaseTestCase {
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_post_lock_differentRegistryLockEmail() throws Exception {
|
||||
User user =
|
||||
persistResource(
|
||||
new User.Builder()
|
||||
.setEmailAddress("anotheruser@example.tld")
|
||||
.setUserRoles(
|
||||
new UserRoles.Builder()
|
||||
.setRegistrarRoles(
|
||||
ImmutableMap.of(
|
||||
"TheRegistrar", RegistrarRole.ACCOUNT_MANAGER_WITH_REGISTRY_LOCK))
|
||||
.build())
|
||||
.setRegistryLockEmailAddress("anotherregistrylock@theregistrar.com")
|
||||
.build());
|
||||
verificationCode = saveRequest(PasswordResetRequest.Type.REGISTRY_LOCK).getVerificationCode();
|
||||
createAction(user, "POST", verificationCode, "newPassword").run();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo("User anotheruser@example.tld has the wrong registry lock email address");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_get_lock_differentRegistryLockEmail() throws Exception {
|
||||
User user =
|
||||
persistResource(
|
||||
new User.Builder()
|
||||
.setEmailAddress("anotheruser@example.tld")
|
||||
.setUserRoles(
|
||||
new UserRoles.Builder()
|
||||
.setRegistrarRoles(
|
||||
ImmutableMap.of(
|
||||
"TheRegistrar", RegistrarRole.ACCOUNT_MANAGER_WITH_REGISTRY_LOCK))
|
||||
.build())
|
||||
.setRegistryLockEmailAddress("anotherregistrylock@theregistrar.com")
|
||||
.build());
|
||||
verificationCode = saveRequest(PasswordResetRequest.Type.REGISTRY_LOCK).getVerificationCode();
|
||||
createAction(user, "GET", verificationCode, null).run();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo("User anotheruser@example.tld has the wrong registry lock email address");
|
||||
}
|
||||
|
||||
private User createTechUser() {
|
||||
return new User.Builder()
|
||||
.setEmailAddress("tech@example.tld")
|
||||
|
||||
+34
-3
@@ -45,6 +45,7 @@ import google.registry.testing.ConsoleApiParamsUtils;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.ui.server.console.ConsoleActionBaseTestCase;
|
||||
import google.registry.ui.server.console.ConsoleApiParams;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -173,7 +174,9 @@ public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase {
|
||||
|
||||
@Test
|
||||
void testFailure_badActionString() {
|
||||
ConsoleBulkDomainAction action = createAction("bad", GSON.toJsonTree(ImmutableMap.of()));
|
||||
ConsoleBulkDomainAction action =
|
||||
createAction(
|
||||
"bad", GSON.toJsonTree(ImmutableMap.of("domainList", ImmutableList.of("domain.tld"))));
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST);
|
||||
assertThat(response.getPayload())
|
||||
@@ -190,6 +193,29 @@ public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase {
|
||||
assertThat(response.getPayload()).isEqualTo("Bulk action payload must be present");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_listTooLarge() {
|
||||
JsonElement payload =
|
||||
GSON.toJsonTree(
|
||||
ImmutableMap.of(
|
||||
"domainList", Collections.nCopies(6, "domain.tld"), "reason", "reason"));
|
||||
ConsoleBulkDomainAction action = createAction("DELETE", payload, fteUser, 5);
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST);
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo("Cannot process more than 5 domains in a single bulk action");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_emptyList() {
|
||||
JsonElement payload =
|
||||
GSON.toJsonTree(ImmutableMap.of("domainList", ImmutableList.of(), "reason", "reason"));
|
||||
ConsoleBulkDomainAction action = createAction("DELETE", payload);
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST);
|
||||
assertThat(response.getPayload()).isEqualTo("Domain list cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_noPermission() {
|
||||
JsonElement payload =
|
||||
@@ -232,15 +258,20 @@ public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase {
|
||||
// }
|
||||
|
||||
private ConsoleBulkDomainAction createAction(String action, JsonElement payload) {
|
||||
return createAction(action, payload, fteUser);
|
||||
return createAction(action, payload, fteUser, 500);
|
||||
}
|
||||
|
||||
private ConsoleBulkDomainAction createAction(String action, JsonElement payload, User user) {
|
||||
return createAction(action, payload, user, 500);
|
||||
}
|
||||
|
||||
private ConsoleBulkDomainAction createAction(
|
||||
String action, JsonElement payload, User user, int limit) {
|
||||
AuthResult authResult = AuthResult.createUser(user);
|
||||
ConsoleApiParams params = ConsoleApiParamsUtils.createFake(authResult);
|
||||
when(params.request().getMethod()).thenReturn("POST");
|
||||
response = (FakeResponse) params.response();
|
||||
return new ConsoleBulkDomainAction(
|
||||
params, eppController, "TheRegistrar", action, Optional.ofNullable(payload));
|
||||
params, eppController, limit, "TheRegistrar", action, Optional.ofNullable(payload));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user