mirror of
https://github.com/google/nomulus.git
synced 2025-07-22 10:46:10 +02:00
Add email notification of BSA job status (#2368)
This commit is contained in:
parent
cd95be4776
commit
9af006836c
10 changed files with 342 additions and 23 deletions
41
core/src/main/java/google/registry/bsa/BsaEmailSender.java
Normal file
41
core/src/main/java/google/registry/bsa/BsaEmailSender.java
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright 2024 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.bsa;
|
||||||
|
|
||||||
|
import google.registry.config.RegistryConfig.Config;
|
||||||
|
import google.registry.groups.GmailClient;
|
||||||
|
import google.registry.util.EmailMessage;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.mail.internet.InternetAddress;
|
||||||
|
|
||||||
|
/** Sends BSA-related email notifications. */
|
||||||
|
class BsaEmailSender {
|
||||||
|
|
||||||
|
private final InternetAddress alertRecipientAddress;
|
||||||
|
private final GmailClient gmailClient;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
BsaEmailSender(
|
||||||
|
GmailClient gmailClient,
|
||||||
|
@Config("newAlertRecipientEmailAddress") InternetAddress alertRecipientAddress) {
|
||||||
|
this.alertRecipientAddress = alertRecipientAddress;
|
||||||
|
this.gmailClient = gmailClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sends an email to the configured alert recipient. */
|
||||||
|
void sendNotification(String subject, String body) {
|
||||||
|
this.gmailClient.sendEmail(EmailMessage.create(subject, body, alertRecipientAddress));
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
package google.registry.bsa;
|
package google.registry.bsa;
|
||||||
|
|
||||||
|
import static com.google.common.base.Throwables.getStackTraceAsString;
|
||||||
import static google.registry.bsa.BsaStringUtils.LINE_SPLITTER;
|
import static google.registry.bsa.BsaStringUtils.LINE_SPLITTER;
|
||||||
import static google.registry.request.Action.Method.GET;
|
import static google.registry.request.Action.Method.GET;
|
||||||
import static google.registry.request.Action.Method.POST;
|
import static google.registry.request.Action.Method.POST;
|
||||||
|
@ -55,6 +56,7 @@ public class BsaRefreshAction implements Runnable {
|
||||||
private final BsaReportSender bsaReportSender;
|
private final BsaReportSender bsaReportSender;
|
||||||
private final int transactionBatchSize;
|
private final int transactionBatchSize;
|
||||||
private final Duration domainCreateTxnCommitTimeLag;
|
private final Duration domainCreateTxnCommitTimeLag;
|
||||||
|
private final BsaEmailSender emailSender;
|
||||||
private final BsaLock bsaLock;
|
private final BsaLock bsaLock;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
private final Response response;
|
private final Response response;
|
||||||
|
@ -66,6 +68,7 @@ public class BsaRefreshAction implements Runnable {
|
||||||
BsaReportSender bsaReportSender,
|
BsaReportSender bsaReportSender,
|
||||||
@Config("bsaTxnBatchSize") int transactionBatchSize,
|
@Config("bsaTxnBatchSize") int transactionBatchSize,
|
||||||
@Config("domainCreateTxnCommitTimeLag") Duration domainCreateTxnCommitTimeLag,
|
@Config("domainCreateTxnCommitTimeLag") Duration domainCreateTxnCommitTimeLag,
|
||||||
|
BsaEmailSender emailSender,
|
||||||
BsaLock bsaLock,
|
BsaLock bsaLock,
|
||||||
Clock clock,
|
Clock clock,
|
||||||
Response response) {
|
Response response) {
|
||||||
|
@ -74,6 +77,7 @@ public class BsaRefreshAction implements Runnable {
|
||||||
this.bsaReportSender = bsaReportSender;
|
this.bsaReportSender = bsaReportSender;
|
||||||
this.transactionBatchSize = transactionBatchSize;
|
this.transactionBatchSize = transactionBatchSize;
|
||||||
this.domainCreateTxnCommitTimeLag = domainCreateTxnCommitTimeLag;
|
this.domainCreateTxnCommitTimeLag = domainCreateTxnCommitTimeLag;
|
||||||
|
this.emailSender = emailSender;
|
||||||
this.bsaLock = bsaLock;
|
this.bsaLock = bsaLock;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
this.response = response;
|
this.response = response;
|
||||||
|
@ -83,11 +87,15 @@ public class BsaRefreshAction implements Runnable {
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
if (!bsaLock.executeWithLock(this::runWithinLock)) {
|
if (!bsaLock.executeWithLock(this::runWithinLock)) {
|
||||||
logger.atInfo().log("Job is being executed by another worker.");
|
String message = "BSA refresh did not run: another BSA related task is running";
|
||||||
|
logger.atInfo().log("%s.", message);
|
||||||
|
emailSender.sendNotification(message, /* body= */ "");
|
||||||
|
} else {
|
||||||
|
emailSender.sendNotification("BSA refreshed successfully", "");
|
||||||
}
|
}
|
||||||
} catch (Throwable throwable) {
|
} catch (Throwable throwable) {
|
||||||
// TODO(12/31/2023): consider sending an alert email.
|
logger.atWarning().withCause(throwable).log("Failed to refresh BSA data.");
|
||||||
logger.atWarning().withCause(throwable).log("Failed to update block lists.");
|
emailSender.sendNotification("BSA refresh aborted", getStackTraceAsString(throwable));
|
||||||
}
|
}
|
||||||
// Always return OK. No need to use a retrier on `runWithinLock`. Its individual steps are
|
// Always return OK. No need to use a retrier on `runWithinLock`. Its individual steps are
|
||||||
// implicitly retried. If action fails, the next cron will continue at checkpoint.
|
// implicitly retried. If action fails, the next cron will continue at checkpoint.
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
package google.registry.bsa;
|
package google.registry.bsa;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkArgument;
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
import static google.registry.bsa.persistence.DownloadScheduler.fetchMostRecentDownloadJobIdIfCompleted;
|
import static com.google.common.base.Throwables.getStackTraceAsString;
|
||||||
|
import static google.registry.bsa.BsaTransactions.bsaQuery;
|
||||||
import static google.registry.bsa.persistence.Queries.batchReadBsaLabelText;
|
import static google.registry.bsa.persistence.Queries.batchReadBsaLabelText;
|
||||||
import static google.registry.request.Action.Method.GET;
|
import static google.registry.request.Action.Method.GET;
|
||||||
import static google.registry.request.Action.Method.POST;
|
import static google.registry.request.Action.Method.POST;
|
||||||
|
@ -28,6 +29,7 @@ import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import com.google.common.collect.Sets.SetView;
|
import com.google.common.collect.Sets.SetView;
|
||||||
import com.google.common.flogger.FluentLogger;
|
import com.google.common.flogger.FluentLogger;
|
||||||
|
import google.registry.bsa.persistence.DownloadScheduler;
|
||||||
import google.registry.config.RegistryConfig.Config;
|
import google.registry.config.RegistryConfig.Config;
|
||||||
import google.registry.request.Action;
|
import google.registry.request.Action;
|
||||||
import google.registry.request.Response;
|
import google.registry.request.Response;
|
||||||
|
@ -48,6 +50,7 @@ public class BsaValidateAction implements Runnable {
|
||||||
|
|
||||||
static final String PATH = "/_dr/task/bsaValidate";
|
static final String PATH = "/_dr/task/bsaValidate";
|
||||||
private final GcsClient gcsClient;
|
private final GcsClient gcsClient;
|
||||||
|
private final BsaEmailSender emailSender;
|
||||||
private final int transactionBatchSize;
|
private final int transactionBatchSize;
|
||||||
private final BsaLock bsaLock;
|
private final BsaLock bsaLock;
|
||||||
private final Response response;
|
private final Response response;
|
||||||
|
@ -55,10 +58,12 @@ public class BsaValidateAction implements Runnable {
|
||||||
@Inject
|
@Inject
|
||||||
BsaValidateAction(
|
BsaValidateAction(
|
||||||
GcsClient gcsClient,
|
GcsClient gcsClient,
|
||||||
|
BsaEmailSender emailSender,
|
||||||
@Config("bsaTxnBatchSize") int transactionBatchSize,
|
@Config("bsaTxnBatchSize") int transactionBatchSize,
|
||||||
BsaLock bsaLock,
|
BsaLock bsaLock,
|
||||||
Response response) {
|
Response response) {
|
||||||
this.gcsClient = gcsClient;
|
this.gcsClient = gcsClient;
|
||||||
|
this.emailSender = emailSender;
|
||||||
this.transactionBatchSize = transactionBatchSize;
|
this.transactionBatchSize = transactionBatchSize;
|
||||||
this.bsaLock = bsaLock;
|
this.bsaLock = bsaLock;
|
||||||
this.response = response;
|
this.response = response;
|
||||||
|
@ -68,12 +73,13 @@ public class BsaValidateAction implements Runnable {
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
if (!bsaLock.executeWithLock(this::runWithinLock)) {
|
if (!bsaLock.executeWithLock(this::runWithinLock)) {
|
||||||
logger.atInfo().log("Cannot execute action. Other BSA related task is executing.");
|
String message = "BSA validation did not run: another BSA related task is running";
|
||||||
// TODO(blocked by go/r3pr/2354): send email
|
logger.atInfo().log("%s.", message);
|
||||||
|
emailSender.sendNotification(message, /* body= */ "");
|
||||||
}
|
}
|
||||||
} catch (Throwable throwable) {
|
} catch (Throwable throwable) {
|
||||||
logger.atWarning().withCause(throwable).log("Failed to update block lists.");
|
logger.atWarning().withCause(throwable).log("Failed to validate block lists.");
|
||||||
// TODO(blocked by go/r3pr/2354): send email
|
emailSender.sendNotification("BSA validation aborted", getStackTraceAsString(throwable));
|
||||||
}
|
}
|
||||||
// Always return OK. No need to retry since all queries and GCS accesses are already
|
// Always return OK. No need to retry since all queries and GCS accesses are already
|
||||||
// implicitly retried.
|
// implicitly retried.
|
||||||
|
@ -82,23 +88,35 @@ public class BsaValidateAction implements Runnable {
|
||||||
|
|
||||||
/** Executes the validation action while holding the BSA lock. */
|
/** Executes the validation action while holding the BSA lock. */
|
||||||
Void runWithinLock() {
|
Void runWithinLock() {
|
||||||
Optional<String> downloadJobName = fetchMostRecentDownloadJobIdIfCompleted();
|
Optional<String> downloadJobName =
|
||||||
|
bsaQuery(DownloadScheduler::fetchMostRecentDownloadJobIdIfCompleted);
|
||||||
if (downloadJobName.isEmpty()) {
|
if (downloadJobName.isEmpty()) {
|
||||||
logger.atInfo().log("Cannot validate: latest download not found or unfinished.");
|
logger.atInfo().log("Cannot validate: block list downloads not found.");
|
||||||
|
emailSender.sendNotification(
|
||||||
|
"BSA validation does not run: block list downloads not found", "");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
logger.atInfo().log("Validating BSA with latest download: %s", downloadJobName.get());
|
logger.atInfo().log("Validating BSA with latest download: %s", downloadJobName.get());
|
||||||
|
|
||||||
ImmutableList.Builder<String> errors = new ImmutableList.Builder();
|
ImmutableList.Builder<String> errorsBuilder = new ImmutableList.Builder<>();
|
||||||
errors.addAll(checkBsaLabels(downloadJobName.get()));
|
errorsBuilder.addAll(checkBsaLabels(downloadJobName.get()));
|
||||||
|
|
||||||
emailValidationResults(downloadJobName.get(), errors.build());
|
ImmutableList<String> errors = errorsBuilder.build();
|
||||||
|
|
||||||
|
String resultSummary =
|
||||||
|
errors.isEmpty()
|
||||||
|
? "BSA validation completed: no errors found"
|
||||||
|
: "BSA validation completed with errors";
|
||||||
|
|
||||||
|
emailValidationResults(resultSummary, downloadJobName.get(), errors);
|
||||||
logger.atInfo().log("Finished validating BSA with latest download: %s", downloadJobName.get());
|
logger.atInfo().log("Finished validating BSA with latest download: %s", downloadJobName.get());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void emailValidationResults(String job, ImmutableList<String> errors) {
|
void emailValidationResults(String subject, String jobName, ImmutableList<String> results) {
|
||||||
// TODO(blocked by go/r3pr/2354): send email
|
String body =
|
||||||
|
String.format("Most recent download is %s.\n\n", jobName) + Joiner.on('\n').join(results);
|
||||||
|
emailSender.sendNotification(subject, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImmutableList<String> checkBsaLabels(String jobName) {
|
ImmutableList<String> checkBsaLabels(String jobName) {
|
||||||
|
|
|
@ -91,6 +91,7 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
|
||||||
String gcsBucket;
|
String gcsBucket;
|
||||||
|
|
||||||
String apiUrl;
|
String apiUrl;
|
||||||
|
BsaEmailSender emailSender;
|
||||||
|
|
||||||
google.registry.request.Response response;
|
google.registry.request.Response response;
|
||||||
|
|
||||||
|
@ -99,6 +100,7 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
|
||||||
Clock clock,
|
Clock clock,
|
||||||
BsaCredential bsaCredential,
|
BsaCredential bsaCredential,
|
||||||
GcsUtils gcsUtils,
|
GcsUtils gcsUtils,
|
||||||
|
BsaEmailSender emailSender,
|
||||||
@Config("bsaUnavailableDomainsGcsBucket") String gcsBucket,
|
@Config("bsaUnavailableDomainsGcsBucket") String gcsBucket,
|
||||||
@Config("bsaUploadUnavailableDomainsUrl") String apiUrl,
|
@Config("bsaUploadUnavailableDomainsUrl") String apiUrl,
|
||||||
google.registry.request.Response response) {
|
google.registry.request.Response response) {
|
||||||
|
@ -107,6 +109,7 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
|
||||||
this.gcsUtils = gcsUtils;
|
this.gcsUtils = gcsUtils;
|
||||||
this.gcsBucket = gcsBucket;
|
this.gcsBucket = gcsBucket;
|
||||||
this.apiUrl = apiUrl;
|
this.apiUrl = apiUrl;
|
||||||
|
this.emailSender = emailSender;
|
||||||
this.response = response;
|
this.response = response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,26 +121,36 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
|
||||||
String unavailableDomains = Joiner.on("\n").join(getUnavailableDomains(runTime));
|
String unavailableDomains = Joiner.on("\n").join(getUnavailableDomains(runTime));
|
||||||
if (unavailableDomains.isEmpty()) {
|
if (unavailableDomains.isEmpty()) {
|
||||||
logger.atWarning().log("No unavailable domains found; terminating.");
|
logger.atWarning().log("No unavailable domains found; terminating.");
|
||||||
|
emailSender.sendNotification(
|
||||||
|
"BSA daily upload found no domains to upload", "This is unexpected. Please investigate.");
|
||||||
} else {
|
} else {
|
||||||
uploadToGcs(unavailableDomains, runTime);
|
boolean isGcsSuccess = uploadToGcs(unavailableDomains, runTime);
|
||||||
uploadToBsa(unavailableDomains, runTime);
|
boolean isBsaSuccess = uploadToBsa(unavailableDomains, runTime);
|
||||||
|
if (isBsaSuccess && isGcsSuccess) {
|
||||||
|
emailSender.sendNotification("BSA daily upload completed successfully", "");
|
||||||
|
} else {
|
||||||
|
emailSender.sendNotification(
|
||||||
|
"BSA daily upload completed with errors", "Please see logs for details.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Uploads the unavailable domains list to GCS in the unavailable domains bucket. */
|
/** Uploads the unavailable domains list to GCS in the unavailable domains bucket. */
|
||||||
void uploadToGcs(String unavailableDomains, DateTime runTime) {
|
boolean uploadToGcs(String unavailableDomains, DateTime runTime) {
|
||||||
logger.atInfo().log("Uploading unavailable names file to GCS in bucket %s", gcsBucket);
|
logger.atInfo().log("Uploading unavailable names file to GCS in bucket %s", gcsBucket);
|
||||||
BlobId blobId = BlobId.of(gcsBucket, createFilename(runTime));
|
BlobId blobId = BlobId.of(gcsBucket, createFilename(runTime));
|
||||||
try (OutputStream gcsOutput = gcsUtils.openOutputStream(blobId);
|
try (OutputStream gcsOutput = gcsUtils.openOutputStream(blobId);
|
||||||
Writer osWriter = new OutputStreamWriter(gcsOutput, US_ASCII)) {
|
Writer osWriter = new OutputStreamWriter(gcsOutput, US_ASCII)) {
|
||||||
osWriter.write(unavailableDomains);
|
osWriter.write(unavailableDomains);
|
||||||
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.atSevere().withCause(e).log(
|
logger.atSevere().withCause(e).log(
|
||||||
"Error writing BSA unavailable domains to GCS; skipping to BSA upload ...");
|
"Error writing BSA unavailable domains to GCS; skipping to BSA upload ...");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void uploadToBsa(String unavailableDomains, DateTime runTime) {
|
boolean uploadToBsa(String unavailableDomains, DateTime runTime) {
|
||||||
try {
|
try {
|
||||||
byte[] gzippedContents = gzipUnavailableDomains(unavailableDomains);
|
byte[] gzippedContents = gzipUnavailableDomains(unavailableDomains);
|
||||||
String sha512Hash = ByteSource.wrap(gzippedContents).hash(Hashing.sha512()).toString();
|
String sha512Hash = ByteSource.wrap(gzippedContents).hash(Hashing.sha512()).toString();
|
||||||
|
@ -174,10 +187,12 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
|
||||||
uploadResponse.code(),
|
uploadResponse.code(),
|
||||||
uploadResponse.body() == null ? "(none)" : uploadResponse.body().string());
|
uploadResponse.body() == null ? "(none)" : uploadResponse.body().string());
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.atSevere().withCause(e).log("Error while attempting to upload to BSA, aborting.");
|
logger.atSevere().withCause(e).log("Error while attempting to upload to BSA, aborting.");
|
||||||
response.setStatus(HttpStatusCodes.STATUS_CODE_SERVER_ERROR);
|
response.setStatus(HttpStatusCodes.STATUS_CODE_SERVER_ERROR);
|
||||||
response.setPayload("Error while attempting to upload to BSA: " + e.getMessage());
|
response.setPayload("Error while attempting to upload to BSA: " + e.getMessage());
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import dagger.Component;
|
||||||
import dagger.Lazy;
|
import dagger.Lazy;
|
||||||
import google.registry.config.CredentialModule;
|
import google.registry.config.CredentialModule;
|
||||||
import google.registry.config.RegistryConfig.ConfigModule;
|
import google.registry.config.RegistryConfig.ConfigModule;
|
||||||
|
import google.registry.groups.GmailModule;
|
||||||
import google.registry.keyring.KeyringModule;
|
import google.registry.keyring.KeyringModule;
|
||||||
import google.registry.keyring.secretmanager.SecretManagerKeyringModule;
|
import google.registry.keyring.secretmanager.SecretManagerKeyringModule;
|
||||||
import google.registry.module.bsa.BsaRequestComponent.BsaRequestComponentModule;
|
import google.registry.module.bsa.BsaRequestComponent.BsaRequestComponentModule;
|
||||||
|
@ -39,6 +40,7 @@ import javax.inject.Singleton;
|
||||||
BsaRequestComponentModule.class,
|
BsaRequestComponentModule.class,
|
||||||
ConfigModule.class,
|
ConfigModule.class,
|
||||||
CredentialModule.class,
|
CredentialModule.class,
|
||||||
|
GmailModule.class,
|
||||||
GsonModule.class,
|
GsonModule.class,
|
||||||
PersistenceModule.class,
|
PersistenceModule.class,
|
||||||
KeyringModule.class,
|
KeyringModule.class,
|
||||||
|
|
109
core/src/test/java/google/registry/bsa/BsaRefreshActionTest.java
Normal file
109
core/src/test/java/google/registry/bsa/BsaRefreshActionTest.java
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// Copyright 2024 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.bsa;
|
||||||
|
|
||||||
|
import static com.google.common.base.Throwables.getStackTraceAsString;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import google.registry.bsa.api.BsaReportSender;
|
||||||
|
import google.registry.bsa.persistence.RefreshScheduler;
|
||||||
|
import google.registry.groups.GmailClient;
|
||||||
|
import google.registry.request.Response;
|
||||||
|
import google.registry.testing.FakeClock;
|
||||||
|
import google.registry.util.EmailMessage;
|
||||||
|
import javax.mail.internet.InternetAddress;
|
||||||
|
import org.joda.time.DateTime;
|
||||||
|
import org.joda.time.Duration;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
/** Unit tests for {@link BsaRefreshAction}. */
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class BsaRefreshActionTest {
|
||||||
|
|
||||||
|
FakeClock fakeClock = new FakeClock(DateTime.parse("2023-11-09T02:08:57.880Z"));
|
||||||
|
|
||||||
|
@Mock RefreshScheduler scheduler;
|
||||||
|
|
||||||
|
@Mock GmailClient gmailClient;
|
||||||
|
|
||||||
|
@Mock private InternetAddress emailRecipient;
|
||||||
|
|
||||||
|
@Mock Response response;
|
||||||
|
|
||||||
|
@Mock private BsaLock bsaLock;
|
||||||
|
|
||||||
|
@Mock private GcsClient gcsClient;
|
||||||
|
|
||||||
|
@Mock private BsaReportSender bsaReportSender;
|
||||||
|
|
||||||
|
BsaRefreshAction action;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() {
|
||||||
|
action =
|
||||||
|
new BsaRefreshAction(
|
||||||
|
scheduler,
|
||||||
|
gcsClient,
|
||||||
|
bsaReportSender,
|
||||||
|
/* transactionBatchSize= */ 5,
|
||||||
|
/* domainCreateTxnCommitTimeLag= */ Duration.millis(1),
|
||||||
|
new BsaEmailSender(gmailClient, emailRecipient),
|
||||||
|
bsaLock,
|
||||||
|
fakeClock,
|
||||||
|
response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void notificationSent_cannotAcquireLock() {
|
||||||
|
when(bsaLock.executeWithLock(any())).thenReturn(false);
|
||||||
|
action.run();
|
||||||
|
verify(gmailClient, times(1))
|
||||||
|
.sendEmail(
|
||||||
|
EmailMessage.create(
|
||||||
|
"BSA refresh did not run: another BSA related task is running",
|
||||||
|
"",
|
||||||
|
emailRecipient));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void notificationSent_abortedByException() {
|
||||||
|
RuntimeException throwable = new RuntimeException("Error");
|
||||||
|
when(bsaLock.executeWithLock(any())).thenThrow(throwable);
|
||||||
|
action.run();
|
||||||
|
verify(gmailClient, times(1))
|
||||||
|
.sendEmail(
|
||||||
|
EmailMessage.create(
|
||||||
|
"BSA refresh aborted", getStackTraceAsString(throwable), emailRecipient));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void notificationSent_success() {
|
||||||
|
when(bsaLock.executeWithLock(any()))
|
||||||
|
.thenAnswer(
|
||||||
|
args -> {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
action.run();
|
||||||
|
verify(gmailClient, times(1))
|
||||||
|
.sendEmail(EmailMessage.create("BSA refreshed successfully", "", emailRecipient));
|
||||||
|
}
|
||||||
|
}
|
|
@ -86,6 +86,8 @@ class BsaRefreshFunctionalTest {
|
||||||
|
|
||||||
@Mock BsaReportSender bsaReportSender;
|
@Mock BsaReportSender bsaReportSender;
|
||||||
|
|
||||||
|
@Mock BsaEmailSender emailSender;
|
||||||
|
|
||||||
private GcsClient gcsClient;
|
private GcsClient gcsClient;
|
||||||
private Response response;
|
private Response response;
|
||||||
private BsaRefreshAction action;
|
private BsaRefreshAction action;
|
||||||
|
@ -102,6 +104,7 @@ class BsaRefreshFunctionalTest {
|
||||||
bsaReportSender,
|
bsaReportSender,
|
||||||
/* transactionBatchSize= */ 5,
|
/* transactionBatchSize= */ 5,
|
||||||
/* domainCreateTxnCommitTimeLag= */ Duration.millis(1),
|
/* domainCreateTxnCommitTimeLag= */ Duration.millis(1),
|
||||||
|
emailSender,
|
||||||
new BsaLock(
|
new BsaLock(
|
||||||
new FakeLockHandler(/* lockSucceeds= */ true), Duration.standardSeconds(30)),
|
new FakeLockHandler(/* lockSucceeds= */ true), Duration.standardSeconds(30)),
|
||||||
fakeClock,
|
fakeClock,
|
||||||
|
@ -145,6 +148,8 @@ class BsaRefreshFunctionalTest {
|
||||||
verify(bsaReportSender, never()).removeUnblockableDomainsUpdates(anyString());
|
verify(bsaReportSender, never()).removeUnblockableDomainsUpdates(anyString());
|
||||||
verify(bsaReportSender, times(1))
|
verify(bsaReportSender, times(1))
|
||||||
.addUnblockableDomainsUpdates("{\n \"reserved\": [\n \"blocked1.app\"\n ]\n}");
|
.addUnblockableDomainsUpdates("{\n \"reserved\": [\n \"blocked1.app\"\n ]\n}");
|
||||||
|
|
||||||
|
verify(emailSender, times(1)).sendNotification("BSA refreshed successfully", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -14,22 +14,38 @@
|
||||||
|
|
||||||
package google.registry.bsa;
|
package google.registry.bsa;
|
||||||
|
|
||||||
|
import static com.google.common.base.Throwables.getStackTraceAsString;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.bsa.persistence.BsaTestingUtils.persistDownloadSchedule;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import com.google.cloud.storage.BlobId;
|
import com.google.cloud.storage.BlobId;
|
||||||
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
|
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import google.registry.bsa.persistence.BsaTestingUtils;
|
import google.registry.bsa.persistence.BsaTestingUtils;
|
||||||
import google.registry.gcs.GcsUtils;
|
import google.registry.gcs.GcsUtils;
|
||||||
|
import google.registry.groups.GmailClient;
|
||||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
|
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
|
||||||
import google.registry.request.Response;
|
import google.registry.request.Response;
|
||||||
import google.registry.testing.FakeClock;
|
import google.registry.testing.FakeClock;
|
||||||
|
import google.registry.util.EmailMessage;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import javax.mail.internet.InternetAddress;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ -45,19 +61,31 @@ public class BsaValidateActionTest {
|
||||||
final JpaIntegrationWithCoverageExtension jpa =
|
final JpaIntegrationWithCoverageExtension jpa =
|
||||||
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
|
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
|
||||||
|
|
||||||
@Mock BsaLock bsaLock;
|
@Mock GmailClient gmailClient;
|
||||||
|
|
||||||
@Mock Response response;
|
@Mock Response response;
|
||||||
|
|
||||||
|
@Mock private BsaLock bsaLock;
|
||||||
|
|
||||||
|
@Mock private InternetAddress emailRecipient;
|
||||||
|
|
||||||
|
@Captor ArgumentCaptor<EmailMessage> emailCaptor = ArgumentCaptor.forClass(EmailMessage.class);
|
||||||
|
|
||||||
private GcsClient gcsClient;
|
private GcsClient gcsClient;
|
||||||
|
|
||||||
private BsaValidateAction action;
|
private BsaValidateAction action;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setup() {
|
void setup() throws Exception {
|
||||||
gcsClient =
|
gcsClient =
|
||||||
new GcsClient(new GcsUtils(LocalStorageHelper.getOptions()), "my-bucket", "SHA-256");
|
new GcsClient(new GcsUtils(LocalStorageHelper.getOptions()), "my-bucket", "SHA-256");
|
||||||
action = new BsaValidateAction(gcsClient, /* transactionBatchSize= */ 500, bsaLock, response);
|
action =
|
||||||
|
new BsaValidateAction(
|
||||||
|
gcsClient,
|
||||||
|
new BsaEmailSender(gmailClient, emailRecipient),
|
||||||
|
/* transactionBatchSize= */ 500,
|
||||||
|
bsaLock,
|
||||||
|
response);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void createBlockList(GcsClient gcsClient, BlockListType blockListType, String content)
|
static void createBlockList(GcsClient gcsClient, BlockListType blockListType, String content)
|
||||||
|
@ -152,4 +180,83 @@ public class BsaValidateActionTest {
|
||||||
assertThat(allErrors).contains("Found 1 missing labels in the DB. Examples: [test1]");
|
assertThat(allErrors).contains("Found 1 missing labels in the DB. Examples: [test1]");
|
||||||
assertThat(allErrors).contains("Found 1 unexpected labels in the DB. Examples: [test3]");
|
assertThat(allErrors).contains("Found 1 unexpected labels in the DB. Examples: [test3]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void notificationSent_cannotAcquireLock() {
|
||||||
|
when(bsaLock.executeWithLock(any())).thenReturn(false);
|
||||||
|
action.run();
|
||||||
|
verify(gmailClient, times(1))
|
||||||
|
.sendEmail(
|
||||||
|
EmailMessage.create(
|
||||||
|
"BSA validation did not run: another BSA related task is running",
|
||||||
|
"",
|
||||||
|
emailRecipient));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void notificationSent_abortedByException() {
|
||||||
|
RuntimeException throwable = new RuntimeException("Error");
|
||||||
|
when(bsaLock.executeWithLock(any())).thenThrow(throwable);
|
||||||
|
action.run();
|
||||||
|
verify(gmailClient, times(1))
|
||||||
|
.sendEmail(
|
||||||
|
EmailMessage.create(
|
||||||
|
"BSA validation aborted", getStackTraceAsString(throwable), emailRecipient));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void notificationSent_noDownloads() {
|
||||||
|
when(bsaLock.executeWithLock(any()))
|
||||||
|
.thenAnswer(
|
||||||
|
args -> {
|
||||||
|
args.getArgument(0, Callable.class).call();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
action.run();
|
||||||
|
verify(gmailClient, times(1))
|
||||||
|
.sendEmail(
|
||||||
|
EmailMessage.create(
|
||||||
|
"BSA validation does not run: block list downloads not found", "", emailRecipient));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void notificationSent_withValidationError() {
|
||||||
|
when(bsaLock.executeWithLock(any()))
|
||||||
|
.thenAnswer(
|
||||||
|
args -> {
|
||||||
|
args.getArgument(0, Callable.class).call();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
persistDownloadSchedule(DownloadStage.DONE);
|
||||||
|
action = spy(action);
|
||||||
|
doReturn(ImmutableList.of("Error line 1.", "Error line 2"))
|
||||||
|
.when(action)
|
||||||
|
.checkBsaLabels(anyString());
|
||||||
|
action.run();
|
||||||
|
verify(gmailClient, times(1)).sendEmail(emailCaptor.capture());
|
||||||
|
EmailMessage message = emailCaptor.getValue();
|
||||||
|
assertThat(message.subject()).isEqualTo("BSA validation completed with errors");
|
||||||
|
assertThat(message.body()).startsWith("Most recent download is");
|
||||||
|
assertThat(message.body())
|
||||||
|
.isEqualTo(
|
||||||
|
"Most recent download is 2023-11-09t020857.880z.\n\n" + "Error line 1.\nError line 2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void notificationSent_noError() {
|
||||||
|
when(bsaLock.executeWithLock(any()))
|
||||||
|
.thenAnswer(
|
||||||
|
args -> {
|
||||||
|
args.getArgument(0, Callable.class).call();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
persistDownloadSchedule(DownloadStage.DONE);
|
||||||
|
action = spy(action);
|
||||||
|
doReturn(ImmutableList.of()).when(action).checkBsaLabels(anyString());
|
||||||
|
action.run();
|
||||||
|
verify(gmailClient, times(1)).sendEmail(emailCaptor.capture());
|
||||||
|
EmailMessage message = emailCaptor.getValue();
|
||||||
|
assertThat(message.subject()).isEqualTo("BSA validation completed: no errors found");
|
||||||
|
assertThat(message.body()).isEqualTo("Most recent download is 2023-11-09t020857.880z.\n\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ import static google.registry.testing.DatabaseHelper.persistReservedList;
|
||||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
import com.google.cloud.storage.BlobId;
|
import com.google.cloud.storage.BlobId;
|
||||||
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
|
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
|
||||||
|
@ -64,6 +66,8 @@ public class UploadBsaUnavailableDomainsActionTest {
|
||||||
|
|
||||||
@Mock BsaCredential bsaCredential;
|
@Mock BsaCredential bsaCredential;
|
||||||
|
|
||||||
|
@Mock BsaEmailSender emailSender;
|
||||||
|
|
||||||
private final GcsUtils gcsUtils = new GcsUtils(LocalStorageHelper.getOptions());
|
private final GcsUtils gcsUtils = new GcsUtils(LocalStorageHelper.getOptions());
|
||||||
|
|
||||||
private final FakeResponse response = new FakeResponse();
|
private final FakeResponse response = new FakeResponse();
|
||||||
|
@ -86,7 +90,7 @@ public class UploadBsaUnavailableDomainsActionTest {
|
||||||
.build());
|
.build());
|
||||||
action =
|
action =
|
||||||
new UploadBsaUnavailableDomainsAction(
|
new UploadBsaUnavailableDomainsAction(
|
||||||
clock, bsaCredential, gcsUtils, BUCKET, API_URL, response);
|
clock, bsaCredential, gcsUtils, emailSender, BUCKET, API_URL, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -101,6 +105,10 @@ public class UploadBsaUnavailableDomainsActionTest {
|
||||||
assertThat(blockList).isEqualTo("ace.tld\nflagrant.tld\nfoobar.tld\njimmy.tld\ntine.tld");
|
assertThat(blockList).isEqualTo("ace.tld\nflagrant.tld\nfoobar.tld\njimmy.tld\ntine.tld");
|
||||||
assertThat(blockList).doesNotContain("not-blocked.tld");
|
assertThat(blockList).doesNotContain("not-blocked.tld");
|
||||||
|
|
||||||
|
// This test currently fails in the upload-to-bsa step.
|
||||||
|
verify(emailSender, times(1))
|
||||||
|
.sendNotification("BSA daily upload completed with errors", "Please see logs for details.");
|
||||||
|
|
||||||
// TODO(mcilwain): Add test of BSA API upload as well.
|
// TODO(mcilwain): Add test of BSA API upload as well.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import google.registry.bsa.DownloadStage;
|
||||||
import google.registry.bsa.api.UnblockableDomain;
|
import google.registry.bsa.api.UnblockableDomain;
|
||||||
import google.registry.util.Clock;
|
import google.registry.util.Clock;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
|
@ -42,6 +43,11 @@ public final class BsaTestingUtils {
|
||||||
tm().transact(() -> tm().put(BsaUnblockableDomain.of(unblockableDomain)));
|
tm().transact(() -> tm().put(BsaUnblockableDomain.of(unblockableDomain)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void persistDownloadSchedule(DownloadStage stage) {
|
||||||
|
BsaDownload bsaDownload = new BsaDownload().setStage(stage);
|
||||||
|
tm().transact(() -> tm().put(bsaDownload));
|
||||||
|
}
|
||||||
|
|
||||||
public static DownloadScheduler createDownloadScheduler(Clock clock) {
|
public static DownloadScheduler createDownloadScheduler(Clock clock) {
|
||||||
return new DownloadScheduler(DEFAULT_DOWNLOAD_INTERVAL, DEFAULT_NOP_INTERVAL, clock);
|
return new DownloadScheduler(DEFAULT_DOWNLOAD_INTERVAL, DEFAULT_NOP_INTERVAL, clock);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue