mirror of
https://github.com/google/nomulus.git
synced 2025-05-13 07:57:13 +02:00
Add publish functionality to billing pipeline
This closes the end-to-end billing pipeline, allowing us to share generated detail reports with registrars via Drive and e-mail the invoicing team a link to the generated invoice. This also factors out the email configs from ICANN reporting into the common 'misc' config, since we'll likely need alert e-mails for future periodic tasks. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180805972
This commit is contained in:
parent
27f12b9390
commit
ab5e16ab67
25 changed files with 721 additions and 95 deletions
|
@ -9,6 +9,7 @@ java_library(
|
||||||
srcs = glob(["*.java"]),
|
srcs = glob(["*.java"]),
|
||||||
resources = glob(["sql/*"]),
|
resources = glob(["sql/*"]),
|
||||||
deps = [
|
deps = [
|
||||||
|
"//java/google/registry/billing",
|
||||||
"//java/google/registry/config",
|
"//java/google/registry/config",
|
||||||
"//java/google/registry/util",
|
"//java/google/registry/util",
|
||||||
"@com_google_apis_google_api_services_bigquery",
|
"@com_google_apis_google_api_services_bigquery",
|
||||||
|
|
|
@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import google.registry.billing.BillingModule;
|
||||||
import google.registry.util.FormattingLogger;
|
import google.registry.util.FormattingLogger;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
@ -173,7 +174,8 @@ public abstract class BillingEvent implements Serializable {
|
||||||
* filepath with the arguments, such as "../sensitive_info".
|
* filepath with the arguments, such as "../sensitive_info".
|
||||||
*/
|
*/
|
||||||
String toFilename(String yearMonth) {
|
String toFilename(String yearMonth) {
|
||||||
return String.format("invoice_details_%s_%s_%s", yearMonth, registrarId(), tld());
|
return String.format(
|
||||||
|
"%s_%s_%s_%s", BillingModule.DETAIL_REPORT_PREFIX, yearMonth, registrarId(), tld());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generates a CSV representation of this {@code BillingEvent}. */
|
/** Generates a CSV representation of this {@code BillingEvent}. */
|
||||||
|
|
|
@ -16,6 +16,7 @@ package google.registry.beam;
|
||||||
|
|
||||||
import google.registry.beam.BillingEvent.InvoiceGroupingKey;
|
import google.registry.beam.BillingEvent.InvoiceGroupingKey;
|
||||||
import google.registry.beam.BillingEvent.InvoiceGroupingKey.InvoiceGroupingKeyCoder;
|
import google.registry.beam.BillingEvent.InvoiceGroupingKey.InvoiceGroupingKeyCoder;
|
||||||
|
import google.registry.billing.BillingModule;
|
||||||
import google.registry.config.RegistryConfig.Config;
|
import google.registry.config.RegistryConfig.Config;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
@ -130,7 +131,11 @@ public class InvoicingPipeline implements Serializable {
|
||||||
.to(
|
.to(
|
||||||
NestedValueProvider.of(
|
NestedValueProvider.of(
|
||||||
yearMonthProvider,
|
yearMonthProvider,
|
||||||
yearMonth -> String.format("%s/results/CRR-INV-%s", beamBucket, yearMonth)))
|
// TODO(larryruili): Replace with billing bucket after verifying 2017-12 output.
|
||||||
|
yearMonth ->
|
||||||
|
String.format(
|
||||||
|
"%s/results/%s-%s",
|
||||||
|
beamBucket, BillingModule.OVERALL_INVOICE_PREFIX, yearMonth)))
|
||||||
.withHeader(InvoiceGroupingKey.invoiceHeader())
|
.withHeader(InvoiceGroupingKey.invoiceHeader())
|
||||||
.withoutSharding()
|
.withoutSharding()
|
||||||
.withSuffix(".csv");
|
.withSuffix(".csv");
|
||||||
|
@ -140,6 +145,7 @@ public class InvoicingPipeline implements Serializable {
|
||||||
private TextIO.TypedWrite<BillingEvent, Params> writeDetailReports(
|
private TextIO.TypedWrite<BillingEvent, Params> writeDetailReports(
|
||||||
ValueProvider<String> yearMonthProvider) {
|
ValueProvider<String> yearMonthProvider) {
|
||||||
return TextIO.<BillingEvent>writeCustomType()
|
return TextIO.<BillingEvent>writeCustomType()
|
||||||
|
// TODO(larryruili): Replace with billing bucket/yyyy-MM after verifying 2017-12 output.
|
||||||
.to(
|
.to(
|
||||||
InvoicingUtils.makeDestinationFunction(beamBucket + "/results", yearMonthProvider),
|
InvoicingUtils.makeDestinationFunction(beamBucket + "/results", yearMonthProvider),
|
||||||
InvoicingUtils.makeEmptyDestinationParams(beamBucket + "/results"))
|
InvoicingUtils.makeEmptyDestinationParams(beamBucket + "/results"))
|
||||||
|
|
|
@ -68,7 +68,7 @@ public class InvoicingUtils {
|
||||||
return new Params()
|
return new Params()
|
||||||
.withBaseFilename(
|
.withBaseFilename(
|
||||||
FileBasedSink.convertToFileResourceIfPossible(
|
FileBasedSink.convertToFileResourceIfPossible(
|
||||||
String.format("%s/%s", outputBucket, "failed")));
|
String.format("%s/%s", outputBucket, "FAILURES")));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -12,12 +12,16 @@ java_library(
|
||||||
],
|
],
|
||||||
deps = [
|
deps = [
|
||||||
"//java/google/registry/config",
|
"//java/google/registry/config",
|
||||||
|
"//java/google/registry/gcs",
|
||||||
|
"//java/google/registry/model",
|
||||||
"//java/google/registry/request",
|
"//java/google/registry/request",
|
||||||
"//java/google/registry/request/auth",
|
"//java/google/registry/request/auth",
|
||||||
|
"//java/google/registry/storage/drive",
|
||||||
"//java/google/registry/util",
|
"//java/google/registry/util",
|
||||||
"@com_google_api_client_appengine",
|
"@com_google_api_client_appengine",
|
||||||
"@com_google_apis_google_api_services_dataflow",
|
"@com_google_apis_google_api_services_dataflow",
|
||||||
"@com_google_appengine_api_1_0_sdk",
|
"@com_google_appengine_api_1_0_sdk",
|
||||||
|
"@com_google_appengine_tools_appengine_gcs_client",
|
||||||
"@com_google_dagger",
|
"@com_google_dagger",
|
||||||
"@com_google_guava",
|
"@com_google_guava",
|
||||||
"@com_google_http_client",
|
"@com_google_http_client",
|
||||||
|
|
85
java/google/registry/billing/BillingEmailUtils.java
Normal file
85
java/google/registry/billing/BillingEmailUtils.java
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
// Copyright 2017 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.billing;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import google.registry.config.RegistryConfig.Config;
|
||||||
|
import google.registry.util.FormattingLogger;
|
||||||
|
import google.registry.util.SendEmailService;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.mail.Message;
|
||||||
|
import javax.mail.Message.RecipientType;
|
||||||
|
import javax.mail.MessagingException;
|
||||||
|
import javax.mail.internet.InternetAddress;
|
||||||
|
import org.joda.time.YearMonth;
|
||||||
|
|
||||||
|
/** Utility functions for sending emails involving monthly invoices. */
|
||||||
|
class BillingEmailUtils {
|
||||||
|
|
||||||
|
private final SendEmailService emailService;
|
||||||
|
private final YearMonth yearMonth;
|
||||||
|
private final String alertSenderAddress;
|
||||||
|
private final ImmutableList<String> invoiceEmailRecipients;
|
||||||
|
// TODO(larryruili): Replace this bucket after verifying 2017-12 output.
|
||||||
|
private final String beamBucketUrl;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
BillingEmailUtils(
|
||||||
|
SendEmailService emailService,
|
||||||
|
YearMonth yearMonth,
|
||||||
|
@Config("alertSenderEmailAddress") String alertSenderAddress,
|
||||||
|
@Config("invoiceEmailRecipients") ImmutableList<String> invoiceEmailRecipients,
|
||||||
|
@Config("apacheBeamBucketUrl") String beamBucketUrl) {
|
||||||
|
this.emailService = emailService;
|
||||||
|
this.yearMonth = yearMonth;
|
||||||
|
this.alertSenderAddress = alertSenderAddress;
|
||||||
|
this.invoiceEmailRecipients = invoiceEmailRecipients;
|
||||||
|
this.beamBucketUrl = beamBucketUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a link to the generated overall invoice in GCS.
|
||||||
|
*
|
||||||
|
* <p>Note the users receiving the e-mail should have access to the object or bucket, via an
|
||||||
|
* authorization mechanism such as IAM.
|
||||||
|
*/
|
||||||
|
void emailInvoiceLink() {
|
||||||
|
// TODO(larryruili): Add read permissions for appropriate buckets.
|
||||||
|
try {
|
||||||
|
String beamBucket = beamBucketUrl.replaceFirst("gs://", "");
|
||||||
|
Message msg = emailService.createMessage();
|
||||||
|
msg.setFrom(new InternetAddress(alertSenderAddress));
|
||||||
|
for (String recipient : invoiceEmailRecipients) {
|
||||||
|
msg.addRecipient(RecipientType.TO, new InternetAddress(recipient));
|
||||||
|
}
|
||||||
|
msg.setSubject(String.format("Domain Registry invoice data %s", yearMonth.toString()));
|
||||||
|
msg.setText(
|
||||||
|
String.format(
|
||||||
|
"Link to invoice on GCS:\nhttps://storage.cloud.google.com/%s/%s",
|
||||||
|
beamBucket,
|
||||||
|
String.format(
|
||||||
|
"%s%s-%s.csv",
|
||||||
|
BillingModule.RESULTS_DIRECTORY_PREFIX,
|
||||||
|
BillingModule.OVERALL_INVOICE_PREFIX,
|
||||||
|
yearMonth.toString())));
|
||||||
|
emailService.sendMessage(msg);
|
||||||
|
} catch (MessagingException e) {
|
||||||
|
// TODO(larryruili): Replace with retrier with final failure email settings.
|
||||||
|
logger.warning(e, "E-mail service failed due to %s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,10 +33,18 @@ import javax.servlet.http.HttpServletRequest;
|
||||||
@Module
|
@Module
|
||||||
public final class BillingModule {
|
public final class BillingModule {
|
||||||
|
|
||||||
|
public static final String DETAIL_REPORT_PREFIX = "invoice_details";
|
||||||
|
public static final String OVERALL_INVOICE_PREFIX = "CRR-INV";
|
||||||
|
|
||||||
|
static final String PARAM_JOB_ID = "jobId";
|
||||||
|
static final String PARAM_DIRECTORY_PREFIX = "directoryPrefix";
|
||||||
|
static final String BILLING_QUEUE = "billing";
|
||||||
|
static final String CRON_QUEUE = "retryable-cron-tasks";
|
||||||
|
// TODO(larryruili): Replace with invoices/yyyy-MM after verifying 2017-12 invoice.
|
||||||
|
static final String RESULTS_DIRECTORY_PREFIX = "results/";
|
||||||
|
|
||||||
private static final String CLOUD_PLATFORM_SCOPE =
|
private static final String CLOUD_PLATFORM_SCOPE =
|
||||||
"https://www.googleapis.com/auth/cloud-platform";
|
"https://www.googleapis.com/auth/cloud-platform";
|
||||||
static final String BILLING_QUEUE = "billing";
|
|
||||||
static final String PARAM_JOB_ID = "jobId";
|
|
||||||
|
|
||||||
/** Provides the invoicing Dataflow jobId enqueued by {@link GenerateInvoicesAction}. */
|
/** Provides the invoicing Dataflow jobId enqueued by {@link GenerateInvoicesAction}. */
|
||||||
@Provides
|
@Provides
|
||||||
|
@ -45,6 +53,13 @@ public final class BillingModule {
|
||||||
return extractRequiredParameter(req, PARAM_JOB_ID);
|
return extractRequiredParameter(req, PARAM_JOB_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Provides the subdirectory under a GCS bucket that we copy detail reports from. */
|
||||||
|
@Provides
|
||||||
|
@Parameter(PARAM_DIRECTORY_PREFIX)
|
||||||
|
static String provideDirectoryPrefix(HttpServletRequest req) {
|
||||||
|
return extractRequiredParameter(req, PARAM_DIRECTORY_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
/** Constructs a {@link Dataflow} API client with default settings. */
|
/** Constructs a {@link Dataflow} API client with default settings. */
|
||||||
@Provides
|
@Provides
|
||||||
static Dataflow provideDataflow(
|
static Dataflow provideDataflow(
|
||||||
|
|
129
java/google/registry/billing/CopyDetailReportsAction.java
Normal file
129
java/google/registry/billing/CopyDetailReportsAction.java
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
// Copyright 2017 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.billing;
|
||||||
|
|
||||||
|
import static google.registry.request.Action.Method.POST;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||||
|
|
||||||
|
import com.google.appengine.tools.cloudstorage.GcsFilename;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.io.ByteStreams;
|
||||||
|
import com.google.common.net.MediaType;
|
||||||
|
import google.registry.config.RegistryConfig.Config;
|
||||||
|
import google.registry.gcs.GcsUtils;
|
||||||
|
import google.registry.model.registrar.Registrar;
|
||||||
|
import google.registry.request.Action;
|
||||||
|
import google.registry.request.Parameter;
|
||||||
|
import google.registry.request.Response;
|
||||||
|
import google.registry.request.auth.Auth;
|
||||||
|
import google.registry.storage.drive.DriveConnection;
|
||||||
|
import google.registry.util.FormattingLogger;
|
||||||
|
import google.registry.util.Retrier;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Optional;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
/** Copy all registrar detail reports in a given bucket's subdirectory from GCS to Drive. */
|
||||||
|
@Action(path = CopyDetailReportsAction.PATH, method = POST, auth = Auth.AUTH_INTERNAL_OR_ADMIN)
|
||||||
|
public final class CopyDetailReportsAction implements Runnable {
|
||||||
|
|
||||||
|
public static final String PATH = "/_dr/task/copyDetailReports";
|
||||||
|
|
||||||
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
|
||||||
|
// TODO(larryruili): Replace this bucket with the billing bucket after verifying 2017-12 output.
|
||||||
|
private final String beamBucketUrl;
|
||||||
|
private final String folderPrefix;
|
||||||
|
private final DriveConnection driveConnection;
|
||||||
|
private final GcsUtils gcsUtils;
|
||||||
|
private final Retrier retrier;
|
||||||
|
private final Response response;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CopyDetailReportsAction(
|
||||||
|
@Config("apacheBeamBucketUrl") String beamBucketUrl,
|
||||||
|
@Parameter(BillingModule.PARAM_DIRECTORY_PREFIX) String folderPrefix,
|
||||||
|
DriveConnection driveConnection,
|
||||||
|
GcsUtils gcsUtils,
|
||||||
|
Retrier retrier,
|
||||||
|
Response response) {
|
||||||
|
this.beamBucketUrl = beamBucketUrl;
|
||||||
|
this.folderPrefix = folderPrefix;
|
||||||
|
this.driveConnection = driveConnection;
|
||||||
|
this.gcsUtils = gcsUtils;
|
||||||
|
this.retrier = retrier;
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Strip the URL prefix from the beam bucket
|
||||||
|
String beamBucket = beamBucketUrl.replace("gs://", "");
|
||||||
|
ImmutableList<String> detailReportObjectNames;
|
||||||
|
try {
|
||||||
|
detailReportObjectNames =
|
||||||
|
gcsUtils
|
||||||
|
.listFolderObjects(beamBucket, folderPrefix)
|
||||||
|
.stream()
|
||||||
|
.filter(objectName -> objectName.startsWith(BillingModule.DETAIL_REPORT_PREFIX))
|
||||||
|
.collect(ImmutableList.toImmutableList());
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.severefmt("Copy failed due to %s", e.getMessage());
|
||||||
|
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||||
|
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||||
|
response.setPayload(String.format("Failure, encountered %s", e.getMessage()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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 = detailReportName.split("_")[3];
|
||||||
|
Optional<Registrar> registrar = Registrar.loadByClientId(registrarId);
|
||||||
|
// TODO(larryruili): Send an email alert if any report fails to be copied for any reason.
|
||||||
|
if (!registrar.isPresent()) {
|
||||||
|
logger.warningfmt(
|
||||||
|
"Registrar %s not found in database for file %s", registrar, detailReportName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String driveFolderId = registrar.get().getDriveFolderId();
|
||||||
|
if (driveFolderId == null) {
|
||||||
|
logger.warningfmt("Drive folder id not found for registrar %s", registrarId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Attempt to copy each detail report to its associated registrar's drive folder.
|
||||||
|
retrier.callWithRetry(
|
||||||
|
() -> {
|
||||||
|
try (InputStream input =
|
||||||
|
gcsUtils.openInputStream(
|
||||||
|
new GcsFilename(beamBucket, folderPrefix + detailReportName))) {
|
||||||
|
driveConnection.createFile(
|
||||||
|
detailReportName,
|
||||||
|
MediaType.CSV_UTF_8,
|
||||||
|
driveFolderId,
|
||||||
|
ByteStreams.toByteArray(input));
|
||||||
|
logger.infofmt(
|
||||||
|
"Published detail report for %s to folder %s using GCS file gs://%s/%s.",
|
||||||
|
registrarId, driveFolderId, beamBucket, detailReportName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IOException.class);
|
||||||
|
}
|
||||||
|
response.setStatus(SC_OK);
|
||||||
|
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||||
|
response.setPayload("Copied detail reports.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,23 +49,28 @@ public class GenerateInvoicesAction implements Runnable {
|
||||||
|
|
||||||
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
|
||||||
@Inject
|
|
||||||
@Config("projectId")
|
|
||||||
String projectId;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@Config("apacheBeamBucketUrl")
|
|
||||||
String beamBucketUrl;
|
|
||||||
|
|
||||||
@Inject YearMonth yearMonth;
|
|
||||||
@Inject Dataflow dataflow;
|
|
||||||
@Inject Response response;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
GenerateInvoicesAction() {}
|
|
||||||
|
|
||||||
static final String PATH = "/_dr/task/generateInvoices";
|
static final String PATH = "/_dr/task/generateInvoices";
|
||||||
|
|
||||||
|
private final String projectId;
|
||||||
|
private final String beamBucketUrl;
|
||||||
|
private final YearMonth yearMonth;
|
||||||
|
private final Dataflow dataflow;
|
||||||
|
private final Response response;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
GenerateInvoicesAction(
|
||||||
|
@Config("projectId") String projectId,
|
||||||
|
@Config("apacheBeamBucketUrl") String beamBucketUrl,
|
||||||
|
YearMonth yearMonth,
|
||||||
|
Dataflow dataflow,
|
||||||
|
Response response) {
|
||||||
|
this.projectId = projectId;
|
||||||
|
this.beamBucketUrl = beamBucketUrl;
|
||||||
|
this.yearMonth = yearMonth;
|
||||||
|
this.dataflow = dataflow;
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
logger.infofmt("Launching invoicing pipeline for %s", yearMonth);
|
logger.infofmt("Launching invoicing pipeline for %s", yearMonth);
|
||||||
|
@ -87,13 +92,7 @@ public class GenerateInvoicesAction implements Runnable {
|
||||||
.execute();
|
.execute();
|
||||||
logger.infofmt("Got response: %s", launchResponse.getJob().toPrettyString());
|
logger.infofmt("Got response: %s", launchResponse.getJob().toPrettyString());
|
||||||
String jobId = launchResponse.getJob().getId();
|
String jobId = launchResponse.getJob().getId();
|
||||||
TaskOptions uploadTask =
|
enqueuePublishTask(jobId);
|
||||||
TaskOptions.Builder.withUrl(PublishInvoicesAction.PATH)
|
|
||||||
.method(TaskOptions.Method.POST)
|
|
||||||
// Dataflow jobs tend to take about 10 minutes to complete.
|
|
||||||
.countdownMillis(Duration.standardMinutes(10).getMillis())
|
|
||||||
.param(BillingModule.PARAM_JOB_ID, jobId);
|
|
||||||
QueueFactory.getQueue(BillingModule.BILLING_QUEUE).add(uploadTask);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.warningfmt("Template Launch failed due to: %s", e.getMessage());
|
logger.warningfmt("Template Launch failed due to: %s", e.getMessage());
|
||||||
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||||
|
@ -105,4 +104,14 @@ public class GenerateInvoicesAction implements Runnable {
|
||||||
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||||
response.setPayload("Launched dataflow template.");
|
response.setPayload("Launched dataflow template.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void enqueuePublishTask(String jobId) {
|
||||||
|
TaskOptions publishTask =
|
||||||
|
TaskOptions.Builder.withUrl(PublishInvoicesAction.PATH)
|
||||||
|
.method(TaskOptions.Method.POST)
|
||||||
|
// Dataflow jobs tend to take about 10 minutes to complete.
|
||||||
|
.countdownMillis(Duration.standardMinutes(10).getMillis())
|
||||||
|
.param(BillingModule.PARAM_JOB_ID, jobId);
|
||||||
|
QueueFactory.getQueue(BillingModule.BILLING_QUEUE).add(publishTask);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||||
|
|
||||||
import com.google.api.services.dataflow.Dataflow;
|
import com.google.api.services.dataflow.Dataflow;
|
||||||
import com.google.api.services.dataflow.model.Job;
|
import com.google.api.services.dataflow.model.Job;
|
||||||
|
import com.google.appengine.api.taskqueue.QueueFactory;
|
||||||
|
import com.google.appengine.api.taskqueue.TaskOptions;
|
||||||
import com.google.common.net.MediaType;
|
import com.google.common.net.MediaType;
|
||||||
import google.registry.config.RegistryConfig.Config;
|
import google.registry.config.RegistryConfig.Config;
|
||||||
import google.registry.request.Action;
|
import google.registry.request.Action;
|
||||||
|
@ -48,31 +50,45 @@ public class PublishInvoicesAction implements Runnable {
|
||||||
private static final String JOB_DONE = "JOB_STATE_DONE";
|
private static final String JOB_DONE = "JOB_STATE_DONE";
|
||||||
private static final String JOB_FAILED = "JOB_STATE_FAILED";
|
private static final String JOB_FAILED = "JOB_STATE_FAILED";
|
||||||
|
|
||||||
@Inject @Config("projectId") String projectId;
|
private final String projectId;
|
||||||
@Inject @Parameter(BillingModule.PARAM_JOB_ID) String jobId;
|
private final String jobId;
|
||||||
@Inject Dataflow dataflow;
|
private final BillingEmailUtils emailUtils;
|
||||||
@Inject Response response;
|
private final Dataflow dataflow;
|
||||||
@Inject PublishInvoicesAction() {}
|
private final Response response;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
PublishInvoicesAction(
|
||||||
|
@Config("projectId") String projectId,
|
||||||
|
@Parameter(BillingModule.PARAM_JOB_ID) String jobId,
|
||||||
|
BillingEmailUtils emailUtils,
|
||||||
|
Dataflow dataflow,
|
||||||
|
Response response) {
|
||||||
|
this.projectId = projectId;
|
||||||
|
this.jobId = jobId;
|
||||||
|
this.emailUtils = emailUtils;
|
||||||
|
this.dataflow = dataflow;
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
|
||||||
static final String PATH = "/_dr/task/publishInvoices";
|
static final String PATH = "/_dr/task/publishInvoices";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
logger.info("Starting publish job.");
|
|
||||||
try {
|
try {
|
||||||
|
logger.info("Starting publish job.");
|
||||||
Job job = dataflow.projects().jobs().get(projectId, jobId).execute();
|
Job job = dataflow.projects().jobs().get(projectId, jobId).execute();
|
||||||
String state = job.getCurrentState();
|
String state = job.getCurrentState();
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case JOB_DONE:
|
case JOB_DONE:
|
||||||
logger.infofmt("Dataflow job %s finished successfully.", jobId);
|
logger.infofmt("Dataflow job %s finished successfully, publishing results.", jobId);
|
||||||
response.setStatus(SC_OK);
|
response.setStatus(SC_OK);
|
||||||
// TODO(larryruili): Implement upload logic.
|
enqueueCopyDetailReportsTask();
|
||||||
|
emailUtils.emailInvoiceLink();
|
||||||
break;
|
break;
|
||||||
case JOB_FAILED:
|
case JOB_FAILED:
|
||||||
logger.severefmt("Dataflow job %s finished unsuccessfully.", jobId);
|
logger.severefmt("Dataflow job %s finished unsuccessfully.", jobId);
|
||||||
// Return a 'success' code to stop task queue retry.
|
|
||||||
response.setStatus(SC_NO_CONTENT);
|
response.setStatus(SC_NO_CONTENT);
|
||||||
// TODO(larryruili): Implement failure response.
|
// TODO(larryruili): Email failure message
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
logger.infofmt("Job in non-terminal state %s, retrying:", state);
|
logger.infofmt("Job in non-terminal state %s, retrying:", state);
|
||||||
|
@ -80,10 +96,18 @@ public class PublishInvoicesAction implements Runnable {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.warningfmt("Template Launch failed due to: %s", e.getMessage());
|
|
||||||
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||||
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||||
response.setPayload(String.format("Template launch failed: %s", e.getMessage()));
|
response.setPayload(String.format("Template launch failed: %s", e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void enqueueCopyDetailReportsTask() {
|
||||||
|
TaskOptions copyDetailTask =
|
||||||
|
TaskOptions.Builder.withUrl(CopyDetailReportsAction.PATH)
|
||||||
|
.method(TaskOptions.Method.POST)
|
||||||
|
.param(
|
||||||
|
BillingModule.PARAM_DIRECTORY_PREFIX, BillingModule.RESULTS_DIRECTORY_PREFIX);
|
||||||
|
QueueFactory.getQueue(BillingModule.CRON_QUEUE).add(copyDetailTask);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -509,27 +509,15 @@ public final class RegistryConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the email address from which we send ICANN reporting email summaries from.
|
* Returns the list of addresses that receive monthly invoicing emails.
|
||||||
*
|
*
|
||||||
* @see google.registry.reporting.ReportingEmailUtils
|
* @see google.registry.billing.BillingEmailUtils
|
||||||
*/
|
*/
|
||||||
@Provides
|
@Provides
|
||||||
@Config("icannReportingSenderEmailAddress")
|
@Config("invoiceEmailRecipients")
|
||||||
public static String provideIcannReportingEmailSenderAddress(
|
public static ImmutableList<String> provideInvoiceEmailRecipients(
|
||||||
@Config("projectId") String projectId, RegistryConfigSettings config) {
|
RegistryConfigSettings config) {
|
||||||
return String.format(
|
return ImmutableList.copyOf(config.billing.invoiceEmailRecipients);
|
||||||
"%s@%s", projectId, config.icannReporting.icannReportingEmailSenderDomain);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the email address from which we send ICANN reporting email summaries to.
|
|
||||||
*
|
|
||||||
* @see google.registry.reporting.ReportingEmailUtils
|
|
||||||
*/
|
|
||||||
@Provides
|
|
||||||
@Config("icannReportingRecipientEmailAddress")
|
|
||||||
public static String provideIcannReportingEmailRecipientAddress(RegistryConfigSettings config) {
|
|
||||||
return config.icannReporting.icannReportingEmailRecipient;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -712,6 +700,35 @@ public final class RegistryConfig {
|
||||||
return Optional.ofNullable(config.misc.sheetExportId);
|
return Optional.ofNullable(config.misc.sheetExportId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the email address we send various alert e-mails to.
|
||||||
|
*
|
||||||
|
* <p>This allows us to easily verify the success or failure of periodic tasks by passively
|
||||||
|
* checking e-mail.
|
||||||
|
*
|
||||||
|
* @see google.registry.reporting.ReportingEmailUtils
|
||||||
|
* @see google.registry.billing.BillingEmailUtils
|
||||||
|
*/
|
||||||
|
@Provides
|
||||||
|
@Config("alertRecipientEmailAddress")
|
||||||
|
public static String provideAlertRecipientEmailAddress(RegistryConfigSettings config) {
|
||||||
|
return config.misc.alertRecipientEmailAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the email address we send emails from.
|
||||||
|
*
|
||||||
|
* @see google.registry.reporting.ReportingEmailUtils
|
||||||
|
* @see google.registry.billing.BillingEmailUtils
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Config("alertSenderEmailAddress")
|
||||||
|
public static String provideAlertSenderEmailAddress(
|
||||||
|
@Config("projectId") String projectId, RegistryConfigSettings config) {
|
||||||
|
return String.format("%s@%s", projectId, config.misc.alertEmailSenderDomain);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns SSH client connection and read timeout.
|
* Returns SSH client connection and read timeout.
|
||||||
*
|
*
|
||||||
|
|
|
@ -28,6 +28,7 @@ public class RegistryConfigSettings {
|
||||||
public CloudDns cloudDns;
|
public CloudDns cloudDns;
|
||||||
public Caching caching;
|
public Caching caching;
|
||||||
public IcannReporting icannReporting;
|
public IcannReporting icannReporting;
|
||||||
|
public Billing billing;
|
||||||
public Rde rde;
|
public Rde rde;
|
||||||
public RegistrarConsole registrarConsole;
|
public RegistrarConsole registrarConsole;
|
||||||
public Monitoring monitoring;
|
public Monitoring monitoring;
|
||||||
|
@ -114,8 +115,11 @@ public class RegistryConfigSettings {
|
||||||
public static class IcannReporting {
|
public static class IcannReporting {
|
||||||
public String icannTransactionsReportingUploadUrl;
|
public String icannTransactionsReportingUploadUrl;
|
||||||
public String icannActivityReportingUploadUrl;
|
public String icannActivityReportingUploadUrl;
|
||||||
public String icannReportingEmailSenderDomain;
|
}
|
||||||
public String icannReportingEmailRecipient;
|
|
||||||
|
/** Configuration for monthly invoices. */
|
||||||
|
public static class Billing {
|
||||||
|
public List<String> invoiceEmailRecipients;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Configuration for Registry Data Escrow (RDE). */
|
/** Configuration for Registry Data Escrow (RDE). */
|
||||||
|
@ -145,6 +149,8 @@ public class RegistryConfigSettings {
|
||||||
/** Miscellaneous configuration that doesn't quite fit in anywhere else. */
|
/** Miscellaneous configuration that doesn't quite fit in anywhere else. */
|
||||||
public static class Misc {
|
public static class Misc {
|
||||||
public String sheetExportId;
|
public String sheetExportId;
|
||||||
|
public String alertRecipientEmailAddress;
|
||||||
|
public String alertEmailSenderDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Configuration for Braintree credit card payment processing. */
|
/** Configuration for Braintree credit card payment processing. */
|
||||||
|
|
|
@ -160,11 +160,8 @@ icannReporting:
|
||||||
# URL we PUT monthly ICANN activity reports to.
|
# URL we PUT monthly ICANN activity reports to.
|
||||||
icannActivityReportingUploadUrl: https://ry-api.icann.org/report/registry-functions-activity
|
icannActivityReportingUploadUrl: https://ry-api.icann.org/report/registry-functions-activity
|
||||||
|
|
||||||
# Domain for the email address we send reporting pipeline summary emails from.
|
billing:
|
||||||
icannReportingEmailSenderDomain: appspotmail.com
|
invoiceEmailRecipients: []
|
||||||
|
|
||||||
# Address we send reporting pipeline summary emails to.
|
|
||||||
icannReportingEmailRecipient: email@example.com
|
|
||||||
|
|
||||||
rde:
|
rde:
|
||||||
# URL prefix of ICANN's server to upload RDE reports to. Nomulus adds /TLD/ID
|
# URL prefix of ICANN's server to upload RDE reports to. Nomulus adds /TLD/ID
|
||||||
|
@ -215,6 +212,12 @@ misc:
|
||||||
# to. Leave this null to disable syncing.
|
# to. Leave this null to disable syncing.
|
||||||
sheetExportId: null
|
sheetExportId: null
|
||||||
|
|
||||||
|
# Address we send alert summary emails to.
|
||||||
|
alertRecipientEmailAddress: email@example.com
|
||||||
|
|
||||||
|
# Domain for the email address we send alert summary emails from.
|
||||||
|
alertEmailSenderDomain: appspotmail.com
|
||||||
|
|
||||||
# Braintree is a credit card payment processor that is used on the registrar
|
# Braintree is a credit card payment processor that is used on the registrar
|
||||||
# console to allow registrars to pay their invoices.
|
# console to allow registrars to pay their invoices.
|
||||||
braintree:
|
braintree:
|
||||||
|
|
|
@ -77,6 +77,14 @@
|
||||||
<url-pattern>/_dr/task/publishInvoices</url-pattern>
|
<url-pattern>/_dr/task/publishInvoices</url-pattern>
|
||||||
</servlet-mapping>
|
</servlet-mapping>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Copies invoice detail reports from GCS to the associated registrar's Drive folder.
|
||||||
|
-->
|
||||||
|
<servlet-mapping>
|
||||||
|
<servlet-name>backend-servlet</servlet-name>
|
||||||
|
<url-pattern>/_dr/task/copyDetailReports</url-pattern>
|
||||||
|
</servlet-mapping>
|
||||||
|
|
||||||
<!-- ICANN Monthly Reporting -->
|
<!-- ICANN Monthly Reporting -->
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
|
@ -37,12 +37,19 @@ import java.io.InputStream;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
/** Publish a single registrar detail report from GCS to Drive. */
|
/**
|
||||||
|
* Publish a single registrar detail report from GCS to Drive.
|
||||||
|
*
|
||||||
|
* <p>This is now DEPRECATED, and will be removed upon completion of the billing migration. If you
|
||||||
|
* wish to use the functionality, use {@link google.registry.billing.CopyDetailReportsAction}
|
||||||
|
* instead.
|
||||||
|
*/
|
||||||
@Action(
|
@Action(
|
||||||
path = PublishDetailReportAction.PATH,
|
path = PublishDetailReportAction.PATH,
|
||||||
method = Action.Method.POST,
|
method = Action.Method.POST,
|
||||||
auth = Auth.AUTH_INTERNAL_OR_ADMIN
|
auth = Auth.AUTH_INTERNAL_OR_ADMIN
|
||||||
)
|
)
|
||||||
|
@Deprecated
|
||||||
public final class PublishDetailReportAction implements Runnable, JsonAction {
|
public final class PublishDetailReportAction implements Runnable, JsonAction {
|
||||||
|
|
||||||
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
|
|
@ -21,7 +21,10 @@ import com.google.appengine.tools.cloudstorage.GcsFileOptions;
|
||||||
import com.google.appengine.tools.cloudstorage.GcsFileOptions.Builder;
|
import com.google.appengine.tools.cloudstorage.GcsFileOptions.Builder;
|
||||||
import com.google.appengine.tools.cloudstorage.GcsFilename;
|
import com.google.appengine.tools.cloudstorage.GcsFilename;
|
||||||
import com.google.appengine.tools.cloudstorage.GcsService;
|
import com.google.appengine.tools.cloudstorage.GcsService;
|
||||||
|
import com.google.appengine.tools.cloudstorage.ListOptions;
|
||||||
|
import com.google.appengine.tools.cloudstorage.ListResult;
|
||||||
import com.google.common.base.Splitter;
|
import com.google.common.base.Splitter;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.net.MediaType;
|
import com.google.common.net.MediaType;
|
||||||
import google.registry.config.RegistryConfig.Config;
|
import google.registry.config.RegistryConfig.Config;
|
||||||
|
@ -72,6 +75,30 @@ public class GcsUtils {
|
||||||
gcsService.createOrReplace(filename, getOptions(filename), ByteBuffer.wrap(bytes));
|
gcsService.createOrReplace(filename, getOptions(filename), ByteBuffer.wrap(bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all object names within a bucket for a given prefix.
|
||||||
|
*
|
||||||
|
* <p>Note this also strips the provided prefix from the object, leaving only the object name
|
||||||
|
* (i.e. if the bucket hello contains gs://hello/world/myobj.txt and gs://hello/world/a/b.csv,
|
||||||
|
* then listFolderObjects("hello", "world/") would return {"myobj.txt", "a/b.csv"}.
|
||||||
|
*
|
||||||
|
* @param bucketName the name of the bucket, with no gs:// namespace or trailing slashes
|
||||||
|
* @param prefix the string prefix all objects in the bucket should start with
|
||||||
|
*/
|
||||||
|
public ImmutableList<String> listFolderObjects(String bucketName, String prefix)
|
||||||
|
throws IOException {
|
||||||
|
ListResult result =
|
||||||
|
gcsService.list(bucketName, new ListOptions.Builder().setPrefix(prefix).build());
|
||||||
|
final ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
|
||||||
|
result.forEachRemaining(
|
||||||
|
listItem -> {
|
||||||
|
if (!listItem.isDirectory()) {
|
||||||
|
builder.add(listItem.getName().replaceFirst(prefix, ""));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns {@code true} if a file exists and is non-empty on Google Cloud Storage. */
|
/** Returns {@code true} if a file exists and is non-empty on Google Cloud Storage. */
|
||||||
public boolean existsAndNotEmpty(GcsFilename file) {
|
public boolean existsAndNotEmpty(GcsFilename file) {
|
||||||
GcsFileMetadata metadata;
|
GcsFileMetadata metadata;
|
||||||
|
|
|
@ -29,6 +29,7 @@ import google.registry.batch.RefreshDnsOnHostRenameAction;
|
||||||
import google.registry.batch.ResaveAllEppResourcesAction;
|
import google.registry.batch.ResaveAllEppResourcesAction;
|
||||||
import google.registry.batch.VerifyEntityIntegrityAction;
|
import google.registry.batch.VerifyEntityIntegrityAction;
|
||||||
import google.registry.billing.BillingModule;
|
import google.registry.billing.BillingModule;
|
||||||
|
import google.registry.billing.CopyDetailReportsAction;
|
||||||
import google.registry.billing.GenerateInvoicesAction;
|
import google.registry.billing.GenerateInvoicesAction;
|
||||||
import google.registry.billing.PublishInvoicesAction;
|
import google.registry.billing.PublishInvoicesAction;
|
||||||
import google.registry.cron.CommitLogFanoutAction;
|
import google.registry.cron.CommitLogFanoutAction;
|
||||||
|
@ -112,6 +113,7 @@ interface BackendRequestComponent {
|
||||||
CheckSnapshotAction checkSnapshotAction();
|
CheckSnapshotAction checkSnapshotAction();
|
||||||
CommitLogCheckpointAction commitLogCheckpointAction();
|
CommitLogCheckpointAction commitLogCheckpointAction();
|
||||||
CommitLogFanoutAction commitLogFanoutAction();
|
CommitLogFanoutAction commitLogFanoutAction();
|
||||||
|
CopyDetailReportsAction copyDetailReportAction();
|
||||||
DeleteContactsAndHostsAction deleteContactsAndHostsAction();
|
DeleteContactsAndHostsAction deleteContactsAndHostsAction();
|
||||||
DeleteOldCommitLogsAction deleteOldCommitLogsAction();
|
DeleteOldCommitLogsAction deleteOldCommitLogsAction();
|
||||||
DeleteProberDataAction deleteProberDataAction();
|
DeleteProberDataAction deleteProberDataAction();
|
||||||
|
|
|
@ -25,8 +25,8 @@ import javax.mail.internet.InternetAddress;
|
||||||
/** Static utils for emailing reporting results. */
|
/** Static utils for emailing reporting results. */
|
||||||
public class ReportingEmailUtils {
|
public class ReportingEmailUtils {
|
||||||
|
|
||||||
@Inject @Config("icannReportingSenderEmailAddress") String sender;
|
@Inject @Config("alertSenderEmailAddress") String sender;
|
||||||
@Inject @Config("icannReportingRecipientEmailAddress") String recipient;
|
@Inject @Config("alertRecipientEmailAddress") String recipient;
|
||||||
@Inject SendEmailService emailService;
|
@Inject SendEmailService emailService;
|
||||||
@Inject ReportingEmailUtils() {}
|
@Inject ReportingEmailUtils() {}
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ public class InvoicingUtilsTest {
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
new Params()
|
new Params()
|
||||||
.withBaseFilename(
|
.withBaseFilename(
|
||||||
FileBasedSink.convertToFileResourceIfPossible("my/directory/failed")));
|
FileBasedSink.convertToFileResourceIfPossible("my/directory/FAILURES")));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Asserts that the instantiated sql template matches a golden expected file. */
|
/** Asserts that the instantiated sql template matches a golden expected file. */
|
||||||
|
|
|
@ -12,8 +12,13 @@ java_library(
|
||||||
srcs = glob(["*.java"]),
|
srcs = glob(["*.java"]),
|
||||||
deps = [
|
deps = [
|
||||||
"//java/google/registry/billing",
|
"//java/google/registry/billing",
|
||||||
|
"//java/google/registry/gcs",
|
||||||
|
"//java/google/registry/storage/drive",
|
||||||
|
"//java/google/registry/util",
|
||||||
"//javatests/google/registry/testing",
|
"//javatests/google/registry/testing",
|
||||||
"@com_google_apis_google_api_services_dataflow",
|
"@com_google_apis_google_api_services_dataflow",
|
||||||
|
"@com_google_appengine_api_1_0_sdk",
|
||||||
|
"@com_google_appengine_tools_appengine_gcs_client",
|
||||||
"@com_google_dagger",
|
"@com_google_dagger",
|
||||||
"@com_google_guava",
|
"@com_google_guava",
|
||||||
"@com_google_truth",
|
"@com_google_truth",
|
||||||
|
|
76
javatests/google/registry/billing/BillingEmailUtilsTest.java
Normal file
76
javatests/google/registry/billing/BillingEmailUtilsTest.java
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright 2017 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.billing;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import google.registry.util.SendEmailService;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Properties;
|
||||||
|
import javax.mail.Message;
|
||||||
|
import javax.mail.MessagingException;
|
||||||
|
import javax.mail.Session;
|
||||||
|
import javax.mail.internet.InternetAddress;
|
||||||
|
import javax.mail.internet.MimeMessage;
|
||||||
|
import org.joda.time.YearMonth;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Unit tests for {@link BillingEmailUtils}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class BillingEmailUtilsTest {
|
||||||
|
|
||||||
|
private SendEmailService emailService;
|
||||||
|
private Message msg;
|
||||||
|
private BillingEmailUtils emailUtils;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
msg = new MimeMessage(Session.getDefaultInstance(new Properties(), null));
|
||||||
|
emailService = mock(SendEmailService.class);
|
||||||
|
when(emailService.createMessage()).thenReturn(msg);
|
||||||
|
|
||||||
|
emailUtils =
|
||||||
|
new BillingEmailUtils(
|
||||||
|
emailService,
|
||||||
|
new YearMonth(2017, 10),
|
||||||
|
"my-sender@test.com",
|
||||||
|
ImmutableList.of("hello@world.com", "hola@mundo.com"),
|
||||||
|
"gs://test-bucket");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_sendsEmail() throws MessagingException, IOException {
|
||||||
|
emailUtils.emailInvoiceLink();
|
||||||
|
assertThat(msg.getFrom()).hasLength(1);
|
||||||
|
assertThat(msg.getFrom()[0])
|
||||||
|
.isEqualTo(new InternetAddress("my-sender@test.com"));
|
||||||
|
assertThat(msg.getAllRecipients())
|
||||||
|
.asList()
|
||||||
|
.containsExactly(
|
||||||
|
new InternetAddress("hello@world.com"), new InternetAddress("hola@mundo.com"));
|
||||||
|
assertThat(msg.getSubject()).isEqualTo("Domain Registry invoice data 2017-10");
|
||||||
|
assertThat(msg.getContentType()).isEqualTo("text/plain");
|
||||||
|
assertThat(msg.getContent().toString())
|
||||||
|
.isEqualTo(
|
||||||
|
"Link to invoice on GCS:\n"
|
||||||
|
+ "https://storage.cloud.google.com/test-bucket/results/CRR-INV-2017-10.csv");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
// Copyright 2017 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.billing;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.testing.DatastoreHelper.loadRegistrar;
|
||||||
|
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||||
|
import static google.registry.testing.GcsTestingUtils.writeGcsFile;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||||
|
import static org.mockito.Matchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.google.appengine.tools.cloudstorage.GcsFilename;
|
||||||
|
import com.google.appengine.tools.cloudstorage.GcsService;
|
||||||
|
import com.google.appengine.tools.cloudstorage.GcsServiceFactory;
|
||||||
|
import com.google.common.net.MediaType;
|
||||||
|
import google.registry.gcs.GcsUtils;
|
||||||
|
import google.registry.storage.drive.DriveConnection;
|
||||||
|
import google.registry.testing.AppEngineRule;
|
||||||
|
import google.registry.testing.FakeClock;
|
||||||
|
import google.registry.testing.FakeResponse;
|
||||||
|
import google.registry.testing.FakeSleeper;
|
||||||
|
import google.registry.util.Retrier;
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Unit tests for {@link CopyDetailReportsAction}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class CopyDetailReportsActionTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||||
|
.withDatastore()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
private final GcsService gcsService = GcsServiceFactory.createGcsService();
|
||||||
|
private final GcsUtils gcsUtils = new GcsUtils(gcsService, 1024);
|
||||||
|
|
||||||
|
private FakeResponse response;
|
||||||
|
private DriveConnection driveConnection;
|
||||||
|
private CopyDetailReportsAction action;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
persistResource(loadRegistrar("TheRegistrar").asBuilder().setDriveFolderId("0B-12345").build());
|
||||||
|
response = new FakeResponse();
|
||||||
|
driveConnection = mock(DriveConnection.class);
|
||||||
|
action =
|
||||||
|
new CopyDetailReportsAction(
|
||||||
|
"gs://test-bucket",
|
||||||
|
"results/",
|
||||||
|
driveConnection,
|
||||||
|
gcsUtils,
|
||||||
|
new Retrier(new FakeSleeper(new FakeClock()), 3),
|
||||||
|
response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess() throws IOException {
|
||||||
|
writeGcsFile(
|
||||||
|
gcsService,
|
||||||
|
new GcsFilename("test-bucket", "results/invoice_details_2017-10_TheRegistrar_test.csv"),
|
||||||
|
"hello,world\n1,2".getBytes(UTF_8));
|
||||||
|
|
||||||
|
writeGcsFile(
|
||||||
|
gcsService,
|
||||||
|
new GcsFilename("test-bucket", "results/invoice_details_2017-10_TheRegistrar_hello.csv"),
|
||||||
|
"hola,mundo\n3,4".getBytes(UTF_8));
|
||||||
|
|
||||||
|
action.run();
|
||||||
|
verify(driveConnection)
|
||||||
|
.createFile(
|
||||||
|
"invoice_details_2017-10_TheRegistrar_test.csv",
|
||||||
|
MediaType.CSV_UTF_8,
|
||||||
|
"0B-12345",
|
||||||
|
"hello,world\n1,2".getBytes(UTF_8));
|
||||||
|
|
||||||
|
verify(driveConnection)
|
||||||
|
.createFile(
|
||||||
|
"invoice_details_2017-10_TheRegistrar_hello.csv",
|
||||||
|
MediaType.CSV_UTF_8,
|
||||||
|
"0B-12345",
|
||||||
|
"hola,mundo\n3,4".getBytes(UTF_8));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||||
|
assertThat(response.getContentType()).isEqualTo(MediaType.PLAIN_TEXT_UTF_8);
|
||||||
|
assertThat(response.getPayload()).isEqualTo("Copied detail reports.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_nonDetailReportFiles_notSent() throws IOException{
|
||||||
|
writeGcsFile(
|
||||||
|
gcsService,
|
||||||
|
new GcsFilename("test-bucket", "results/invoice_details_2017-10_TheRegistrar_hello.csv"),
|
||||||
|
"hola,mundo\n3,4".getBytes(UTF_8));
|
||||||
|
|
||||||
|
writeGcsFile(
|
||||||
|
gcsService,
|
||||||
|
new GcsFilename("test-bucket", "results/not_a_detail_report_2017-10_TheRegistrar_test.csv"),
|
||||||
|
"hello,world\n1,2".getBytes(UTF_8));
|
||||||
|
action.run();
|
||||||
|
verify(driveConnection)
|
||||||
|
.createFile(
|
||||||
|
"invoice_details_2017-10_TheRegistrar_hello.csv",
|
||||||
|
MediaType.CSV_UTF_8,
|
||||||
|
"0B-12345",
|
||||||
|
"hola,mundo\n3,4".getBytes(UTF_8));
|
||||||
|
// Verify we didn't copy the non-detail report file.
|
||||||
|
verifyNoMoreInteractions(driveConnection);
|
||||||
|
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||||
|
assertThat(response.getContentType()).isEqualTo(MediaType.PLAIN_TEXT_UTF_8);
|
||||||
|
assertThat(response.getPayload()).isEqualTo("Copied detail reports.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_transientIOException_retries() throws IOException {
|
||||||
|
writeGcsFile(
|
||||||
|
gcsService,
|
||||||
|
new GcsFilename("test-bucket", "results/invoice_details_2017-10_TheRegistrar_hello.csv"),
|
||||||
|
"hola,mundo\n3,4".getBytes(UTF_8));
|
||||||
|
when(driveConnection.createFile(any(), any(), any(), any()))
|
||||||
|
.thenThrow(new IOException("expected"))
|
||||||
|
.thenReturn("success");
|
||||||
|
|
||||||
|
action.run();
|
||||||
|
verify(driveConnection, times(2))
|
||||||
|
.createFile(
|
||||||
|
"invoice_details_2017-10_TheRegistrar_hello.csv",
|
||||||
|
MediaType.CSV_UTF_8,
|
||||||
|
"0B-12345",
|
||||||
|
"hola,mundo\n3,4".getBytes(UTF_8));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||||
|
assertThat(response.getContentType()).isEqualTo(MediaType.PLAIN_TEXT_UTF_8);
|
||||||
|
assertThat(response.getPayload()).isEqualTo("Copied detail reports.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFail_registrarDoesntExist_doesntCopy() throws IOException {
|
||||||
|
writeGcsFile(
|
||||||
|
gcsService,
|
||||||
|
new GcsFilename("test-bucket", "results/invoice_details_2017-10_notExistent_hello.csv"),
|
||||||
|
"hola,mundo\n3,4".getBytes(UTF_8));
|
||||||
|
action.run();
|
||||||
|
verifyZeroInteractions(driveConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFail_noRegistrarFolderId_doesntCopy() throws IOException {
|
||||||
|
persistResource(loadRegistrar("TheRegistrar").asBuilder().setDriveFolderId(null).build());
|
||||||
|
writeGcsFile(
|
||||||
|
gcsService,
|
||||||
|
new GcsFilename(
|
||||||
|
"test-bucket", "results/invoice_details_2017-10_TheRegistrar_hello.csv"),
|
||||||
|
"hola,mundo\n3,4".getBytes(UTF_8));
|
||||||
|
action.run();
|
||||||
|
verifyZeroInteractions(driveConnection);
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,33 +45,35 @@ import org.junit.runners.JUnit4;
|
||||||
@RunWith(JUnit4.class)
|
@RunWith(JUnit4.class)
|
||||||
public class GenerateInvoicesActionTest {
|
public class GenerateInvoicesActionTest {
|
||||||
|
|
||||||
Dataflow dataflow = mock(Dataflow.class);
|
|
||||||
Projects projects = mock(Projects.class);
|
|
||||||
Templates templates = mock(Templates.class);
|
|
||||||
Launch launch = mock(Launch.class);
|
|
||||||
GenerateInvoicesAction action;
|
|
||||||
FakeResponse response = new FakeResponse();
|
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public final AppEngineRule appEngine = AppEngineRule.builder().withTaskQueue().build();
|
public final AppEngineRule appEngine = AppEngineRule.builder().withTaskQueue().build();
|
||||||
|
|
||||||
|
private Dataflow dataflow;
|
||||||
|
private Projects projects;
|
||||||
|
private Templates templates;
|
||||||
|
private Launch launch;
|
||||||
|
private FakeResponse response;
|
||||||
|
private GenerateInvoicesAction action;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void initializeObjects() throws Exception {
|
public void setUp() throws IOException {
|
||||||
|
dataflow = mock(Dataflow.class);
|
||||||
|
projects = mock(Projects.class);
|
||||||
|
templates = mock(Templates.class);
|
||||||
|
launch = mock(Launch.class);
|
||||||
when(dataflow.projects()).thenReturn(projects);
|
when(dataflow.projects()).thenReturn(projects);
|
||||||
when(projects.templates()).thenReturn(templates);
|
when(projects.templates()).thenReturn(templates);
|
||||||
when(templates.launch(any(String.class), any(LaunchTemplateParameters.class)))
|
when(templates.launch(any(String.class), any(LaunchTemplateParameters.class)))
|
||||||
.thenReturn(launch);
|
.thenReturn(launch);
|
||||||
when(launch.setGcsPath(any(String.class))).thenReturn(launch);
|
when(launch.setGcsPath(any(String.class))).thenReturn(launch);
|
||||||
|
|
||||||
|
response = new FakeResponse();
|
||||||
Job job = new Job();
|
Job job = new Job();
|
||||||
job.setId("12345");
|
job.setId("12345");
|
||||||
when(launch.execute()).thenReturn(new LaunchTemplateResponse().setJob(job));
|
when(launch.execute()).thenReturn(new LaunchTemplateResponse().setJob(job));
|
||||||
|
|
||||||
action = new GenerateInvoicesAction();
|
action = new GenerateInvoicesAction(
|
||||||
action.dataflow = dataflow;
|
"test-project", "gs://test-project-beam", new YearMonth(2017, 10), dataflow, response);
|
||||||
action.response = response;
|
|
||||||
action.projectId = "test-project";
|
|
||||||
action.beamBucketUrl = "gs://test-project-beam";
|
|
||||||
action.yearMonth = new YearMonth(2017, 10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -99,7 +101,7 @@ public class GenerateInvoicesActionTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCaughtIOException() throws Exception {
|
public void testCaughtIOException() throws IOException {
|
||||||
when(launch.execute()).thenThrow(new IOException("expected"));
|
when(launch.execute()).thenThrow(new IOException("expected"));
|
||||||
action.run();
|
action.run();
|
||||||
assertThat(response.getStatus()).isEqualTo(500);
|
assertThat(response.getStatus()).isEqualTo(500);
|
||||||
|
|
|
@ -15,11 +15,13 @@
|
||||||
package google.registry.billing;
|
package google.registry.billing;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
|
||||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||||
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
|
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
|
||||||
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
|
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
|
||||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import com.google.api.services.dataflow.Dataflow;
|
import com.google.api.services.dataflow.Dataflow;
|
||||||
|
@ -28,9 +30,12 @@ import com.google.api.services.dataflow.Dataflow.Projects.Jobs;
|
||||||
import com.google.api.services.dataflow.Dataflow.Projects.Jobs.Get;
|
import com.google.api.services.dataflow.Dataflow.Projects.Jobs.Get;
|
||||||
import com.google.api.services.dataflow.model.Job;
|
import com.google.api.services.dataflow.model.Job;
|
||||||
import com.google.common.net.MediaType;
|
import com.google.common.net.MediaType;
|
||||||
|
import google.registry.testing.AppEngineRule;
|
||||||
import google.registry.testing.FakeResponse;
|
import google.registry.testing.FakeResponse;
|
||||||
|
import google.registry.testing.TaskQueueHelper.TaskMatcher;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.junit.runners.JUnit4;
|
import org.junit.runners.JUnit4;
|
||||||
|
@ -38,36 +43,48 @@ import org.junit.runners.JUnit4;
|
||||||
@RunWith(JUnit4.class)
|
@RunWith(JUnit4.class)
|
||||||
public class PublishInvoicesActionTest {
|
public class PublishInvoicesActionTest {
|
||||||
|
|
||||||
private final Dataflow dataflow = mock(Dataflow.class);
|
private Dataflow dataflow;
|
||||||
private final Projects projects = mock(Projects.class);
|
private Projects projects;
|
||||||
private final Jobs jobs = mock(Jobs.class);
|
private Jobs jobs;
|
||||||
private final Get get = mock(Get.class);
|
private Get get;
|
||||||
|
private BillingEmailUtils emailUtils;
|
||||||
|
|
||||||
private Job expectedJob;
|
private Job expectedJob;
|
||||||
private FakeResponse response;
|
private FakeResponse response;
|
||||||
private PublishInvoicesAction uploadAction;
|
private PublishInvoicesAction uploadAction;
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public final AppEngineRule appEngine = AppEngineRule.builder().withTaskQueue().build();
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void initializeObjects() throws Exception {
|
public void setUp() throws IOException {
|
||||||
|
dataflow = mock(Dataflow.class);
|
||||||
|
projects = mock(Projects.class);
|
||||||
|
jobs = mock(Jobs.class);
|
||||||
|
get = mock(Get.class);
|
||||||
when(dataflow.projects()).thenReturn(projects);
|
when(dataflow.projects()).thenReturn(projects);
|
||||||
when(projects.jobs()).thenReturn(jobs);
|
when(projects.jobs()).thenReturn(jobs);
|
||||||
when(jobs.get("test-project", "12345")).thenReturn(get);
|
when(jobs.get("test-project", "12345")).thenReturn(get);
|
||||||
expectedJob = new Job();
|
expectedJob = new Job();
|
||||||
when(get.execute()).thenReturn(expectedJob);
|
when(get.execute()).thenReturn(expectedJob);
|
||||||
|
emailUtils = mock(BillingEmailUtils.class);
|
||||||
uploadAction = new PublishInvoicesAction();
|
|
||||||
uploadAction.projectId = "test-project";
|
|
||||||
uploadAction.jobId = "12345";
|
|
||||||
uploadAction.dataflow = dataflow;
|
|
||||||
response = new FakeResponse();
|
response = new FakeResponse();
|
||||||
uploadAction.response = response;
|
uploadAction =
|
||||||
|
new PublishInvoicesAction("test-project", "12345", emailUtils, dataflow, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testJobDone_returnsSuccess() {
|
public void testJobDone_enqueuesCopyAction_emailsResults() throws Exception {
|
||||||
expectedJob.setCurrentState("JOB_STATE_DONE");
|
expectedJob.setCurrentState("JOB_STATE_DONE");
|
||||||
uploadAction.run();
|
uploadAction.run();
|
||||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||||
|
verify(emailUtils).emailInvoiceLink();
|
||||||
|
TaskMatcher matcher =
|
||||||
|
new TaskMatcher()
|
||||||
|
.url("/_dr/task/copyDetailReports")
|
||||||
|
.method("POST")
|
||||||
|
.param("directoryPrefix", "results/");
|
||||||
|
assertTasksEnqueued("retryable-cron-tasks", matcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -85,7 +102,7 @@ public class PublishInvoicesActionTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIOException_returnsFailureMessage() throws Exception {
|
public void testIOException_returnsFailureMessage() throws IOException {
|
||||||
when(get.execute()).thenThrow(new IOException("expected"));
|
when(get.execute()).thenThrow(new IOException("expected"));
|
||||||
uploadAction.run();
|
uploadAction.run();
|
||||||
assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
|
assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
|
||||||
|
|
|
@ -6,6 +6,7 @@ PATH CLASS METHOD
|
||||||
/_dr/dnsRefresh RefreshDnsAction GET y INTERNAL APP IGNORED
|
/_dr/dnsRefresh RefreshDnsAction GET y INTERNAL APP IGNORED
|
||||||
/_dr/task/brdaCopy BrdaCopyAction POST y INTERNAL APP IGNORED
|
/_dr/task/brdaCopy BrdaCopyAction POST y INTERNAL APP IGNORED
|
||||||
/_dr/task/checkSnapshot CheckSnapshotAction POST,GET y INTERNAL APP IGNORED
|
/_dr/task/checkSnapshot CheckSnapshotAction POST,GET y INTERNAL APP IGNORED
|
||||||
|
/_dr/task/copyDetailReports CopyDetailReportsAction POST n INTERNAL,API APP ADMIN
|
||||||
/_dr/task/deleteContactsAndHosts DeleteContactsAndHostsAction GET n INTERNAL APP IGNORED
|
/_dr/task/deleteContactsAndHosts DeleteContactsAndHostsAction GET n INTERNAL APP IGNORED
|
||||||
/_dr/task/deleteOldCommitLogs DeleteOldCommitLogsAction GET n INTERNAL APP IGNORED
|
/_dr/task/deleteOldCommitLogs DeleteOldCommitLogsAction GET n INTERNAL APP IGNORED
|
||||||
/_dr/task/deleteProberData DeleteProberDataAction POST n INTERNAL APP IGNORED
|
/_dr/task/deleteProberData DeleteProberDataAction POST n INTERNAL APP IGNORED
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue