diff --git a/core/src/main/java/google/registry/bsa/BsaEmailSender.java b/core/src/main/java/google/registry/bsa/BsaEmailSender.java new file mode 100644 index 00000000000..e34778d0558 --- /dev/null +++ b/core/src/main/java/google/registry/bsa/BsaEmailSender.java @@ -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)); + } +} diff --git a/core/src/main/java/google/registry/bsa/BsaRefreshAction.java b/core/src/main/java/google/registry/bsa/BsaRefreshAction.java index 14492c3be86..f50b2c7c8f7 100644 --- a/core/src/main/java/google/registry/bsa/BsaRefreshAction.java +++ b/core/src/main/java/google/registry/bsa/BsaRefreshAction.java @@ -14,6 +14,7 @@ package google.registry.bsa; +import static com.google.common.base.Throwables.getStackTraceAsString; import static google.registry.bsa.BsaStringUtils.LINE_SPLITTER; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.POST; @@ -55,6 +56,7 @@ public class BsaRefreshAction implements Runnable { private final BsaReportSender bsaReportSender; private final int transactionBatchSize; private final Duration domainCreateTxnCommitTimeLag; + private final BsaEmailSender emailSender; private final BsaLock bsaLock; private final Clock clock; private final Response response; @@ -66,6 +68,7 @@ public class BsaRefreshAction implements Runnable { BsaReportSender bsaReportSender, @Config("bsaTxnBatchSize") int transactionBatchSize, @Config("domainCreateTxnCommitTimeLag") Duration domainCreateTxnCommitTimeLag, + BsaEmailSender emailSender, BsaLock bsaLock, Clock clock, Response response) { @@ -74,6 +77,7 @@ public class BsaRefreshAction implements Runnable { this.bsaReportSender = bsaReportSender; this.transactionBatchSize = transactionBatchSize; this.domainCreateTxnCommitTimeLag = domainCreateTxnCommitTimeLag; + this.emailSender = emailSender; this.bsaLock = bsaLock; this.clock = clock; this.response = response; @@ -83,11 +87,15 @@ public class BsaRefreshAction implements Runnable { public void run() { try { 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) { - // TODO(12/31/2023): consider sending an alert email. - logger.atWarning().withCause(throwable).log("Failed to update block lists."); + logger.atWarning().withCause(throwable).log("Failed to refresh BSA data."); + emailSender.sendNotification("BSA refresh aborted", getStackTraceAsString(throwable)); } // 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. diff --git a/core/src/main/java/google/registry/bsa/BsaValidateAction.java b/core/src/main/java/google/registry/bsa/BsaValidateAction.java index f190fec5743..c642c3fda31 100644 --- a/core/src/main/java/google/registry/bsa/BsaValidateAction.java +++ b/core/src/main/java/google/registry/bsa/BsaValidateAction.java @@ -15,7 +15,8 @@ package google.registry.bsa; 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.request.Action.Method.GET; import static google.registry.request.Action.Method.POST; @@ -28,6 +29,7 @@ import com.google.common.collect.Sets; import com.google.common.collect.Sets.SetView; import com.google.common.flogger.FluentLogger; +import google.registry.bsa.persistence.DownloadScheduler; import google.registry.config.RegistryConfig.Config; import google.registry.request.Action; import google.registry.request.Response; @@ -48,6 +50,7 @@ public class BsaValidateAction implements Runnable { static final String PATH = "/_dr/task/bsaValidate"; private final GcsClient gcsClient; + private final BsaEmailSender emailSender; private final int transactionBatchSize; private final BsaLock bsaLock; private final Response response; @@ -55,10 +58,12 @@ public class BsaValidateAction implements Runnable { @Inject BsaValidateAction( GcsClient gcsClient, + BsaEmailSender emailSender, @Config("bsaTxnBatchSize") int transactionBatchSize, BsaLock bsaLock, Response response) { this.gcsClient = gcsClient; + this.emailSender = emailSender; this.transactionBatchSize = transactionBatchSize; this.bsaLock = bsaLock; this.response = response; @@ -68,12 +73,13 @@ public class BsaValidateAction implements Runnable { public void run() { try { if (!bsaLock.executeWithLock(this::runWithinLock)) { - logger.atInfo().log("Cannot execute action. Other BSA related task is executing."); - // TODO(blocked by go/r3pr/2354): send email + String message = "BSA validation did not run: another BSA related task is running"; + logger.atInfo().log("%s.", message); + emailSender.sendNotification(message, /* body= */ ""); } } catch (Throwable throwable) { - logger.atWarning().withCause(throwable).log("Failed to update block lists."); - // TODO(blocked by go/r3pr/2354): send email + logger.atWarning().withCause(throwable).log("Failed to validate block lists."); + emailSender.sendNotification("BSA validation aborted", getStackTraceAsString(throwable)); } // Always return OK. No need to retry since all queries and GCS accesses are already // implicitly retried. @@ -82,23 +88,35 @@ public void run() { /** Executes the validation action while holding the BSA lock. */ Void runWithinLock() { - Optional downloadJobName = fetchMostRecentDownloadJobIdIfCompleted(); + Optional downloadJobName = + bsaQuery(DownloadScheduler::fetchMostRecentDownloadJobIdIfCompleted); 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; } logger.atInfo().log("Validating BSA with latest download: %s", downloadJobName.get()); - ImmutableList.Builder errors = new ImmutableList.Builder(); - errors.addAll(checkBsaLabels(downloadJobName.get())); + ImmutableList.Builder errorsBuilder = new ImmutableList.Builder<>(); + errorsBuilder.addAll(checkBsaLabels(downloadJobName.get())); - emailValidationResults(downloadJobName.get(), errors.build()); + ImmutableList 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()); return null; } - void emailValidationResults(String job, ImmutableList errors) { - // TODO(blocked by go/r3pr/2354): send email + void emailValidationResults(String subject, String jobName, ImmutableList results) { + String body = + String.format("Most recent download is %s.\n\n", jobName) + Joiner.on('\n').join(results); + emailSender.sendNotification(subject, body); } ImmutableList checkBsaLabels(String jobName) { diff --git a/core/src/main/java/google/registry/bsa/UploadBsaUnavailableDomainsAction.java b/core/src/main/java/google/registry/bsa/UploadBsaUnavailableDomainsAction.java index a52ef1dfc26..43c92f89dc5 100644 --- a/core/src/main/java/google/registry/bsa/UploadBsaUnavailableDomainsAction.java +++ b/core/src/main/java/google/registry/bsa/UploadBsaUnavailableDomainsAction.java @@ -91,6 +91,7 @@ public class UploadBsaUnavailableDomainsAction implements Runnable { String gcsBucket; String apiUrl; + BsaEmailSender emailSender; google.registry.request.Response response; @@ -99,6 +100,7 @@ public UploadBsaUnavailableDomainsAction( Clock clock, BsaCredential bsaCredential, GcsUtils gcsUtils, + BsaEmailSender emailSender, @Config("bsaUnavailableDomainsGcsBucket") String gcsBucket, @Config("bsaUploadUnavailableDomainsUrl") String apiUrl, google.registry.request.Response response) { @@ -107,6 +109,7 @@ public UploadBsaUnavailableDomainsAction( this.gcsUtils = gcsUtils; this.gcsBucket = gcsBucket; this.apiUrl = apiUrl; + this.emailSender = emailSender; this.response = response; } @@ -118,26 +121,36 @@ public void run() { String unavailableDomains = Joiner.on("\n").join(getUnavailableDomains(runTime)); if (unavailableDomains.isEmpty()) { logger.atWarning().log("No unavailable domains found; terminating."); + emailSender.sendNotification( + "BSA daily upload found no domains to upload", "This is unexpected. Please investigate."); } else { - uploadToGcs(unavailableDomains, runTime); - uploadToBsa(unavailableDomains, runTime); + boolean isGcsSuccess = uploadToGcs(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. */ - 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); BlobId blobId = BlobId.of(gcsBucket, createFilename(runTime)); try (OutputStream gcsOutput = gcsUtils.openOutputStream(blobId); Writer osWriter = new OutputStreamWriter(gcsOutput, US_ASCII)) { osWriter.write(unavailableDomains); + return true; } catch (Exception e) { logger.atSevere().withCause(e).log( "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 { byte[] gzippedContents = gzipUnavailableDomains(unavailableDomains); String sha512Hash = ByteSource.wrap(gzippedContents).hash(Hashing.sha512()).toString(); @@ -174,10 +187,12 @@ void uploadToBsa(String unavailableDomains, DateTime runTime) { uploadResponse.code(), uploadResponse.body() == null ? "(none)" : uploadResponse.body().string()); } + return true; } catch (IOException e) { logger.atSevere().withCause(e).log("Error while attempting to upload to BSA, aborting."); response.setStatus(HttpStatusCodes.STATUS_CODE_SERVER_ERROR); response.setPayload("Error while attempting to upload to BSA: " + e.getMessage()); + return false; } } diff --git a/core/src/main/java/google/registry/module/bsa/BsaComponent.java b/core/src/main/java/google/registry/module/bsa/BsaComponent.java index 23154df4025..af091ebac2a 100644 --- a/core/src/main/java/google/registry/module/bsa/BsaComponent.java +++ b/core/src/main/java/google/registry/module/bsa/BsaComponent.java @@ -19,6 +19,7 @@ import dagger.Lazy; import google.registry.config.CredentialModule; import google.registry.config.RegistryConfig.ConfigModule; +import google.registry.groups.GmailModule; import google.registry.keyring.KeyringModule; import google.registry.keyring.secretmanager.SecretManagerKeyringModule; import google.registry.module.bsa.BsaRequestComponent.BsaRequestComponentModule; @@ -39,6 +40,7 @@ BsaRequestComponentModule.class, ConfigModule.class, CredentialModule.class, + GmailModule.class, GsonModule.class, PersistenceModule.class, KeyringModule.class, diff --git a/core/src/test/java/google/registry/bsa/BsaRefreshActionTest.java b/core/src/test/java/google/registry/bsa/BsaRefreshActionTest.java new file mode 100644 index 00000000000..4cce0b74ae5 --- /dev/null +++ b/core/src/test/java/google/registry/bsa/BsaRefreshActionTest.java @@ -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)); + } +} diff --git a/core/src/test/java/google/registry/bsa/BsaRefreshFunctionalTest.java b/core/src/test/java/google/registry/bsa/BsaRefreshFunctionalTest.java index f859c078ec4..2119b790f9b 100644 --- a/core/src/test/java/google/registry/bsa/BsaRefreshFunctionalTest.java +++ b/core/src/test/java/google/registry/bsa/BsaRefreshFunctionalTest.java @@ -86,6 +86,8 @@ class BsaRefreshFunctionalTest { @Mock BsaReportSender bsaReportSender; + @Mock BsaEmailSender emailSender; + private GcsClient gcsClient; private Response response; private BsaRefreshAction action; @@ -102,6 +104,7 @@ void setup() throws Exception { bsaReportSender, /* transactionBatchSize= */ 5, /* domainCreateTxnCommitTimeLag= */ Duration.millis(1), + emailSender, new BsaLock( new FakeLockHandler(/* lockSucceeds= */ true), Duration.standardSeconds(30)), fakeClock, @@ -145,6 +148,8 @@ void newReservedDomain_addedAsUnblockable() throws Exception { verify(bsaReportSender, never()).removeUnblockableDomainsUpdates(anyString()); verify(bsaReportSender, times(1)) .addUnblockableDomainsUpdates("{\n \"reserved\": [\n \"blocked1.app\"\n ]\n}"); + + verify(emailSender, times(1)).sendNotification("BSA refreshed successfully", ""); } @Test diff --git a/core/src/test/java/google/registry/bsa/BsaValidateActionTest.java b/core/src/test/java/google/registry/bsa/BsaValidateActionTest.java index 8813b82b7d2..6f6b658e77e 100644 --- a/core/src/test/java/google/registry/bsa/BsaValidateActionTest.java +++ b/core/src/test/java/google/registry/bsa/BsaValidateActionTest.java @@ -14,22 +14,38 @@ package google.registry.bsa; +import static com.google.common.base.Throwables.getStackTraceAsString; 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.contrib.nio.testing.LocalStorageHelper; import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; import google.registry.bsa.persistence.BsaTestingUtils; import google.registry.gcs.GcsUtils; +import google.registry.groups.GmailClient; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension; import google.registry.request.Response; 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.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -45,19 +61,31 @@ public class BsaValidateActionTest { final JpaIntegrationWithCoverageExtension jpa = new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension(); - @Mock BsaLock bsaLock; + @Mock GmailClient gmailClient; @Mock Response response; + @Mock private BsaLock bsaLock; + + @Mock private InternetAddress emailRecipient; + + @Captor ArgumentCaptor emailCaptor = ArgumentCaptor.forClass(EmailMessage.class); + private GcsClient gcsClient; private BsaValidateAction action; @BeforeEach - void setup() { + void setup() throws Exception { gcsClient = 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) @@ -152,4 +180,83 @@ void checkBsaLabels_withErrors() throws Exception { assertThat(allErrors).contains("Found 1 missing labels in the DB. Examples: [test1]"); 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"); + } } diff --git a/core/src/test/java/google/registry/bsa/UploadBsaUnavailableDomainsActionTest.java b/core/src/test/java/google/registry/bsa/UploadBsaUnavailableDomainsActionTest.java index 71d9e521e4c..7c078e5d4ab 100644 --- a/core/src/test/java/google/registry/bsa/UploadBsaUnavailableDomainsActionTest.java +++ b/core/src/test/java/google/registry/bsa/UploadBsaUnavailableDomainsActionTest.java @@ -22,6 +22,8 @@ import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.util.DateTimeUtils.START_OF_TIME; 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.contrib.nio.testing.LocalStorageHelper; @@ -64,6 +66,8 @@ public class UploadBsaUnavailableDomainsActionTest { @Mock BsaCredential bsaCredential; + @Mock BsaEmailSender emailSender; + private final GcsUtils gcsUtils = new GcsUtils(LocalStorageHelper.getOptions()); private final FakeResponse response = new FakeResponse(); @@ -86,7 +90,7 @@ void beforeEach() { .build()); action = new UploadBsaUnavailableDomainsAction( - clock, bsaCredential, gcsUtils, BUCKET, API_URL, response); + clock, bsaCredential, gcsUtils, emailSender, BUCKET, API_URL, response); } @Test @@ -101,6 +105,10 @@ void calculatesEntriesCorrectly() throws Exception { assertThat(blockList).isEqualTo("ace.tld\nflagrant.tld\nfoobar.tld\njimmy.tld\ntine.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. } } diff --git a/core/src/test/java/google/registry/bsa/persistence/BsaTestingUtils.java b/core/src/test/java/google/registry/bsa/persistence/BsaTestingUtils.java index 970971e2911..eb8d9b22dc2 100644 --- a/core/src/test/java/google/registry/bsa/persistence/BsaTestingUtils.java +++ b/core/src/test/java/google/registry/bsa/persistence/BsaTestingUtils.java @@ -18,6 +18,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import com.google.common.collect.ImmutableList; +import google.registry.bsa.DownloadStage; import google.registry.bsa.api.UnblockableDomain; import google.registry.util.Clock; import org.joda.time.DateTime; @@ -42,6 +43,11 @@ public static void persistUnblockableDomain(UnblockableDomain 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) { return new DownloadScheduler(DEFAULT_DOWNLOAD_INTERVAL, DEFAULT_NOP_INTERVAL, clock); }