diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 4f71d7d0777..8667645ae93 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -36,6 +36,7 @@ import google.registry.bsa.UploadBsaUnavailableDomainsAction; import google.registry.dns.ReadDnsRefreshRequestsAction; import google.registry.model.common.DnsRefreshRequest; +import google.registry.mosapi.client.MosApiClient; import google.registry.persistence.transaction.JpaTransactionManager; import google.registry.request.Action.Service; import google.registry.util.RegistryEnvironment; @@ -49,6 +50,7 @@ import java.lang.annotation.Retention; import java.net.URI; import java.net.URL; +import java.util.List; import java.util.Map.Entry; import java.util.Optional; import java.util.function.Supplier; @@ -1415,6 +1417,40 @@ public static String provideBsaUploadUnavailableDomainsUrl(RegistryConfigSetting return config.bsa.uploadUnavailableDomainsUrl; } + /** + * Returns the URL we send HTTP requests for MoSAPI. + * + * @see MosApiClient + */ + @Provides + @Config("mosapiUrl") + public static String provideMosapiUrl(RegistryConfigSettings config) { + return config.mosapi.mosapiUrl; + } + + /** + * Returns the entityType we send HTTP requests for MoSAPI. + * + * @see MosApiClient + */ + @Provides + @Config("entityType") + public static String provideMosapiEntityType(RegistryConfigSettings config) { + return config.mosapi.entityType; + } + + @Provides + @Config("mosapiTlds") + public static List provideMosapiTlds(RegistryConfigSettings config) { + return ImmutableList.copyOf(config.mosapi.tlds); + } + + @Provides + @Config("mosapiServices") + public static List provideMosapiServices(RegistryConfigSettings config) { + return ImmutableList.copyOf(config.mosapi.services); + } + private static String formatComments(String text) { return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream() .map(s -> "# " + s) diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index b4b8c6fad7b..3578bed4471 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -43,6 +43,7 @@ public class RegistryConfigSettings { public DnsUpdate dnsUpdate; public BulkPricingPackageMonitoring bulkPricingPackageMonitoring; public Bsa bsa; + public MosApi mosapi; /** Configuration options that apply to the entire GCP project. */ public static class GcpProject { @@ -267,4 +268,12 @@ public static class Bsa { public String unblockableDomainsUrl; public String uploadUnavailableDomainsUrl; } + + /** Configuration for Mosapi. */ + public static class MosApi { + public String mosapiUrl; + public String entityType; + public List tlds; + public List services; + } } diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index 9aa6756c470..7b845ac8387 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -628,3 +628,20 @@ bsa: unblockableDomainsUrl: "https://" # API endpoint for uploading the list of unavailable domain names. uploadUnavailableDomainsUrl: "https://" + +mosapi: + # URL for the MosAPI + mosapiUrl: https://mosapi.icann.org + entityType: ry + # List of TLDs to be monitored. + tlds: + - "test" + # List of services to check for each TLD. + services: + - "dns" + - "rdap" + - "rdds" + - "epp" + - "dnssec" + + diff --git a/core/src/main/java/google/registry/module/RegistryComponent.java b/core/src/main/java/google/registry/module/RegistryComponent.java index f8b00fe86ff..346120047cf 100644 --- a/core/src/main/java/google/registry/module/RegistryComponent.java +++ b/core/src/main/java/google/registry/module/RegistryComponent.java @@ -40,6 +40,7 @@ import google.registry.module.RegistryComponent.RegistryModule; import google.registry.module.RequestComponent.RequestComponentModule; import google.registry.monitoring.whitebox.StackdriverModule; +import google.registry.mosapi.module.MosApiModule; import google.registry.persistence.PersistenceModule; import google.registry.privileges.secretmanager.SecretManagerModule; import google.registry.rde.JSchModule; @@ -49,6 +50,7 @@ import google.registry.request.RequestHandler; import google.registry.request.auth.AuthModule; import google.registry.request.auth.RequestAuthenticator; +import google.registry.util.HttpModule; import google.registry.util.UtilsModule; import jakarta.inject.Provider; import jakarta.inject.Singleton; @@ -71,6 +73,8 @@ GroupsModule.class, GroupssettingsModule.class, GsonModule.class, + HttpModule.class, + MosApiModule.class, JSchModule.class, KeyModule.class, KeyringModule.class, diff --git a/core/src/main/java/google/registry/module/RequestComponent.java b/core/src/main/java/google/registry/module/RequestComponent.java index 58992d4b54e..fe131cb4b41 100644 --- a/core/src/main/java/google/registry/module/RequestComponent.java +++ b/core/src/main/java/google/registry/module/RequestComponent.java @@ -61,6 +61,12 @@ import google.registry.module.ReadinessProbeAction.ReadinessProbeActionPubApi; import google.registry.module.ReadinessProbeAction.ReadinessProbeConsoleAction; import google.registry.monitoring.whitebox.WhiteboxModule; +import google.registry.mosapi.action.GetAlarmsStateAction; +import google.registry.mosapi.action.GetMetricaReportAction; +import google.registry.mosapi.action.GetServiceDowntimeAction; +import google.registry.mosapi.action.GetServiceStateAction; +import google.registry.mosapi.action.ListMetricaReportsAction; +import google.registry.mosapi.module.MosApiRequestModule; import google.registry.rdap.RdapAutnumAction; import google.registry.rdap.RdapDomainAction; import google.registry.rdap.RdapDomainSearchAction; @@ -150,6 +156,7 @@ EppToolModule.class, IcannReportingModule.class, LoadTestModule.class, + MosApiRequestModule.class, RdapModule.class, RdeModule.class, ReportingModule.class, @@ -229,6 +236,14 @@ interface RequestComponent { GenerateZoneFilesAction generateZoneFilesAction(); + GetAlarmsStateAction checkMosApiAlarmsAction(); + + GetMetricaReportAction getMetricaReportAction(); + + GetServiceDowntimeAction getServiceDowntimeAction(); + + GetServiceStateAction getServiceStateAction(); + IcannReportingStagingAction icannReportingStagingAction(); IcannReportingUploadAction icannReportingUploadAction(); @@ -237,6 +252,8 @@ interface RequestComponent { ListHostsAction listHostsAction(); + ListMetricaReportsAction listMetricaReportsAction(); + ListPremiumListsAction listPremiumListsAction(); ListRegistrarsAction listRegistrarsAction(); @@ -341,6 +358,7 @@ interface RequestComponent { WipeOutContactHistoryPiiAction wipeOutContactHistoryPiiAction(); + @Subcomponent.Builder abstract class Builder implements RequestComponentBuilder { @Override diff --git a/core/src/main/java/google/registry/mosapi/action/GetAlarmsStateAction.java b/core/src/main/java/google/registry/mosapi/action/GetAlarmsStateAction.java new file mode 100644 index 00000000000..66c6ad85844 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/action/GetAlarmsStateAction.java @@ -0,0 +1,52 @@ +// Copyright 2025 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.mosapi.action; + +import static google.registry.request.Action.Method.GET; + +import com.google.common.net.MediaType; +import com.google.gson.Gson; +import google.registry.mosapi.services.MosApiAlarmService; +import google.registry.request.Action; +import google.registry.request.Response; +import google.registry.request.auth.Auth; +import jakarta.inject.Inject; + +/** An action that checks the alarm status for all configured MoSAPI entities. */ +@Action( + service = Action.Service.BACKEND, + path = GetAlarmsStateAction.PATH, + method = GET, + auth = Auth.AUTH_ADMIN) +public class GetAlarmsStateAction implements Runnable { + public static final String PATH = "/_dr/mosapi/checkalarm"; + + private final MosApiAlarmService alarmsService; + private final Response response; + private final Gson gson; + + @Inject + public GetAlarmsStateAction(MosApiAlarmService alarmsService, Response response, Gson gson) { + this.alarmsService = alarmsService; + this.response = response; + this.gson = gson; + } + + @Override + public void run() { + response.setContentType(MediaType.JSON_UTF_8); + response.setPayload(gson.toJson(alarmsService.checkAllAlarms())); + } +} diff --git a/core/src/main/java/google/registry/mosapi/action/GetMetricaReportAction.java b/core/src/main/java/google/registry/mosapi/action/GetMetricaReportAction.java new file mode 100644 index 00000000000..2b7d5338aeb --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/action/GetMetricaReportAction.java @@ -0,0 +1,74 @@ +// Copyright 2025 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.mosapi.action; + +import com.google.common.flogger.FluentLogger; +import com.google.common.net.MediaType; +import com.google.gson.Gson; +import google.registry.mosapi.exception.MosApiException; +import google.registry.mosapi.services.MosApiMetricaService; +import google.registry.request.Action; +import google.registry.request.HttpException.ServiceUnavailableException; +import google.registry.request.Parameter; +import google.registry.request.Response; +import google.registry.request.auth.Auth; +import jakarta.inject.Inject; +import java.time.LocalDate; +import java.util.Optional; + +@Action( + service = Action.Service.BACKEND, + path = GetMetricaReportAction.PATH, + method = Action.Method.GET, + auth = Auth.AUTH_ADMIN) +public class GetMetricaReportAction implements Runnable { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + public static final String PATH = "/_dr/mosapi/getMetricaReport"; + public static final String TLD_PARAM = "tld"; + public static final String DATE_PARAM = "date"; + + private final MosApiMetricaService metricaService; + private final Response response; + private final Gson gson; + private final String tld; + private final Optional date; + + @Inject + public GetMetricaReportAction( + MosApiMetricaService metricaService, + Response response, + Gson gson, + @Parameter(TLD_PARAM) String tld, + @Parameter(DATE_PARAM) Optional date) { + this.metricaService = metricaService; + this.response = response; + this.gson = gson; + this.tld = tld; + this.date = date; + } + + @Override + public void run() { + try { + response.setContentType(MediaType.JSON_UTF_8); + response.setPayload(gson.toJson(metricaService.getReport(tld, date))); + } catch (MosApiException e) { + logger.atWarning().withCause(e).log( + "MoSAPI client failed to get response for Metrica report for TLD: %s", tld); + throw new ServiceUnavailableException("Error fetching METRICA report."); + } + } +} diff --git a/core/src/main/java/google/registry/mosapi/action/GetServiceDowntimeAction.java b/core/src/main/java/google/registry/mosapi/action/GetServiceDowntimeAction.java new file mode 100644 index 00000000000..cb4970294c7 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/action/GetServiceDowntimeAction.java @@ -0,0 +1,66 @@ +// Copyright 2025 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.mosapi.action; + +import com.google.common.net.MediaType; +import com.google.gson.Gson; +import google.registry.mosapi.services.MosApiDowntimeService; +import google.registry.request.Action; +import google.registry.request.Parameter; +import google.registry.request.Response; +import google.registry.request.auth.Auth; +import jakarta.inject.Inject; +import java.util.Optional; + +/** An action that returns the MoSAPI downtime for a given TLD and service. */ +@Action( + service = Action.Service.BACKEND, + path = GetServiceDowntimeAction.PATH, + method = Action.Method.GET, + auth = Auth.AUTH_ADMIN) +public class GetServiceDowntimeAction implements Runnable { + + public static final String PATH = "/_dr/mosapi/getServiceDowntime"; + public static final String TLD_PARAM = "tld"; + + private final MosApiDowntimeService downtimeService; + private final Response response; + private final Gson gson; + private final Optional tld; + + @Inject + public GetServiceDowntimeAction( + MosApiDowntimeService downtimeService, + Response response, + Gson gson, + @Parameter(TLD_PARAM) Optional tld) { + this.downtimeService = downtimeService; + this.response = response; + this.gson = gson; + this.tld = tld; + } + + @Override + public void run() { + response.setContentType(MediaType.JSON_UTF_8); + if (tld.isPresent()) { + // Handle the case for a single TLD. + response.setPayload(gson.toJson(downtimeService.getDowntimeForTld(tld.get()))); + } else { + // Handle the case for all TLDs. + response.setPayload(gson.toJson(downtimeService.getDowntimeForAllTlds())); + } + } +} diff --git a/core/src/main/java/google/registry/mosapi/action/GetServiceStateAction.java b/core/src/main/java/google/registry/mosapi/action/GetServiceStateAction.java new file mode 100644 index 00000000000..002fa3e1cf5 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/action/GetServiceStateAction.java @@ -0,0 +1,74 @@ +// Copyright 2025 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.mosapi.action; + +import com.google.common.flogger.FluentLogger; +import com.google.common.net.MediaType; +import com.google.gson.Gson; +import google.registry.mosapi.exception.MosApiException; +import google.registry.mosapi.services.MosApiStateService; +import google.registry.request.Action; +import google.registry.request.HttpException.ServiceUnavailableException; +import google.registry.request.Parameter; +import google.registry.request.Response; +import google.registry.request.auth.Auth; +import jakarta.inject.Inject; +import java.util.Optional; + +/** An action that returns the current MoSAPI service state for a given TLD. */ +@Action( + service = Action.Service.BACKEND, + path = GetServiceStateAction.PATH, + method = Action.Method.GET, + auth = Auth.AUTH_ADMIN) +public class GetServiceStateAction implements Runnable { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + public static final String PATH = "/_dr/mosapi/getServiceState"; + public static final String TLD_PARAM = "tld"; + + private final MosApiStateService stateService; + private final Response response; + private final Gson gson; + private final Optional tld; + + @Inject + public GetServiceStateAction( + MosApiStateService stateService, + Response response, + Gson gson, + @Parameter(TLD_PARAM) Optional tld) { + this.stateService = stateService; + this.response = response; + this.gson = gson; + this.tld = tld; + } + + @Override + public void run() { + response.setContentType(MediaType.JSON_UTF_8); + try { + if (tld.isPresent()) { + response.setPayload(gson.toJson(stateService.getServiceStateSummary(tld.get()))); + } else { + response.setPayload(gson.toJson(stateService.getAllServiceStateSummaries())); + } + } catch (MosApiException e) { + logger.atWarning().withCause(e).log( + "MoSAPI client failed to get Service state for %s TLD", tld.orElse("all")); + throw new ServiceUnavailableException("Error fetching MoSAPI service state."); + } + } +} diff --git a/core/src/main/java/google/registry/mosapi/action/ListMetricaReportsAction.java b/core/src/main/java/google/registry/mosapi/action/ListMetricaReportsAction.java new file mode 100644 index 00000000000..234fd155c98 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/action/ListMetricaReportsAction.java @@ -0,0 +1,80 @@ +// Copyright 2025 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.mosapi.action; + +import com.google.common.flogger.FluentLogger; +import com.google.common.net.MediaType; +import com.google.gson.Gson; +import google.registry.mosapi.exception.MosApiException; +import google.registry.mosapi.services.MosApiMetricaService; +import google.registry.request.Action; +import google.registry.request.HttpException.ServiceUnavailableException; +import google.registry.request.Parameter; +import google.registry.request.Response; +import google.registry.request.auth.Auth; +import jakarta.inject.Inject; +import java.time.LocalDate; +import java.util.Optional; + +/** An action that lists available MoSAPI Domain METRICA reports for a given TLD. */ +@Action( + service = Action.Service.BACKEND, + path = ListMetricaReportsAction.PATH, + method = Action.Method.GET, + auth = Auth.AUTH_ADMIN) +public class ListMetricaReportsAction implements Runnable { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + public static final String PATH = "/_dr/mosapi/listMetricaReports"; + public static final String TLD_PARAM = "tld"; + public static final String START_DATE_PARAM = "startDate"; + public static final String END_DATE_PARAM = "endDate"; + + private final MosApiMetricaService metricaService; + private final Response response; + private final Gson gson; + private final String tld; + private final Optional startDate; + private final Optional endDate; + + @Inject + public ListMetricaReportsAction( + MosApiMetricaService metricaService, + Response response, + Gson gson, + @Parameter(TLD_PARAM) String tld, + @Parameter(START_DATE_PARAM) Optional startDate, + @Parameter(END_DATE_PARAM) Optional endDate) { + this.metricaService = metricaService; + this.response = response; + this.gson = gson; + this.tld = tld; + this.startDate = startDate; + this.endDate = endDate; + } + + @Override + public void run() { + try { + response.setContentType(MediaType.JSON_UTF_8); + response.setPayload( + gson.toJson(metricaService.listAvailableReports(tld, startDate, endDate))); + } catch (MosApiException e) { + logger.atWarning().withCause(e).log( + "MoSAPI client failed to list metrica report for TLD: %s", tld); + throw new ServiceUnavailableException("Error listing METRICA reports."); + } + } +} diff --git a/core/src/main/java/google/registry/mosapi/client/DomainMetricaClient.java b/core/src/main/java/google/registry/mosapi/client/DomainMetricaClient.java new file mode 100644 index 00000000000..a0ac1b9e168 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/client/DomainMetricaClient.java @@ -0,0 +1,143 @@ +// Copyright 2025 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.mosapi.client; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import google.registry.mosapi.dto.MosApiErrorResponse; +import google.registry.mosapi.dto.domainmetrica.MetricaReport; +import google.registry.mosapi.dto.domainmetrica.MetricaReportInfo; +import google.registry.mosapi.exception.MosApiException; +import jakarta.inject.Inject; +import java.net.HttpURLConnection; +import java.net.http.HttpResponse; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +/** Facade for MoSAPI's Domain METRICA endpoints. */ +public class DomainMetricaClient { + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private final MosApiClient mosApiClient; + private final Gson gson; + + @Inject + public DomainMetricaClient(MosApiClient mosApiClient, Gson gson) { + this.mosApiClient = mosApiClient; + this.gson = gson; + } + + /** + * Fetches the most recent daily Domain METRICA report. + * + * @see ICANN MoSAPI Specification, + * Section 9.1 + */ + public MetricaReport getLatestMetricaReport(String tld) throws MosApiException { + String endpoint = String.format("v2/metrica/domainList/latest"); + + HttpResponse response = + mosApiClient.sendGetRequestWithDecompression( + tld, + endpoint, + Collections.emptyMap(), + ImmutableMap.of("Accept-Encoding", "gzip, deflate")); + + // Return 404 if no report exists for the TLD. + if (response.statusCode() == HttpURLConnection.HTTP_NOT_FOUND) { + throw new MosApiException("No METRICA report found for TLD: " + tld); + } + return gson.fromJson(response.body(), MetricaReport.class); + } + + /** + * Fetches the Domain METRICA report for a specific date. + * + * @see ICANN MoSAPI Specification, + * Section 9.2 + */ + public MetricaReport getMetricaReportForDate(String tld, LocalDate date) throws MosApiException { + String formattedDate = date.format(DATE_FORMATTER); + String endpoint = String.format("v2/metrica/domainList/%s", formattedDate); + HttpResponse response = + mosApiClient.sendGetRequestWithDecompression( + tld, + endpoint, + Collections.emptyMap(), + ImmutableMap.of("Accept-Encoding", "gzip, deflate")); + + // Return 404 if no report exists for the specified date. + if (response.statusCode() == HttpURLConnection.HTTP_NOT_FOUND) { + throw new MosApiException( + String.format("No METRICA report found for TLD %s on %s", tld, formattedDate)); + } + return gson.fromJson(response.body(), MetricaReport.class); + } + + /** + * Lists available Domain METRICA report dates within a given range. + * + * @see ICANN MoSAPI Specification, + * Section 9.3 + */ + public List listAvailableMetricaReports( + String tld, @Nullable LocalDate startDate, @Nullable LocalDate endDate) + throws MosApiException { + String endpoint = String.format("v2/metrica/domainLists"); + ImmutableMap.Builder params = new ImmutableMap.Builder<>(); + // Optional startDate and endDate parameters + if (startDate != null) { + params.put("startDate", startDate.format(DATE_FORMATTER)); + } + if (endDate != null) { + params.put("endDate", endDate.format(DATE_FORMATTER)); + } + + HttpResponse response = + mosApiClient.sendGetRequestWithDecompression( + tld, endpoint, params.build(), ImmutableMap.of("Accept-Encoding", "gzip, deflate")); + // Handle HTTP 400 for specific business logic errors (codes 2012, 2013, 2014) + if (response.statusCode() == HttpURLConnection.HTTP_BAD_REQUEST) { + MosApiErrorResponse error = gson.fromJson(response.body(), MosApiErrorResponse.class); + throw MosApiException.create(error); + } + + if (response.statusCode() != HttpURLConnection.HTTP_OK) { + throw new MosApiException( + String.format( + "Request to %s failed with status code %d", response.uri(), response.statusCode())); + } + + MetricaReportListResponse listResponse = + gson.fromJson(response.body(), MetricaReportListResponse.class); + return listResponse.domainLists(); + } + + /** Helper class for deserializing the response from the 'domainLists' endpoint. */ + private static class MetricaReportListResponse { + @Expose + @SerializedName("domainLists") + private List domainLists; + + List domainLists() { + return domainLists; + } + } +} diff --git a/core/src/main/java/google/registry/mosapi/client/MosApiClient.java b/core/src/main/java/google/registry/mosapi/client/MosApiClient.java new file mode 100644 index 00000000000..b494645a228 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/client/MosApiClient.java @@ -0,0 +1,127 @@ +// Copyright 2025 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.mosapi.client; + +import google.registry.config.RegistryConfig.Config; +import google.registry.mosapi.exception.MosApiException; +import google.registry.mosapi.exception.MosApiException.MosApiAuthorizationException; +import google.registry.util.HttpUtils; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import java.net.HttpURLConnection; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.stream.Collectors; + +@Singleton +public class MosApiClient { + + private final HttpClient httpClient; + private final String baseUrl; + + @Inject + public MosApiClient( + @Named("mosapiHttpClient") HttpClient httpClient, + @Config("mosapiUrl") String mosapiUrl, + @Config("entityType") String entityType) { + this.httpClient = httpClient; + this.baseUrl = String.format("%s/%s", mosapiUrl, entityType); + } + + public HttpResponse sendGetRequest( + String entityId, String endpoint, Map params, Map headers) + throws MosApiException { + String url = buildUrl(entityId, endpoint, params); + try { + HttpResponse response = HttpUtils.sendGetRequest(httpClient, url, headers); + return checkResponseForAuthError(response); + } catch (RuntimeException e) { + throw new MosApiException("Error during GET request to " + url, e); + } + } + + /** + * Sends a GET request and decompresses the response body if it is gzip-encoded. + * + *

Note that this method returns the response body directly as a {@code String} rather than the + * full {@link HttpResponse} because constructing a new {@code HttpResponse} with the modified + * (decompressed) body is overly complex. The status code is checked for common error conditions + * before returning. + */ + public HttpResponse sendGetRequestWithDecompression( + String entityId, String endpoint, Map params, Map headers) + throws MosApiException { + String url = buildUrl(entityId, endpoint, params); + try { + HttpResponse response = + HttpUtils.sendGetRequestWithDecompression(httpClient, url, headers); + return checkResponseForAuthError(response); + + } catch (RuntimeException e) { + throw new MosApiException("Error during GET request to " + url, e); + } + } + + public HttpResponse sendPostRequest( + String entityId, + String endpoint, + Map params, + Map headers, + String body) + throws MosApiException { + String url = buildUrl(entityId, endpoint, params); + try { + HttpResponse response = HttpUtils.sendPostRequest(httpClient, url, headers, body); + return checkResponseForAuthError(response); + } catch (RuntimeException e) { + throw new MosApiException("Error during POST request to " + url, e); + } + } + + private HttpResponse checkResponseForAuthError(HttpResponse response) + throws MosApiAuthorizationException { + if (response.statusCode() == HttpURLConnection.HTTP_UNAUTHORIZED) { + throw new MosApiAuthorizationException( + "Authorization failed for the requested resource. The client certificate may not be" + + " authorized for the specified TLD or Registrar."); + } + return response; + } + + /** + * Builds the full URL for a request, including the base URL, entityId, path, and query params. + */ + private String buildUrl(String entityId, String path, Map queryParams) { + String sanitizedPath = path.startsWith("/") ? path : "/" + path; + String fullPath = "/" + entityId + sanitizedPath; + + if (queryParams == null || queryParams.isEmpty()) { + return baseUrl + fullPath; + } + String queryString = + queryParams.entrySet().stream() + .map( + entry -> + entry.getKey() + + "=" + + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&")); + return baseUrl + fullPath + "?" + queryString; + } +} diff --git a/core/src/main/java/google/registry/mosapi/client/ServiceMonitoringClient.java b/core/src/main/java/google/registry/mosapi/client/ServiceMonitoringClient.java new file mode 100644 index 00000000000..f6b3299f818 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/client/ServiceMonitoringClient.java @@ -0,0 +1,95 @@ +// Copyright 2025 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.mosapi.client; + +import com.google.gson.Gson; +import google.registry.mosapi.dto.servicemonitoring.ServiceAlarm; +import google.registry.mosapi.dto.servicemonitoring.ServiceDowntime; +import google.registry.mosapi.dto.servicemonitoring.TldServiceState; +import google.registry.mosapi.exception.MosApiException; +import jakarta.inject.Inject; +import java.net.HttpURLConnection; +import java.net.http.HttpResponse; +import java.util.Collections; + +/** Facade for MoSAPI's service monitoring endpoints. */ +public class ServiceMonitoringClient { + private final MosApiClient mosApiClient; + private final Gson gson; + + @Inject + public ServiceMonitoringClient(MosApiClient mosApiClient, Gson gson) { + this.mosApiClient = mosApiClient; + this.gson = gson; + } + + /** + * Fetches the current state of all monitored services for a given TLD. + * + * @see ICANN MoSAPI Specification, + * Section 5.1 + */ + public TldServiceState getServiceState(String tld) throws MosApiException { + String endpoint = String.format("v2/monitoring/state"); + HttpResponse response = + mosApiClient.sendGetRequest(tld, endpoint, Collections.emptyMap(), Collections.emptyMap()); + return gson.fromJson(response.body(), TldServiceState.class); + } + + /** + * Fetches the total downtime for a specific service over a rolling week period. + * + * @see ICANN MoSAPI Specification, + * Section 5.3 + */ + public ServiceDowntime getDowntime(String tld, String service) throws MosApiException { + String endpoint = String.format("v2/monitoring/%s/downtime", service); + HttpResponse response = + mosApiClient.sendGetRequest(tld, endpoint, Collections.emptyMap(), Collections.emptyMap()); + switch (response.statusCode()) { + case HttpURLConnection.HTTP_OK: + return gson.fromJson(response.body(), ServiceDowntime.class); + case HttpURLConnection.HTTP_NOT_FOUND: + return new ServiceDowntime(2, 0, 0, true); + default: + throw new MosApiException( + String.format( + "Request to %s failed with status code %d", response.uri(), response.statusCode())); + } + } + + /** + * Checks if a specific service has an active alarm. + * + * @see ICANN MoSAPI Specification, + * Section 5.2 + */ + public ServiceAlarm serviceAlarmed(String tld, String service) throws MosApiException { + String endpoint = String.format("v2/monitoring/%s/alarmed", service); + HttpResponse response = + mosApiClient.sendGetRequest(tld, endpoint, Collections.emptyMap(), Collections.emptyMap()); + + switch (response.statusCode()) { + case HttpURLConnection.HTTP_OK: + return gson.fromJson(response.body(), ServiceAlarm.class); + case HttpURLConnection.HTTP_NOT_FOUND: + return new ServiceAlarm(2, 0, "Disabled"); + default: + throw new MosApiException( + String.format( + "Request to %s failed with status code %d", response.uri(), response.statusCode())); + } + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/MosApiErrorResponse.java b/core/src/main/java/google/registry/mosapi/dto/MosApiErrorResponse.java new file mode 100644 index 00000000000..4863f16e782 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/MosApiErrorResponse.java @@ -0,0 +1,23 @@ +// Copyright 2025 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.mosapi.dto; + +/** + * Represents the generic JSON error response from the MoSAPI service for a 400 Bad Request. + * + * @see ICANN MoSAPI Specification, Section + * 8 + */ +public record MosApiErrorResponse(String resultCode, String message, String description) {} diff --git a/core/src/main/java/google/registry/mosapi/dto/domainmetrica/MetricaReport.java b/core/src/main/java/google/registry/mosapi/dto/domainmetrica/MetricaReport.java new file mode 100644 index 00000000000..33004f9e841 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/domainmetrica/MetricaReport.java @@ -0,0 +1,98 @@ +// Copyright 2025 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.mosapi.dto.domainmetrica; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.util.List; + +/** + * Represents a full Domain METRICA report. + * + * @see ICANN MoSAPI Specification, Section + * 9.1 + */ +public final class MetricaReport { + private final int version; + + @Expose + @SerializedName("tld") + private final String tld; + + /** + * A JSON string that contains the date of the report in the format YYYY-MM-DD. + * + * @see ICANN MoSAPI Specification, + * Section 9.1 + */ + @Expose + @SerializedName("domainListDate") + private final String domainListDate; + + /** + * A JSON number that includes the total number of unique abuse domains detected for the + * particular date. + * + * @see ICANN MoSAPI Specification, + * Section 9.1 + */ + @Expose + @SerializedName("uniqueAbuseDomains") + private final int uniqueAbuseDomains; + + /** + * An array of JSON objects describing the type of DNS abuse (spam, phishing, botnetCc, malware) + * and the count of domains. + * + * @see ICANN MoSAPI Specification, + * Section 9.1 + */ + @Expose + @SerializedName("domainListData") + private final List threats; + + public MetricaReport( + int version, + String tld, + String domainListDate, + int uniqueAbuseDomains, + List threats) { + this.version = version; + this.tld = tld; + this.domainListDate = domainListDate; + this.uniqueAbuseDomains = uniqueAbuseDomains; + this.threats = threats; + } + + public int getVersion() { + return version; + } + + public String getTld() { + return tld; + } + + public String getDomainListDate() { + return domainListDate; + } + + public int getUniqueAbuseDomains() { + return uniqueAbuseDomains; + } + + public List getThreats() { + return threats; + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/domainmetrica/MetricaReportInfo.java b/core/src/main/java/google/registry/mosapi/dto/domainmetrica/MetricaReportInfo.java new file mode 100644 index 00000000000..1eaffccd415 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/domainmetrica/MetricaReportInfo.java @@ -0,0 +1,48 @@ +// Copyright 2025 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.mosapi.dto.domainmetrica; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +/** + * Represents information about a single available METRICA report. + * + * @see ICANN + * MoSAPI Specification, Section 9.3 + */ +public final class MetricaReportInfo { + @Expose + @SerializedName("domainListDate") + private final String domainListDate; + + @Expose + @SerializedName("domainListGenerationDate") + private final String domainListGenerationDate; + + public MetricaReportInfo(String domainListDate, String domainListGenerationDate) { + this.domainListDate = domainListDate; + this.domainListGenerationDate = domainListGenerationDate; + } + + public String getDomainListDate() { + return domainListDate; + } + + public String getDomainListGenerationDate() { + return domainListGenerationDate; + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/domainmetrica/MetricaThreatData.java b/core/src/main/java/google/registry/mosapi/dto/domainmetrica/MetricaThreatData.java new file mode 100644 index 00000000000..2223291be79 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/domainmetrica/MetricaThreatData.java @@ -0,0 +1,73 @@ +// Copyright 2025 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.mosapi.dto.domainmetrica; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.util.List; + +/** Represents one entry in the domainListData array of a {@link MetricaReport}. */ +public final class MetricaThreatData { + /** + * A JSON string describing the type of DNS abuse. Known types include: spam, phishing, botnetCc + * and malware. + * + * @see ICANN MoSAPI Specification, + * Section 9.1 + */ + @Expose + @SerializedName("threatType") + private final String threatType; + + /** + * A JSON number containing the number of domains for the type of abuse. If this value is set to + * "-1" the specific threat Type is disabled and is not currently being monitored. + * + * @see ICANN MoSAPI Specification, + * Section 9.1 + */ + @Expose + @SerializedName("count") + private final int count; + + /** + * A JSON array of JSON strings containing the domain names presenting abuse. This list may not + * include all the domains identified in the "count" element. + * + * @see ICANN MoSAPI Specification, + * Section 9.1 + */ + @Expose + @SerializedName("domains") + private final List domains; + + public MetricaThreatData(String threatType, int count, List domains) { + this.threatType = threatType; + this.count = count; + this.domains = domains; + } + + public String getThreatType() { + return threatType; + } + + public int getCount() { + return count; + } + + public List getDomains() { + return domains; + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ActiveIncidentsSummary.java b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ActiveIncidentsSummary.java new file mode 100644 index 00000000000..128fec7863c --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ActiveIncidentsSummary.java @@ -0,0 +1,76 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.util.List; + +/** + * A summary of active incidents for a specific service that is down. + * + * @see ICANN MoSAPI Specification, Section + * 5.1 + */ +public final class ActiveIncidentsSummary { + /** + * The name of the service being monitored (e.g., "DNS", "RDDS"). + * + * @see ICANN MoSAPI Specification, + * Section 5.1 + */ + @Expose + @SerializedName("service") + private final String service; + + /** + * A JSON number that contains the current percentage of the Emergency Threshold of the Service. + * + * @see ICANN MoSAPI Specification, + * Section 5.1 + */ + @Expose + @SerializedName("emergencyThreshold") + private final double emergencyThreshold; + + /** + * A JSON array that contains "incident" objects representing active or resolved incidents for + * this service. + * + * @see ICANN MoSAPI Specification, + * Section 5.1 + */ + @Expose + @SerializedName("incidents") + private final List incidents; + + public ActiveIncidentsSummary( + String service, double emergencyThreshold, List incidents) { + this.service = service; + this.emergencyThreshold = emergencyThreshold; + this.incidents = incidents; + } + + public String getService() { + return service; + } + + public double getEmergencyThreshold() { + return emergencyThreshold; + } + + public List getIncidents() { + return incidents; + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/AlarmResponse.java b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/AlarmResponse.java new file mode 100644 index 00000000000..426f76ba8e4 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/AlarmResponse.java @@ -0,0 +1,37 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import com.google.gson.annotations.Expose; +import java.util.List; + +/** Represents the aggregated alarm status for all monitored entities. */ +public final class AlarmResponse { + /** + * A list of alarm statuses for individual services. + * + * @see ICANN MoSAPI Specification, + * Section 5.2 + */ + @Expose private final List alarmStatuses; + + public AlarmResponse(List alarmStatuses) { + this.alarmStatuses = alarmStatuses; + } + + public List getAlarmStatuses() { + return alarmStatuses; + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/AlarmStatus.java b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/AlarmStatus.java new file mode 100644 index 00000000000..70a0dddf7ae --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/AlarmStatus.java @@ -0,0 +1,63 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import com.google.gson.annotations.Expose; +import javax.annotation.Nullable; + +/** Represents the alarm status for a single TLD and service combination. */ +public final class AlarmStatus { + + // The TLD being monitored. + @Expose private final String tld; + + // The service being monitored (e.g. "dns", "rdds"). + @Expose private final String service; + + /** + * The alarm status (e.g. "Yes", "No", "Disabled"). + * + * @see ICANN MoSAPI Specification, + * Section 5.2 + */ + @Expose private final String status; + + /** An optional error message if the status could not be retrieved. */ + @Expose @Nullable private final String errorMessage; + + public AlarmStatus(String tld, String service, String status, @Nullable String errorMessage) { + this.tld = tld; + this.service = service; + this.status = status; + this.errorMessage = errorMessage; + } + + public String getTld() { + return tld; + } + + public String getService() { + return service; + } + + public String getStatus() { + return status; + } + + @Nullable + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/AllServicesStateResponse.java b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/AllServicesStateResponse.java new file mode 100644 index 00000000000..c3aaded4ec4 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/AllServicesStateResponse.java @@ -0,0 +1,43 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.util.List; + +/** + * A wrapper response containing the state summaries of all monitored services. + * + *

This corresponds to the collection of service statuses returned when monitoring the state of a + * TLD + * + * @see ICANN MoSAPI Specification, Section + * 5.1 + */ +public final class AllServicesStateResponse { + + // A list of state summaries for each monitored service (e.g. DNS, RDDS, etc.) + @Expose + @SerializedName("serviceStates") + private final List serviceStates; + + public AllServicesStateResponse(List serviceStates) { + this.serviceStates = serviceStates; + } + + public List getServiceStates() { + return serviceStates; + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/AllTldsDowntime.java b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/AllTldsDowntime.java new file mode 100644 index 00000000000..698efcb78ab --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/AllTldsDowntime.java @@ -0,0 +1,42 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.util.List; + +/** + * A wrapper for a list of {@link TldServicesDowntime} objects. + * + *

This class acts as a container for aggregating downtime information across multiple TLDs or + * services, relating to the downtime measurements defined in the specification. + * + * @see ICANN MoSAPI Specification, Section + * 5.3 + */ +public final class AllTldsDowntime { + // A list of downtime metrics for monitored TLDs/Services + @Expose + @SerializedName("allDowntimes") + private final List allDowntimes; + + public AllTldsDowntime(List allDowntimes) { + this.allDowntimes = allDowntimes; + } + + public List getAllDowntimes() { + return allDowntimes; + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/IncidentSummary.java b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/IncidentSummary.java new file mode 100644 index 00000000000..b30da682b93 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/IncidentSummary.java @@ -0,0 +1,77 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import javax.annotation.Nullable; + +/** + * A summary of a service incident. + * + * @see ICANN MoSAPI Specification, Section + * 5.1 + */ +public final class IncidentSummary { + @Expose + @SerializedName("incidentID") + private String incidentID; + + @Expose + @SerializedName("startTime") + private long startTime; + + @Expose + @SerializedName("falsePositive") + private boolean falsePositive; + + @Expose + @SerializedName("state") + private String state; + + @Expose + @SerializedName("endTime") + @Nullable + private Long endTime; + + public IncidentSummary( + String incidentID, long startTime, boolean falsePositive, String state, Long endTime) { + this.incidentID = incidentID; + this.startTime = startTime; + this.falsePositive = falsePositive; + this.state = state; + this.endTime = endTime; + } + + public String getIncidentID() { + return incidentID; + } + + public long getStartTime() { + return startTime; + } + + public boolean isFalsePositive() { + return falsePositive; + } + + public String getState() { + return state; + } + + public Long getEndTime() { + return endTime; + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ServiceAlarm.java b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ServiceAlarm.java new file mode 100644 index 00000000000..2007fef5509 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ServiceAlarm.java @@ -0,0 +1,44 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import com.google.gson.annotations.Expose; + +/** Represents the alarm status for a specific service. */ +public final class ServiceAlarm { + @Expose private int version; + @Expose private long lastUpdateApiDatabase; + + // A JSON string that contains one of the following values: "Yes", "No", or "Disabled" + @Expose private String alarmed; + + public ServiceAlarm(int version, long lastUpdateApiDatabase, String alarmed) { + this.version = version; + this.lastUpdateApiDatabase = lastUpdateApiDatabase; + this.alarmed = alarmed; + } + + public int getVersion() { + return version; + } + + public long getLastUpdateApiDatabase() { + return lastUpdateApiDatabase; + } + + public String getAlarmed() { + return alarmed; + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ServiceDowntime.java b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ServiceDowntime.java new file mode 100644 index 00000000000..77cecc55923 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ServiceDowntime.java @@ -0,0 +1,57 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import com.google.gson.annotations.Expose; + +/** + * Represents the downtime information for a specific service. + * + * @see ICANN MoSAPI Specification, Section + * 5.3 + */ +public final class ServiceDowntime { + private int version; + private long lastUpdateApiDatabase; + + // A JSON number that contains the number of minutes of downtime of the Service during a rolling + // week period. + @Expose private int downtime; + @Expose private Boolean disabledMonitoring; + + public ServiceDowntime( + int version, long lastUpdateApiDatabase, int downtime, Boolean disabledMonitoring) { + this.version = version; + this.lastUpdateApiDatabase = lastUpdateApiDatabase; + this.downtime = downtime; + this.disabledMonitoring = disabledMonitoring; + } + + public int getVersion() { + return version; + } + + public long getLastUpdateApiDatabase() { + return lastUpdateApiDatabase; + } + + public int getDowntime() { + return downtime; + } + + public Boolean getDisabledMonitoring() { + return disabledMonitoring; + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ServiceStateSummary.java b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ServiceStateSummary.java new file mode 100644 index 00000000000..62ce1f357f1 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ServiceStateSummary.java @@ -0,0 +1,69 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.util.List; +import javax.annotation.Nullable; + +/** + * A curated summary of the service state for a TLD. + * + *

This class aggregates the high-level status of a TLD and details of any active incidents + * affecting specific services (like DNS or RDDS), based on the data structures defined in the + * MoSAPI specification. + * + * @see ICANN MoSAPI Specification, Section + * 5.1 + */ +public final class ServiceStateSummary { + @Expose + @SerializedName("tld") + private final String tld; + + // The overall status of the TLD (e.g. "Up", "Down", "UP-inconclusive") + @Expose + @SerializedName("overallStatus") + private final String overallStatus; + + /* + A list of summaries for services that currently have active incidents. May be null or empty if + the status is "up" + */ + @Expose + @SerializedName("activeIncidents") + @Nullable + private final List activeIncidents; + + public ServiceStateSummary( + String tld, String overallStatus, @Nullable List activeIncidents) { + this.tld = tld; + this.overallStatus = overallStatus; + this.activeIncidents = activeIncidents; + } + + public String getTld() { + return tld; + } + + public String getOverallStatus() { + return overallStatus; + } + + @Nullable + public List getActiveIncidents() { + return activeIncidents; + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ServiceStatus.java b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ServiceStatus.java new file mode 100644 index 00000000000..cbe9a2e6619 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/ServiceStatus.java @@ -0,0 +1,53 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import java.util.List; + +/** Represents the status of a single monitored service. */ +public final class ServiceStatus { + /** + * A JSON string that contains the status of the Service as seen from the monitoring system. + * Possible values include "Up", "Down", "Disabled", "UP-inconclusive-no-data", etc. + * + * @see ICANN MoSAPI Specification, + * Section 5.1 + */ + private String status; + + // A JSON number that contains the current percentage of the Emergency Threshold + // of the Service. A value of "0" specifies that there are no Incidents + // affecting the threshold. + private double emergencyThreshold; + private List incidents; + + public ServiceStatus(String status, double emergencyThreshold, List incidents) { + this.status = status; + this.emergencyThreshold = emergencyThreshold; + this.incidents = incidents; + } + + public String getStatus() { + return status; + } + + public double getEmergencyThreshold() { + return emergencyThreshold; + } + + public List getIncidents() { + return incidents; + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/TldServiceState.java b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/TldServiceState.java new file mode 100644 index 00000000000..ba2e4f89352 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/TldServiceState.java @@ -0,0 +1,66 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.util.Map; + +/** + * Represents the overall health of all monitored services for a TLD. + * + * @see ICANN MoSAPI Specification, Section + * 5.1 + */ +public final class TldServiceState { + @Expose private final String tld; + private final long lastUpdateApiDatabase; + + // A JSON string that contains the status of the TLD as seen from the monitoring system + @Expose private final String status; + + // A JSON object containing detailed information for each potential monitored service (i.e., DNS, + // RDDS, EPP, DNSSEC, RDAP). + @Expose + @SerializedName("testedServices") + private final Map serviceStatuses; + + public TldServiceState( + String tld, + long lastUpdateApiDatabase, + String status, + Map serviceStatuses) { + this.tld = tld; + this.lastUpdateApiDatabase = lastUpdateApiDatabase; + this.status = status; + this.serviceStatuses = serviceStatuses; + } + + public String getTld() { + return tld; + } + + public long getLastUpdateApiDatabase() { + return lastUpdateApiDatabase; + } + + public String getStatus() { + return status; + } + + public Map getServiceStatuses() { + return serviceStatuses; + } +} diff --git a/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/TldServicesDowntime.java b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/TldServicesDowntime.java new file mode 100644 index 00000000000..474a3bfafe9 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/dto/servicemonitoring/TldServicesDowntime.java @@ -0,0 +1,43 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.util.Map; + +/** Represents a collection of downtime reports for all services on a given TLD. */ +public final class TldServicesDowntime { + @Expose + @SerializedName("tld") + private final String tld; + + @Expose + @SerializedName("serviceDowntime") + private final Map serviceDowntime; + + public TldServicesDowntime(String tld, Map serviceDowntime) { + this.tld = tld; + this.serviceDowntime = serviceDowntime; + } + + public String getTld() { + return tld; + } + + public Map getServiceDowntime() { + return serviceDowntime; + } +} diff --git a/core/src/main/java/google/registry/mosapi/exception/MosApiException.java b/core/src/main/java/google/registry/mosapi/exception/MosApiException.java new file mode 100644 index 00000000000..98f25cb93e7 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/exception/MosApiException.java @@ -0,0 +1,52 @@ +// Copyright 2025 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.mosapi.exception; + +import google.registry.mosapi.dto.MosApiErrorResponse; +import java.io.IOException; + +/** Custom exception for MoSAPI client errors. */ +public class MosApiException extends IOException { + + public MosApiException(String message) { + super(message); + } + + public MosApiException(String message, Throwable cause) { + super(message, cause); + } + + /** Thrown when MoSAPI returns a 401 Unauthorized error. */ + public static class MosApiAuthorizationException extends MosApiException { + + public MosApiAuthorizationException(String message) { + super(message); + } + } + + /** Creates a specific exception message based on the MoSAPI error response. */ + public static MosApiException create(MosApiErrorResponse errorResponse) { + String message = + switch (errorResponse.resultCode()) { + case "2012" -> "Date order is invalid: " + errorResponse.message(); + case "2013", "2014" -> "Date syntax is invalid: " + errorResponse.message(); + default -> + String.format( + "Bad Request (code: %s): %s", + errorResponse.resultCode(), errorResponse.message()); + }; + return new MosApiException(message); + } +} diff --git a/core/src/main/java/google/registry/mosapi/module/MosApiModule.java b/core/src/main/java/google/registry/mosapi/module/MosApiModule.java new file mode 100644 index 00000000000..79550471934 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/module/MosApiModule.java @@ -0,0 +1,57 @@ +// Copyright 2025 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.mosapi.module; + +import dagger.Module; +import dagger.Provides; +import google.registry.privileges.secretmanager.SecretManagerClient; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import java.util.Optional; + +@Module +public final class MosApiModule { + /** + * Provides a Provider for the MoSAPI TLS Cert. + * + *

This method returns a Dagger {@link Provider} that can be used to fetch the TLS Certs for a + * MosAPI. + * + * @param secretManagerClient The injected Secret Manager client. + * @return A Provider for the MoSAPI TLS Certs. + */ + @Provides + @Named("mosapiTlsCert") + public static String provideMosapiTlsCert(SecretManagerClient secretManagerClient) { + return secretManagerClient.getSecretData( + "nomulus-dot-foo_tls-client-dot-crt-dot-pem", Optional.of("latest")); + } + + /** + * Provides a Provider for the MoSAPI TLS Key. + * + *

This method returns a Dagger {@link Provider} that can be used to fetch the TLS Key for a + * MosAPI. + * + * @param secretManagerClient The injected Secret Manager client. + * @return A Provider for the MoSAPI TLS Key. + */ + @Provides + @Named("mosapiTlsKey") + public static String provideMosapiTlsKey(SecretManagerClient secretManagerClient) { + return secretManagerClient.getSecretData( + "nomulus-dot-foo_tls-client-dot-key", Optional.of("latest")); + } +} diff --git a/core/src/main/java/google/registry/mosapi/module/MosApiRequestModule.java b/core/src/main/java/google/registry/mosapi/module/MosApiRequestModule.java new file mode 100644 index 00000000000..08269e81afd --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/module/MosApiRequestModule.java @@ -0,0 +1,79 @@ +// Copyright 2025 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.mosapi.module; + +import static google.registry.request.RequestParameters.extractOptionalParameter; + +import dagger.Module; +import dagger.Provides; +import google.registry.request.Parameter; +import jakarta.servlet.http.HttpServletRequest; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.Optional; + +/** Dagger module for MoSAPI requests. */ +@Module +public final class MosApiRequestModule { + + @Provides + @Parameter("date") + static Optional provideDate(HttpServletRequest req) { + Optional dateString = extractOptionalParameter(req, "date"); + if (dateString.isEmpty()) { + return Optional.empty(); + } + try { + return dateString.map(LocalDate::parse); + } catch (DateTimeParseException e) { + // For optional parameters, we can return empty on parse error. + return Optional.empty(); + } + } + + @Provides + @Parameter("tld") + static Optional provideTld(HttpServletRequest req) { + return extractOptionalParameter(req, "tld"); + } + + @Provides + @Parameter("startDate") + static Optional provideStartDate(HttpServletRequest req) { + Optional dateString = extractOptionalParameter(req, "startDate"); + if (dateString.isEmpty()) { + return Optional.empty(); + } + try { + return dateString.map(LocalDate::parse); + } catch (DateTimeParseException e) { + return Optional.empty(); + } + } + + @Provides + @Parameter("endDate") + static Optional provideEndDate(HttpServletRequest req) { + Optional dateString = extractOptionalParameter(req, "endDate"); + if (dateString.isEmpty()) { + return Optional.empty(); + } + try { + return dateString.map(LocalDate::parse); + } catch (DateTimeParseException e) { + return Optional.empty(); + } + } +} diff --git a/core/src/main/java/google/registry/mosapi/package-info.java b/core/src/main/java/google/registry/mosapi/package-info.java new file mode 100644 index 00000000000..5c5056dccf5 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/package-info.java @@ -0,0 +1,16 @@ +// Copyright 2025 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. + +@javax.annotation.ParametersAreNonnullByDefault +package google.registry.mosapi; diff --git a/core/src/main/java/google/registry/mosapi/services/MosApiAlarmService.java b/core/src/main/java/google/registry/mosapi/services/MosApiAlarmService.java new file mode 100644 index 00000000000..c69ee52bb77 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/services/MosApiAlarmService.java @@ -0,0 +1,63 @@ +// Copyright 2025 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.mosapi.services; + +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.FluentLogger; +import google.registry.config.RegistryConfig.Config; +import google.registry.mosapi.client.ServiceMonitoringClient; +import google.registry.mosapi.dto.servicemonitoring.AlarmResponse; +import google.registry.mosapi.dto.servicemonitoring.AlarmStatus; +import google.registry.mosapi.dto.servicemonitoring.ServiceAlarm; +import google.registry.mosapi.exception.MosApiException; +import jakarta.inject.Inject; +import java.util.List; + +/** Service to check the alarm status for all configured MoSAPI entities and services. */ +public class MosApiAlarmService { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private final ServiceMonitoringClient serviceMonitoringClient; + private final List tlds; + private final List services; + + @Inject + public MosApiAlarmService( + ServiceMonitoringClient serviceMonitoringClient, + @Config("mosapiTlds") List tlds, + @Config("mosapiServices") List services) { + this.serviceMonitoringClient = serviceMonitoringClient; + this.tlds = tlds; + this.services = services; + } + + public AlarmResponse checkAllAlarms() { + ImmutableList.Builder statuses = new ImmutableList.Builder<>(); + for (String tld : tlds) { + for (String service : services) { + try { + // Call the client to get the full alarm object + ServiceAlarm alarm = serviceMonitoringClient.serviceAlarmed(tld, service); + // Extract the status string to build the final response object + statuses.add(new AlarmStatus(tld, service, alarm.getAlarmed(), null)); + } catch (MosApiException e) { + logger.atWarning().withCause(e).log( + "Failed to get alarm status for tld %s and service %s.", tld, service); + statuses.add(new AlarmStatus(tld, service, "ERROR", e.getMessage())); + } + } + } + return new AlarmResponse(statuses.build()); + } +} diff --git a/core/src/main/java/google/registry/mosapi/services/MosApiDowntimeService.java b/core/src/main/java/google/registry/mosapi/services/MosApiDowntimeService.java new file mode 100644 index 00000000000..a111e5df09b --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/services/MosApiDowntimeService.java @@ -0,0 +1,70 @@ +// Copyright 2025 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.mosapi.services; + +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.FluentLogger; +import google.registry.config.RegistryConfig.Config; +import google.registry.mosapi.client.ServiceMonitoringClient; +import google.registry.mosapi.dto.servicemonitoring.AllTldsDowntime; +import google.registry.mosapi.dto.servicemonitoring.ServiceDowntime; +import google.registry.mosapi.dto.servicemonitoring.TldServicesDowntime; +import google.registry.mosapi.exception.MosApiException; +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Service to fetch downtime reports from the MoSAPI. */ +public class MosApiDowntimeService { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private final ServiceMonitoringClient serviceMonitoringClient; + private final List tlds; + private final List services; + + @Inject + public MosApiDowntimeService( + ServiceMonitoringClient serviceMonitoringClient, + @Config("mosapiTlds") List tlds, + @Config("mosapiServices") List services) { + this.serviceMonitoringClient = serviceMonitoringClient; + this.tlds = tlds; + this.services = services; + } + + /** Fetches the downtime for all configured services for a given TLD. */ + public TldServicesDowntime getDowntimeForTld(String tld) { + Map serviceDowntimes = new HashMap<>(); + for (String service : services) { + try { + serviceDowntimes.put(service, serviceMonitoringClient.getDowntime(tld, service)); + } catch (MosApiException e) { + logger.atWarning().withCause(e).log( + "Failed to get service downtime for TLD %s and service %s.", tld, service); + } + } + return new TldServicesDowntime(tld, serviceDowntimes); + } + + /** Fetches the downtime for all configured services across all configured TLDs. */ + public AllTldsDowntime getDowntimeForAllTlds() { + ImmutableList.Builder allDowntimes = new ImmutableList.Builder<>(); + for (String tld : tlds) { + allDowntimes.add(getDowntimeForTld(tld)); + } + return new AllTldsDowntime(allDowntimes.build()); + } +} diff --git a/core/src/main/java/google/registry/mosapi/services/MosApiMetricaService.java b/core/src/main/java/google/registry/mosapi/services/MosApiMetricaService.java new file mode 100644 index 00000000000..fed24f98bb3 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/services/MosApiMetricaService.java @@ -0,0 +1,59 @@ +// Copyright 2025 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.mosapi.services; + +import google.registry.mosapi.client.DomainMetricaClient; +import google.registry.mosapi.dto.domainmetrica.MetricaReport; +import google.registry.mosapi.dto.domainmetrica.MetricaReportInfo; +import google.registry.mosapi.exception.MosApiException; +import jakarta.inject.Inject; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +/** Service to fetch Domain METRICA reports from the MoSAPI. */ +public class MosApiMetricaService { + private final DomainMetricaClient metricaClient; + + @Inject + public MosApiMetricaService(DomainMetricaClient metricaClient) { + this.metricaClient = metricaClient; + } + + /** + * Fetches a METRICA report for a given TLD. + * + * @param tld the TLD to query for. + * @param date if present, fetches the report for this specific date, otherwise fetches the latest + * available report. + * @return The {@link MetricaReport}. + * @throws MosApiException if the API call fails. + */ + public MetricaReport getReport(String tld, Optional date) throws MosApiException { + if (date.isPresent()) { + return metricaClient.getMetricaReportForDate(tld, date.get()); + } else { + return metricaClient.getLatestMetricaReport(tld); + } + } + + /** Lists available Domain METRICA report dates for a given TLD and optional date range. */ + public List listAvailableReports( + String tld, Optional startDate, Optional endDate) + throws MosApiException { + return metricaClient.listAvailableMetricaReports( + tld, startDate.orElse(null), endDate.orElse(null)); + } +} diff --git a/core/src/main/java/google/registry/mosapi/services/MosApiStateService.java b/core/src/main/java/google/registry/mosapi/services/MosApiStateService.java new file mode 100644 index 00000000000..bf205d4ae00 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/services/MosApiStateService.java @@ -0,0 +1,88 @@ +// Copyright 2025 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.mosapi.services; + +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.FluentLogger; +import google.registry.config.RegistryConfig.Config; +import google.registry.mosapi.client.ServiceMonitoringClient; +import google.registry.mosapi.dto.servicemonitoring.ActiveIncidentsSummary; +import google.registry.mosapi.dto.servicemonitoring.AllServicesStateResponse; +import google.registry.mosapi.dto.servicemonitoring.ServiceStateSummary; +import google.registry.mosapi.dto.servicemonitoring.ServiceStatus; +import google.registry.mosapi.dto.servicemonitoring.TldServiceState; +import google.registry.mosapi.exception.MosApiException; +import jakarta.inject.Inject; +import java.util.List; +import java.util.stream.Collectors; + +/** A service that provides business logic for interacting with MoSAPI Service State. */ +public class MosApiStateService { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private final ServiceMonitoringClient serviceMonitoringClient; + private final List tlds; + + private final String downStatus = "Down"; + + @Inject + public MosApiStateService( + ServiceMonitoringClient serviceMonitoringClient, @Config("mosapiTlds") List tlds) { + this.serviceMonitoringClient = serviceMonitoringClient; + this.tlds = tlds; + } + + /** Fetches and transforms the service state for a given TLD into a summary. */ + public ServiceStateSummary getServiceStateSummary(String tld) throws MosApiException { + TldServiceState rawState = serviceMonitoringClient.getServiceState(tld); + return transformToSummary(rawState); + } + + /** Fetches and transforms the service state for all configured TLDs. */ + public AllServicesStateResponse getAllServiceStateSummaries() { + ImmutableList.Builder summaries = new ImmutableList.Builder<>(); + for (String tld : tlds) { + try { + summaries.add(getServiceStateSummary(tld)); + } catch (MosApiException e) { + logger.atWarning().withCause(e).log("Failed to get service state for TLD %s.", tld); + // Add a summary indicating the error for this TLD + summaries.add(new ServiceStateSummary(tld, "ERROR", null)); + } + } + return new AllServicesStateResponse(summaries.build()); + } + + private ServiceStateSummary transformToSummary(TldServiceState rawState) { + List activeIncidents = null; + if (downStatus.equalsIgnoreCase(rawState.getStatus())) { + activeIncidents = + rawState.getServiceStatuses().entrySet().stream() + .filter( + entry -> { + ServiceStatus service = entry.getValue(); + return service.getIncidents() != null && !service.getIncidents().isEmpty(); + }) + .map( + entry -> + new ActiveIncidentsSummary( + // key is the service name + entry.getKey(), + entry.getValue().getEmergencyThreshold(), + entry.getValue().getIncidents())) + .collect(Collectors.toList()); + } + return new ServiceStateSummary(rawState.getTld(), rawState.getStatus(), activeIncidents); + } +} diff --git a/core/src/test/java/google/registry/module/TestRegistryComponent.java b/core/src/test/java/google/registry/module/TestRegistryComponent.java index 94129fcbe4b..edb06a89eac 100644 --- a/core/src/test/java/google/registry/module/TestRegistryComponent.java +++ b/core/src/test/java/google/registry/module/TestRegistryComponent.java @@ -34,6 +34,7 @@ import google.registry.keyring.api.KeyModule; import google.registry.module.TestRequestComponent.TestRequestComponentModule; import google.registry.monitoring.whitebox.StackdriverModule; +import google.registry.mosapi.module.MosApiModule; import google.registry.persistence.PersistenceModule; import google.registry.privileges.secretmanager.SecretManagerModule; import google.registry.rde.JSchModule; @@ -41,6 +42,7 @@ import google.registry.request.Modules.NetHttpTransportModule; import google.registry.request.Modules.UrlConnectionServiceModule; import google.registry.request.auth.AuthModule; +import google.registry.util.HttpModule; import google.registry.util.UtilsModule; import jakarta.inject.Singleton; @@ -61,6 +63,8 @@ GroupsModule.class, GroupssettingsModule.class, GsonModule.class, + HttpModule.class, + MosApiModule.class, JSchModule.class, KeyModule.class, KeyringModule.class, diff --git a/core/src/test/java/google/registry/module/TestRequestComponent.java b/core/src/test/java/google/registry/module/TestRequestComponent.java index 85f4e4d508f..fdd099a7191 100644 --- a/core/src/test/java/google/registry/module/TestRequestComponent.java +++ b/core/src/test/java/google/registry/module/TestRequestComponent.java @@ -28,6 +28,7 @@ import google.registry.flows.custom.CustomLogicModule; import google.registry.loadtest.LoadTestModule; import google.registry.monitoring.whitebox.WhiteboxModule; +import google.registry.mosapi.module.MosApiRequestModule; import google.registry.rdap.RdapModule; import google.registry.rde.RdeModule; import google.registry.reporting.ReportingModule; @@ -60,6 +61,7 @@ EppToolModule.class, IcannReportingModule.class, LoadTestModule.class, + MosApiRequestModule.class, RdapModule.class, RdeModule.class, ReportingModule.class, diff --git a/core/src/test/java/google/registry/mosapi/action/GetAlarmsStateActionTest.java b/core/src/test/java/google/registry/mosapi/action/GetAlarmsStateActionTest.java new file mode 100644 index 00000000000..9ae7b974e06 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/action/GetAlarmsStateActionTest.java @@ -0,0 +1,63 @@ +// Copyright 2025 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.mosapi.action; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.common.net.MediaType; +import com.google.gson.Gson; +import google.registry.mosapi.dto.servicemonitoring.AlarmResponse; +import google.registry.mosapi.dto.servicemonitoring.AlarmStatus; +import google.registry.mosapi.services.MosApiAlarmService; +import google.registry.testing.FakeResponse; +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 GetAlarmsStateAction}. */ +@ExtendWith(MockitoExtension.class) +public class GetAlarmsStateActionTest { + @Mock private MosApiAlarmService alarmService; + private final FakeResponse response = new FakeResponse(); + private final Gson gson = new Gson(); + private GetAlarmsStateAction action; + + @BeforeEach + void beforeEach() { + action = new GetAlarmsStateAction(alarmService, response, gson); + } + + @Test + void testRun_returnsAlarmData() { + // Create actual objects rather than Maps to avoid ClassCastException + AlarmStatus status = new AlarmStatus("example", "dns", "Up", null); + AlarmResponse alarmResponse = new AlarmResponse(ImmutableList.of(status)); + + when(alarmService.checkAllAlarms()).thenReturn(alarmResponse); + + action.run(); + + assertThat(response.getContentType()).isEqualTo(MediaType.JSON_UTF_8); + // Validate JSON contains expected fields + String payload = response.getPayload(); + assertThat(payload).contains("\"service\":\"dns\""); + assertThat(payload).contains("\"status\":\"Up\""); + verify(alarmService).checkAllAlarms(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/action/GetMetricaReportActionTest.java b/core/src/test/java/google/registry/mosapi/action/GetMetricaReportActionTest.java new file mode 100644 index 00000000000..470cb70cc7d --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/action/GetMetricaReportActionTest.java @@ -0,0 +1,79 @@ +// Copyright 2025 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.mosapi.action; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.common.net.MediaType; +import com.google.gson.Gson; +import google.registry.mosapi.dto.domainmetrica.MetricaReport; +import google.registry.mosapi.exception.MosApiException; +import google.registry.mosapi.services.MosApiMetricaService; +import google.registry.request.HttpException.ServiceUnavailableException; +import google.registry.testing.FakeResponse; +import java.time.LocalDate; +import java.util.Optional; +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 GetMetricaReportAction}. */ +@ExtendWith(MockitoExtension.class) +public class GetMetricaReportActionTest { + @Mock private MosApiMetricaService metricaService; + private final FakeResponse response = new FakeResponse(); + private final Gson gson = new Gson(); + private final String tld = "example"; + private final LocalDate date = LocalDate.of(2025, 1, 1); + + private GetMetricaReportAction action; + + @BeforeEach + void beforeEach() { + action = new GetMetricaReportAction(metricaService, response, gson, tld, Optional.of(date)); + } + + @Test + void testRun_returnsReport() throws Exception { + MetricaReport mockReport = + new MetricaReport(2, "example", "2025-01-01", 10, ImmutableList.of()); + + when(metricaService.getReport(tld, Optional.of(date))).thenReturn(mockReport); + + action.run(); + + assertThat(response.getContentType()).isEqualTo(MediaType.JSON_UTF_8); + assertThat(response.getPayload()).contains("\"tld\":\"example\""); + assertThat(response.getPayload()).contains("\"uniqueAbuseDomains\":10"); + verify(metricaService).getReport(tld, Optional.of(date)); + } + + @Test + void testRun_serviceThrowsMosApiException_throwsServiceUnavailable() throws Exception { + doThrow(new MosApiException("Backend failure")).when(metricaService).getReport(any(), any()); + + ServiceUnavailableException thrown = + assertThrows(ServiceUnavailableException.class, action::run); + + assertThat(thrown).hasMessageThat().isEqualTo("Error fetching METRICA report."); + } +} diff --git a/core/src/test/java/google/registry/mosapi/action/GetServiceDowntimeActionTest.java b/core/src/test/java/google/registry/mosapi/action/GetServiceDowntimeActionTest.java new file mode 100644 index 00000000000..82e337594c7 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/action/GetServiceDowntimeActionTest.java @@ -0,0 +1,71 @@ +// Copyright 2025 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.mosapi.action; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.net.MediaType; +import com.google.gson.Gson; +import google.registry.mosapi.dto.servicemonitoring.AllTldsDowntime; +import google.registry.mosapi.dto.servicemonitoring.TldServicesDowntime; +import google.registry.mosapi.services.MosApiDowntimeService; +import google.registry.testing.FakeResponse; +import java.util.Optional; +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 GetServiceDowntimeAction}. */ +@ExtendWith(MockitoExtension.class) +public class GetServiceDowntimeActionTest { + @Mock private MosApiDowntimeService downtimeService; + private final FakeResponse response = new FakeResponse(); + private final Gson gson = new Gson(); + + @Test + void testRun_singleTld_returnsDowntimeForTld() { + GetServiceDowntimeAction action = + new GetServiceDowntimeAction(downtimeService, response, gson, Optional.of("example")); + + TldServicesDowntime mockDowntime = new TldServicesDowntime("example", ImmutableMap.of()); + when(downtimeService.getDowntimeForTld("example")).thenReturn(mockDowntime); + + action.run(); + + assertThat(response.getContentType()).isEqualTo(MediaType.JSON_UTF_8); + assertThat(response.getPayload()).contains("\"tld\":\"example\""); + verify(downtimeService).getDowntimeForTld("example"); + } + + @Test + void testRun_noTld_returnsDowntimeForAll() { + GetServiceDowntimeAction action = + new GetServiceDowntimeAction(downtimeService, response, gson, Optional.empty()); + + AllTldsDowntime mockAllDowntime = new AllTldsDowntime(ImmutableList.of()); + when(downtimeService.getDowntimeForAllTlds()).thenReturn(mockAllDowntime); + + action.run(); + + assertThat(response.getContentType()).isEqualTo(MediaType.JSON_UTF_8); + assertThat(response.getPayload()).contains("\"allDowntimes\":[]"); + verify(downtimeService).getDowntimeForAllTlds(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/action/GetServiceStateActionTest.java b/core/src/test/java/google/registry/mosapi/action/GetServiceStateActionTest.java new file mode 100644 index 00000000000..028c249582e --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/action/GetServiceStateActionTest.java @@ -0,0 +1,88 @@ +// Copyright 2025 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.mosapi.action; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.common.net.MediaType; +import com.google.gson.Gson; +import google.registry.mosapi.dto.servicemonitoring.AllServicesStateResponse; +import google.registry.mosapi.dto.servicemonitoring.ServiceStateSummary; +import google.registry.mosapi.exception.MosApiException; +import google.registry.mosapi.services.MosApiStateService; +import google.registry.request.HttpException.ServiceUnavailableException; +import google.registry.testing.FakeResponse; +import java.util.Optional; +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 GetServiceStateAction}. */ +@ExtendWith(MockitoExtension.class) +public class GetServiceStateActionTest { + @Mock private MosApiStateService stateService; + private final FakeResponse response = new FakeResponse(); + private final Gson gson = new Gson(); + + @Test + void testRun_singleTld_returnsStateForTld() throws Exception { + GetServiceStateAction action = + new GetServiceStateAction(stateService, response, gson, Optional.of("example")); + + ServiceStateSummary summary = new ServiceStateSummary("example", "Up", null); + when(stateService.getServiceStateSummary("example")).thenReturn(summary); + + action.run(); + + assertThat(response.getContentType()).isEqualTo(MediaType.JSON_UTF_8); + assertThat(response.getPayload()).contains("\"overallStatus\":\"Up\""); + verify(stateService).getServiceStateSummary("example"); + } + + @Test + void testRun_noTld_returnsStateForAll() { + GetServiceStateAction action = + new GetServiceStateAction(stateService, response, gson, Optional.empty()); + + AllServicesStateResponse allStates = new AllServicesStateResponse(ImmutableList.of()); + when(stateService.getAllServiceStateSummaries()).thenReturn(allStates); + + action.run(); + + assertThat(response.getContentType()).isEqualTo(MediaType.JSON_UTF_8); + assertThat(response.getPayload()).contains("\"serviceStates\":[]"); + verify(stateService).getAllServiceStateSummaries(); + } + + @Test + void testRun_serviceThrowsException_throwsServiceUnavailable() throws Exception { + GetServiceStateAction action = + new GetServiceStateAction(stateService, response, gson, Optional.of("example")); + + doThrow(new MosApiException("Backend error")) + .when(stateService) + .getServiceStateSummary("example"); + + ServiceUnavailableException thrown = + assertThrows(ServiceUnavailableException.class, action::run); + + assertThat(thrown).hasMessageThat().isEqualTo("Error fetching MoSAPI service state."); + } +} diff --git a/core/src/test/java/google/registry/mosapi/action/ListMetricaReportsActionTest.java b/core/src/test/java/google/registry/mosapi/action/ListMetricaReportsActionTest.java new file mode 100644 index 00000000000..d486dffa3b3 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/action/ListMetricaReportsActionTest.java @@ -0,0 +1,94 @@ +// Copyright 2025 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.mosapi.action; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.common.net.MediaType; +import com.google.gson.Gson; +import google.registry.mosapi.dto.domainmetrica.MetricaReportInfo; +import google.registry.mosapi.exception.MosApiException; +import google.registry.mosapi.services.MosApiMetricaService; +import google.registry.request.HttpException.ServiceUnavailableException; +import google.registry.testing.FakeResponse; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +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 ListMetricaReportsAction}. */ +@ExtendWith(MockitoExtension.class) +public class ListMetricaReportsActionTest { + @Mock private MosApiMetricaService metricaService; + private final FakeResponse response = new FakeResponse(); + private final Gson gson = new Gson(); + private final String tld = "example"; + private final LocalDate startDate = LocalDate.of(2025, 1, 1); + private final LocalDate endDate = LocalDate.of(2025, 1, 31); + + @Test + void testRun_returnsList() throws Exception { + ListMetricaReportsAction action = + new ListMetricaReportsAction( + metricaService, response, gson, tld, Optional.of(startDate), Optional.of(endDate)); + + List infoList = ImmutableList.of(); + when(metricaService.listAvailableReports(tld, Optional.of(startDate), Optional.of(endDate))) + .thenReturn(infoList); + + action.run(); + + assertThat(response.getContentType()).isEqualTo(MediaType.JSON_UTF_8); + assertThat(response.getPayload()).isEqualTo("[]"); + } + + @Test + void testRun_emptyDates_passedAsEmptyOptionals() throws Exception { + ListMetricaReportsAction action = + new ListMetricaReportsAction( + metricaService, response, gson, tld, Optional.empty(), Optional.empty()); + + when(metricaService.listAvailableReports(tld, Optional.empty(), Optional.empty())) + .thenReturn(ImmutableList.of()); + + action.run(); + + assertThat(response.getPayload()).isEqualTo("[]"); + } + + @Test + void testRun_serviceThrowsException_throwsServiceUnavailable() throws Exception { + ListMetricaReportsAction action = + new ListMetricaReportsAction( + metricaService, response, gson, tld, Optional.empty(), Optional.empty()); + + doThrow(new MosApiException("Failure")) + .when(metricaService) + .listAvailableReports(any(), any(), any()); + + ServiceUnavailableException thrown = + assertThrows(ServiceUnavailableException.class, action::run); + + assertThat(thrown).hasMessageThat().isEqualTo("Error listing METRICA reports."); + } +} diff --git a/core/src/test/java/google/registry/mosapi/client/DomainMetricaClientTest.java b/core/src/test/java/google/registry/mosapi/client/DomainMetricaClientTest.java new file mode 100644 index 00000000000..89c0638ece4 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/client/DomainMetricaClientTest.java @@ -0,0 +1,224 @@ +// Copyright 2025 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.mosapi.client; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import google.registry.mosapi.dto.domainmetrica.MetricaReport; +import google.registry.mosapi.dto.domainmetrica.MetricaReportInfo; +import google.registry.mosapi.exception.MosApiException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.http.HttpResponse; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +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; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** Unit tests for {@link DomainMetricaClient}. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class DomainMetricaClientTest { + private static final String TLD = "example"; + private static final Gson GSON = new Gson(); + private static final String BASE_URI = "https://mosapi.icann.org/v2/metrica"; + + @Mock MosApiClient mosApiClient; + @Mock HttpResponse httpResponse; + + private DomainMetricaClient client; + + @BeforeEach + void beforeEach() { + client = new DomainMetricaClient(mosApiClient, GSON); + // Common mock behavior for URI to avoid NPEs in exception messages + when(httpResponse.uri()).thenReturn(URI.create(BASE_URI)); + } + + @Test + void testGetLatestMetricaReport_success() throws Exception { + String jsonResponse = + "{" + + "\"version\": 2," + + "\"ianald\": 123," + + "\"domainListDate\": \"2025-05-20\"," + + "\"uniqueAbuseDomains\": 14," + + "\"domainListData\": []" + + "}"; + + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(httpResponse.body()).thenReturn(jsonResponse); + when(mosApiClient.sendGetRequestWithDecompression(anyString(), anyString(), anyMap(), anyMap())) + .thenReturn(httpResponse); + + MetricaReport result = client.getLatestMetricaReport(TLD); + + assertThat(result).isNotNull(); + // Assuming MetricaReport has this field (based on JSON) + // assertThat(result.domainListDate()).isEqualTo("2025-05-20"); + + verify(mosApiClient) + .sendGetRequestWithDecompression( + eq(TLD), eq("v2/metrica/domainList/latest"), eq(ImmutableMap.of()), anyMap()); + } + + @Test + void testGetLatestMetricaReport_notFound_throwsException() throws Exception { + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND); + when(mosApiClient.sendGetRequestWithDecompression(anyString(), anyString(), anyMap(), anyMap())) + .thenReturn(httpResponse); + + MosApiException thrown = + assertThrows(MosApiException.class, () -> client.getLatestMetricaReport(TLD)); + + assertThat(thrown).hasMessageThat().contains("No METRICA report found for TLD: " + TLD); + } + + @Test + void testGetMetricaReportForDate_success() throws Exception { + LocalDate date = LocalDate.of(2025, 5, 20); + String jsonResponse = "{ \"version\": 2, \"domainListDate\": \"2025-05-20\" }"; + + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(httpResponse.body()).thenReturn(jsonResponse); + when(mosApiClient.sendGetRequestWithDecompression(anyString(), anyString(), anyMap(), anyMap())) + .thenReturn(httpResponse); + + MetricaReport result = client.getMetricaReportForDate(TLD, date); + + assertThat(result).isNotNull(); + verify(mosApiClient) + .sendGetRequestWithDecompression( + eq(TLD), eq("v2/metrica/domainList/2025-05-20"), eq(ImmutableMap.of()), anyMap()); + } + + @Test + void testGetMetricaReportForDate_notFound_throwsException() throws Exception { + LocalDate date = LocalDate.of(2025, 5, 20); + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND); + when(mosApiClient.sendGetRequestWithDecompression(anyString(), anyString(), anyMap(), anyMap())) + .thenReturn(httpResponse); + + MosApiException thrown = + assertThrows(MosApiException.class, () -> client.getMetricaReportForDate(TLD, date)); + + assertThat(thrown) + .hasMessageThat() + .contains("No METRICA report found for TLD " + TLD + " on 2025-05-20"); + } + + @Test + void testListAvailableMetricaReports_withDates_success() throws Exception { + LocalDate start = LocalDate.of(2025, 1, 1); + LocalDate end = LocalDate.of(2025, 1, 31); + String jsonResponse = + "{" + + "\"version\": 2," + + "\"domainLists\": [" + + " { \"domainListDate\": \"2025-01-10\" }" + + "]" + + "}"; + + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(httpResponse.body()).thenReturn(jsonResponse); + when(mosApiClient.sendGetRequestWithDecompression(anyString(), anyString(), anyMap(), anyMap())) + .thenReturn(httpResponse); + + List result = client.listAvailableMetricaReports(TLD, start, end); + + assertThat(result).hasSize(1); + + Map expectedParams = + ImmutableMap.of( + "startDate", "2025-01-01", + "endDate", "2025-01-31"); + + verify(mosApiClient) + .sendGetRequestWithDecompression( + eq(TLD), eq("v2/metrica/domainLists"), eq(expectedParams), anyMap()); + } + + @Test + void testListAvailableMetricaReports_onlyStartDate_success() throws Exception { + LocalDate start = LocalDate.of(2025, 1, 1); + String jsonResponse = "{ \"version\": 2, \"domainLists\": [] }"; + + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(httpResponse.body()).thenReturn(jsonResponse); + when(mosApiClient.sendGetRequestWithDecompression(anyString(), anyString(), anyMap(), anyMap())) + .thenReturn(httpResponse); + + client.listAvailableMetricaReports(TLD, start, null); + + verify(mosApiClient) + .sendGetRequestWithDecompression( + eq(TLD), + eq("v2/metrica/domainLists"), + eq(ImmutableMap.of("startDate", "2025-01-01")), + anyMap()); + } + + @Test + void testListAvailableMetricaReports_badRequest_throwsException() throws Exception { + String errorJson = + "{" + + "\"resultCode\": 2012," + + "\"message\": \"The endDate is before the startDate.\"," + + "\"description\": \"Validation failed\"" + + "}"; + + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_BAD_REQUEST); + when(httpResponse.body()).thenReturn(errorJson); + when(mosApiClient.sendGetRequestWithDecompression(anyString(), anyString(), anyMap(), anyMap())) + .thenReturn(httpResponse); + + MosApiException thrown = + assertThrows( + MosApiException.class, () -> client.listAvailableMetricaReports(TLD, null, null)); + + // Verifies that the error message from the JSON response is propagated + assertThat(thrown).hasMessageThat().contains("The endDate is before the startDate."); + } + + @Test + void testListAvailableMetricaReports_serverError_throwsException() throws Exception { + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR); + when(mosApiClient.sendGetRequestWithDecompression(anyString(), anyString(), anyMap(), anyMap())) + .thenReturn(httpResponse); + + MosApiException thrown = + assertThrows( + MosApiException.class, () -> client.listAvailableMetricaReports(TLD, null, null)); + + assertThat(thrown).hasMessageThat().contains("failed with status code 500"); + } + + private static String anyString() { + return any(String.class); + } +} diff --git a/core/src/test/java/google/registry/mosapi/client/MosApiClientTest.java b/core/src/test/java/google/registry/mosapi/client/MosApiClientTest.java new file mode 100644 index 00000000000..670d873335d --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/client/MosApiClientTest.java @@ -0,0 +1,247 @@ +// Copyright 2025 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.mosapi.client; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import google.registry.mosapi.exception.MosApiException; +import google.registry.mosapi.exception.MosApiException.MosApiAuthorizationException; +import google.registry.util.HttpUtils; +import java.net.HttpURLConnection; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +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.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +/** Unit tests for {@link MosApiClient}. */ +@ExtendWith(MockitoExtension.class) +public class MosApiClientTest { + private static final String MOSAPI_URL = "https://mosapi.example.com"; + private static final String ENTITY_TYPE = "tld"; + private static final String BASE_URL = MOSAPI_URL + "/" + ENTITY_TYPE; + private static final String ENTITY_ID = "example"; + + @Mock private HttpClient httpClient; + @Mock private HttpResponse httpResponse; + + private MockedStatic httpUtilsMock; + private MosApiClient client; + + @BeforeEach + void setUp() { + httpUtilsMock = mockStatic(HttpUtils.class); + client = new MosApiClient(httpClient, MOSAPI_URL, ENTITY_TYPE); + } + + @AfterEach + void tearDown() { + httpUtilsMock.close(); + } + + @Test + void sendGetRequest_success() throws Exception { + String endpoint = "v2/check"; + Map params = ImmutableMap.of(); + Map headers = ImmutableMap.of("Authorization", "Bearer token"); + String expectedUrl = BASE_URL + "/" + ENTITY_ID + "/" + endpoint; + + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(httpResponse.body()).thenReturn("Success"); + + httpUtilsMock + .when(() -> HttpUtils.sendGetRequest(eq(httpClient), eq(expectedUrl), eq(headers))) + .thenReturn(httpResponse); + + HttpResponse result = client.sendGetRequest(ENTITY_ID, endpoint, params, headers); + + assertThat(result).isEqualTo(httpResponse); + assertThat(result.body()).isEqualTo("Success"); + } + + @Test + void sendGetRequest_withQueryParams_buildsCorrectUrl() throws Exception { + String endpoint = "v2/search"; + Map params = ImmutableMap.of("q", "foo bar", "limit", "10"); + Map headers = ImmutableMap.of(); + + String encodedQ = URLEncoder.encode("foo bar", StandardCharsets.UTF_8); + + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_OK); + httpUtilsMock + .when(() -> HttpUtils.sendGetRequest(eq(httpClient), anyString(), eq(headers))) + .thenAnswer( + invocation -> { + String url = invocation.getArgument(1); + assertThat(url).startsWith(BASE_URL + "/" + ENTITY_ID + "/" + endpoint + "?"); + assertThat(url).contains("q=" + encodedQ); + assertThat(url).contains("limit=10"); + return httpResponse; + }); + + client.sendGetRequest(ENTITY_ID, endpoint, params, headers); + } + + @Test + void sendGetRequest_unauthorized_throwsException() { + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED); + httpUtilsMock + .when(() -> HttpUtils.sendGetRequest(any(), anyString(), anyMap())) + .thenReturn(httpResponse); + + MosApiAuthorizationException thrown = + assertThrows( + MosApiAuthorizationException.class, + () -> client.sendGetRequest(ENTITY_ID, "test", ImmutableMap.of(), ImmutableMap.of())); + + assertThat(thrown).hasMessageThat().contains("Authorization failed"); + } + + @Test + void sendGetRequest_runtimeException_wrapsInMosApiException() { + RuntimeException networkError = new RuntimeException("Connection timeout"); + httpUtilsMock + .when(() -> HttpUtils.sendGetRequest(any(), anyString(), anyMap())) + .thenThrow(networkError); + + MosApiException thrown = + assertThrows( + MosApiException.class, + () -> client.sendGetRequest(ENTITY_ID, "test", ImmutableMap.of(), ImmutableMap.of())); + + assertThat(thrown).hasMessageThat().contains("Error during GET request"); + assertThat(thrown).hasCauseThat().isEqualTo(networkError); + } + + @Test + void sendGetRequestWithDecompression_success() throws Exception { + String endpoint = "v2/heavy-data"; + String expectedUrl = BASE_URL + "/" + ENTITY_ID + "/" + endpoint; + + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(httpResponse.body()).thenReturn("Decompressed Content"); + + httpUtilsMock + .when( + () -> + HttpUtils.sendGetRequestWithDecompression( + eq(httpClient), eq(expectedUrl), anyMap())) + .thenReturn(httpResponse); + + HttpResponse result = + client.sendGetRequestWithDecompression(ENTITY_ID, endpoint, null, ImmutableMap.of()); + + assertThat(result.body()).isEqualTo("Decompressed Content"); + } + + @Test + void sendGetRequestWithDecompression_unauthorized_throwsException() { + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED); + httpUtilsMock + .when(() -> HttpUtils.sendGetRequestWithDecompression(any(), anyString(), anyMap())) + .thenReturn(httpResponse); + + assertThrows( + MosApiAuthorizationException.class, + () -> client.sendGetRequestWithDecompression(ENTITY_ID, "test", null, ImmutableMap.of())); + } + + @Test + void sendGetRequestWithDecompression_runtimeException_wrapsException() { + httpUtilsMock + .when(() -> HttpUtils.sendGetRequestWithDecompression(any(), anyString(), anyMap())) + .thenThrow(new RuntimeException("Gzip error")); + + MosApiException thrown = + assertThrows( + MosApiException.class, + () -> + client.sendGetRequestWithDecompression(ENTITY_ID, "test", null, ImmutableMap.of())); + + assertThat(thrown).hasMessageThat().contains("Error during GET request"); + assertThat(thrown).hasCauseThat().hasMessageThat().isEqualTo("Gzip error"); + } + + @Test + void sendPostRequest_success() throws Exception { + String endpoint = "v2/update"; + String body = "{\"key\":\"value\"}"; + String expectedUrl = BASE_URL + "/" + ENTITY_ID + "/" + endpoint; + + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_CREATED); + + httpUtilsMock + .when(() -> HttpUtils.sendPostRequest(eq(httpClient), eq(expectedUrl), anyMap(), eq(body))) + .thenReturn(httpResponse); + + HttpResponse result = + client.sendPostRequest(ENTITY_ID, endpoint, null, ImmutableMap.of(), body); + + assertThat(result.statusCode()).isEqualTo(HttpURLConnection.HTTP_CREATED); + } + + @Test + void sendPostRequest_unauthorized_throwsException() { + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED); + httpUtilsMock + .when(() -> HttpUtils.sendPostRequest(any(), anyString(), anyMap(), anyString())) + .thenReturn(httpResponse); + + assertThrows( + MosApiAuthorizationException.class, + () -> client.sendPostRequest(ENTITY_ID, "test", null, ImmutableMap.of(), "body")); + } + + @Test + void sendPostRequest_runtimeException_wrapsException() { + httpUtilsMock + .when(() -> HttpUtils.sendPostRequest(any(), anyString(), anyMap(), anyString())) + .thenThrow(new RuntimeException("Network error")); + + MosApiException thrown = + assertThrows( + MosApiException.class, + () -> client.sendPostRequest(ENTITY_ID, "test", null, ImmutableMap.of(), "body")); + + assertThat(thrown).hasMessageThat().contains("Error during POST request"); + } + + @Test + void testBuildUrl_endpointStartingWithSlash() throws Exception { + String endpoint = "/v2/check"; + String expectedUrl = BASE_URL + "/" + ENTITY_ID + endpoint; + + when(httpResponse.statusCode()).thenReturn(200); + httpUtilsMock + .when(() -> HttpUtils.sendGetRequest(any(), eq(expectedUrl), anyMap())) + .thenReturn(httpResponse); + + client.sendGetRequest(ENTITY_ID, endpoint, null, ImmutableMap.of()); + } +} diff --git a/core/src/test/java/google/registry/mosapi/client/ServiceMonitoringClientTest.java b/core/src/test/java/google/registry/mosapi/client/ServiceMonitoringClientTest.java new file mode 100644 index 00000000000..93842261d9a --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/client/ServiceMonitoringClientTest.java @@ -0,0 +1,170 @@ +// Copyright 2025 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.mosapi.client; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import google.registry.mosapi.dto.servicemonitoring.ServiceAlarm; +import google.registry.mosapi.dto.servicemonitoring.ServiceDowntime; +import google.registry.mosapi.dto.servicemonitoring.TldServiceState; +import google.registry.mosapi.exception.MosApiException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.http.HttpResponse; +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; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** Unit tests for {@link ServiceMonitoringClient}. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class ServiceMonitoringClientTest { + private static final String TLD = "example"; + private static final String SERVICE = "dns"; + + @Mock private MosApiClient mosApiClient; + @Mock private HttpResponse httpResponse; + + private ServiceMonitoringClient client; + + @BeforeEach + void beforeEach() throws MosApiException { + client = new ServiceMonitoringClient(mosApiClient, new Gson()); + when(httpResponse.uri()).thenReturn(URI.create("https://mosapi.icann.org/v2/monitoring")); + // Default: successful mock response for any GET request, specific tests override body/status + when(mosApiClient.sendGetRequest(any(), any(), anyMap(), anyMap())).thenReturn(httpResponse); + } + + @Test + void getServiceState_success() throws MosApiException { + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(httpResponse.body()) + .thenReturn( + """ + { + "tld": "example", + "status": "Up", + "testedServices": [] + } + """); + + TldServiceState result = client.getServiceState(TLD); + + assertThat(result).isNotNull(); + assertThat(result.getTld()).isEqualTo("example"); + assertThat(result.getStatus()).isEqualTo("Up"); + verify(mosApiClient) + .sendGetRequest( + eq(TLD), eq("v2/monitoring/state"), eq(ImmutableMap.of()), eq(ImmutableMap.of())); + } + + @Test + void getDowntime_success() throws MosApiException { + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(httpResponse.body()) + .thenReturn( + """ + { + "version": 2, + "lastUpdateApiDatabase": 1000, + "downtime": 45 + } + """); + + ServiceDowntime result = client.getDowntime(TLD, SERVICE); + + assertThat(result.getDowntime()).isEqualTo(45); + verify(mosApiClient) + .sendGetRequest( + eq(TLD), + eq("v2/monitoring/dns/downtime"), + eq(ImmutableMap.of()), + eq(ImmutableMap.of())); + } + + @Test + void getDowntime_notFound_returnsDisabled() throws MosApiException { + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND); + when(httpResponse.body()).thenReturn("Not available"); + + ServiceDowntime result = client.getDowntime(TLD, SERVICE); + + assertThat(result.getDowntime()).isEqualTo(0); + assertThat(result.getDisabledMonitoring()).isTrue(); + } + + @Test + void getDowntime_serverError_throwsException() { + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR); + + MosApiException thrown = + assertThrows(MosApiException.class, () -> client.getDowntime(TLD, SERVICE)); + + assertThat(thrown).hasMessageThat().contains("failed with status code 500"); + } + + @Test + void serviceAlarmed_success() throws MosApiException { + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(httpResponse.body()) + .thenReturn( + """ + { + "version": 2, + "lastUpdateApiDatabase": 1000, + "alarmed": "Yes" + } + """); + + ServiceAlarm result = client.serviceAlarmed(TLD, SERVICE); + + assertThat(result.getAlarmed()).isEqualTo("Yes"); + verify(mosApiClient) + .sendGetRequest( + eq(TLD), eq("v2/monitoring/dns/alarmed"), eq(ImmutableMap.of()), eq(ImmutableMap.of())); + } + + @Test + void serviceAlarmed_notFound_returnsDisabled() throws MosApiException { + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND); + when(httpResponse.body()).thenReturn("Not available"); + + ServiceAlarm result = client.serviceAlarmed(TLD, SERVICE); + + assertThat(result.getAlarmed()).isEqualTo("Disabled"); + } + + @Test + void serviceAlarmed_serverError_throwsException() { + when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR); + + MosApiException thrown = + assertThrows(MosApiException.class, () -> client.serviceAlarmed(TLD, SERVICE)); + + assertThat(thrown).hasMessageThat().contains("failed with status code 500"); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/MosApiErrorResponseTest.java b/core/src/test/java/google/registry/mosapi/dto/MosApiErrorResponseTest.java new file mode 100644 index 00000000000..7dbe203ef13 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/MosApiErrorResponseTest.java @@ -0,0 +1,40 @@ +// Copyright 2025 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.mosapi.dto; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link MosApiErrorResponse}. */ +public class MosApiErrorResponseTest { + @Test + void testJsonDeserialization() { + String json = + """ + { + "resultCode": "2012", + "message": "The endDate is before the startDate.", + "description": "Validation failed" + } + """; + + MosApiErrorResponse response = new Gson().fromJson(json, MosApiErrorResponse.class); + + assertThat(response.resultCode()).isEqualTo("2012"); + assertThat(response.message()).isEqualTo("The endDate is before the startDate."); + assertThat(response.description()).isEqualTo("Validation failed"); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/domainmetrica/MetricaReportInfoTest.java b/core/src/test/java/google/registry/mosapi/dto/domainmetrica/MetricaReportInfoTest.java new file mode 100644 index 00000000000..88c6c3ef80e --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/domainmetrica/MetricaReportInfoTest.java @@ -0,0 +1,63 @@ +// Copyright 2025 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.mosapi.dto.domainmetrica; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link MetricaReportInfo}. */ +public class MetricaReportInfoTest { + /** + * Verifies that the constructor correctly sets all fields and getters return the expected values. + * + * @see ICANN MoSAPI Specification, + * Section 9.3 + */ + @Test + void testMetricaReportInfo_properties() { + String domainListDate = "2025-05-20"; + String domainListGenerationDate = "2025-05-21T10:00:00Z"; + + MetricaReportInfo info = new MetricaReportInfo(domainListDate, domainListGenerationDate); + + assertThat(info.getDomainListDate()).isEqualTo(domainListDate); + assertThat(info.getDomainListGenerationDate()).isEqualTo(domainListGenerationDate); + } + + /** + * Verifies that the object can be correctly deserialized from JSON, respecting the {@code + * SerializedName} annotations. + * + * @see ICANN MoSAPI Specification, + * Section 9.3 + */ + @Test + void testJsonDeserialization() { + // JSON structure based on the array elements defined in Section 9.3 + String json = + "{" + + "\"domainListDate\": \"2025-05-20\"," + + "\"domainListGenerationDate\": \"2025-05-21T10:00:00Z\"" + + "}"; + + Gson gson = new Gson(); + MetricaReportInfo info = gson.fromJson(json, MetricaReportInfo.class); + + assertThat(info.getDomainListDate()).isEqualTo("2025-05-20"); + assertThat(info.getDomainListGenerationDate()).isEqualTo("2025-05-21T10:00:00Z"); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/domainmetrica/MetricaReportTest.java b/core/src/test/java/google/registry/mosapi/dto/domainmetrica/MetricaReportTest.java new file mode 100644 index 00000000000..9fa019d192b --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/domainmetrica/MetricaReportTest.java @@ -0,0 +1,70 @@ +// Copyright 2025 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.mosapi.dto.domainmetrica; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** Unit tests for {@link MetricaReport}. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class MetricaReportTest { + + @Test + void testMetricaReport_properties() { + int version = 2; + String tld = "example"; + String date = "2025-05-20"; + int abuseCount = 42; + List threats = Collections.emptyList(); + + MetricaReport report = new MetricaReport(version, tld, date, abuseCount, threats); + + assertThat(report.getVersion()).isEqualTo(version); + assertThat(report.getTld()).isEqualTo(tld); + assertThat(report.getDomainListDate()).isEqualTo(date); + assertThat(report.getUniqueAbuseDomains()).isEqualTo(abuseCount); + assertThat(report.getThreats()).isEqualTo(threats); + } + + @Test + void testJsonDeserialization() { + String json = + "{" + + "\"version\": 2," + + "\"tld\": \"test_tld\"," + + "\"domainListDate\": \"2025-01-01\"," + + "\"uniqueAbuseDomains\": 10," + + "\"domainListData\": []" + + "}"; + + Gson gson = new Gson(); + MetricaReport report = gson.fromJson(json, MetricaReport.class); + + assertThat(report.getVersion()).isEqualTo(2); + assertThat(report.getTld()).isEqualTo("test_tld"); + assertThat(report.getDomainListDate()).isEqualTo("2025-01-01"); + assertThat(report.getUniqueAbuseDomains()).isEqualTo(10); + assertThat(report.getThreats()).isEmpty(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/domainmetrica/MetricaThreatDataTest.java b/core/src/test/java/google/registry/mosapi/dto/domainmetrica/MetricaThreatDataTest.java new file mode 100644 index 00000000000..b24d0f5cab8 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/domainmetrica/MetricaThreatDataTest.java @@ -0,0 +1,68 @@ +// Copyright 2025 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.mosapi.dto.domainmetrica; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link MetricaThreatData}. */ +public class MetricaThreatDataTest { + @Test + void testMetricaThreatData_properties() { + String threatType = "phishing"; + int count = 5; + List domains = Arrays.asList("example.com", "test.net"); + + MetricaThreatData data = new MetricaThreatData(threatType, count, domains); + + assertThat(data.getThreatType()).isEqualTo(threatType); + assertThat(data.getCount()).isEqualTo(count); + assertThat(data.getDomains()).containsExactlyElementsIn(domains).inOrder(); + } + + @Test + void testJsonDeserialization() { + // JSON structure based on the example in Specification Section 9.1 + String json = + "{" + + "\"threatType\": \"spam\"," + + "\"count\": 123," + + "\"domains\": [\"test1.example\", \"test2.example\"]" + + "}"; + + Gson gson = new Gson(); + MetricaThreatData data = gson.fromJson(json, MetricaThreatData.class); + + assertThat(data.getThreatType()).isEqualTo("spam"); + assertThat(data.getCount()).isEqualTo(123); + assertThat(data.getDomains()).containsExactly("test1.example", "test2.example").inOrder(); + } + + @Test + void testJsonDeserialization_disabledThreat() { + String json = "{" + "\"threatType\": \"malware\"," + "\"count\": -1," + "\"domains\": []" + "}"; + + Gson gson = new Gson(); + MetricaThreatData data = gson.fromJson(json, MetricaThreatData.class); + + assertThat(data.getThreatType()).isEqualTo("malware"); + assertThat(data.getCount()).isEqualTo(-1); + assertThat(data.getDomains()).isEmpty(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ActiveIncidentsSummaryTest.java b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ActiveIncidentsSummaryTest.java new file mode 100644 index 00000000000..ea67fe1ab78 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ActiveIncidentsSummaryTest.java @@ -0,0 +1,54 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ActiveIncidentsSummary}. */ +public class ActiveIncidentsSummaryTest { + @Test + void testActiveIncidentsSummary_properties() { + String service = "DNS"; + double threshold = 10.5; + List incidents = Collections.emptyList(); + + ActiveIncidentsSummary summary = new ActiveIncidentsSummary(service, threshold, incidents); + + assertThat(summary.getService()).isEqualTo(service); + assertThat(summary.getEmergencyThreshold()).isEqualTo(threshold); + assertThat(summary.getIncidents()).isEqualTo(incidents); + } + + @Test + void testJsonDeserialization() { + String json = + "{" + + "\"service\": \"RDDS\"," + + "\"emergencyThreshold\": 50.0," + + "\"incidents\": []" + + "}"; + + Gson gson = new Gson(); + ActiveIncidentsSummary summary = gson.fromJson(json, ActiveIncidentsSummary.class); + + assertThat(summary.getService()).isEqualTo("RDDS"); + assertThat(summary.getEmergencyThreshold()).isEqualTo(50.0); + assertThat(summary.getIncidents()).isEmpty(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/AlarmResponseTest.java b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/AlarmResponseTest.java new file mode 100644 index 00000000000..8eeeb47993b --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/AlarmResponseTest.java @@ -0,0 +1,45 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link AlarmResponse}. */ +public class AlarmResponseTest { + + @Test + void testAlarmResponse_properties() { + List statuses = Collections.emptyList(); + AlarmResponse response = new AlarmResponse(statuses); + + assertThat(response.getAlarmStatuses()).isEqualTo(statuses); + } + + // Verifies that the object can be correctly deserialized from JSON + @Test + void testJsonDeserialization() { + String json = "{ \"alarmStatuses\": [] }"; + + Gson gson = new Gson(); + AlarmResponse response = gson.fromJson(json, AlarmResponse.class); + + assertThat(response).isNotNull(); + assertThat(response.getAlarmStatuses()).isEmpty(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/AlarmStatusTest.java b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/AlarmStatusTest.java new file mode 100644 index 00000000000..e0b2e0b475a --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/AlarmStatusTest.java @@ -0,0 +1,66 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link AlarmStatus}. */ +public class AlarmStatusTest { + + @Test + void testAlarmStatus_properties() { + String tld = "example"; + String service = "dns"; + String status = "Yes"; + String error = "Connection timeout"; + + AlarmStatus alarmStatus = new AlarmStatus(tld, service, status, error); + + assertThat(alarmStatus.getTld()).isEqualTo(tld); + assertThat(alarmStatus.getService()).isEqualTo(service); + assertThat(alarmStatus.getStatus()).isEqualTo(status); + assertThat(alarmStatus.getErrorMessage()).isEqualTo(error); + } + + @Test + void testAlarmStatus_nullableFields() { + AlarmStatus alarmStatus = new AlarmStatus("example", "rdds", "No", null); + + assertThat(alarmStatus.getStatus()).isEqualTo("No"); + assertThat(alarmStatus.getErrorMessage()).isNull(); + } + + @Test + void testJsonDeserialization() { + String json = + "{" + + "\"tld\": \"example\"," + + "\"service\": \"dns\"," + + "\"status\": \"Yes\"," + + "\"errorMessage\": \"Error details\"" + + "}"; + + Gson gson = new Gson(); + AlarmStatus alarmStatus = gson.fromJson(json, AlarmStatus.class); + + assertThat(alarmStatus.getTld()).isEqualTo("example"); + assertThat(alarmStatus.getService()).isEqualTo("dns"); + assertThat(alarmStatus.getStatus()).isEqualTo("Yes"); + assertThat(alarmStatus.getErrorMessage()).isEqualTo("Error details"); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/AllServicesStateResponseTest.java b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/AllServicesStateResponseTest.java new file mode 100644 index 00000000000..e60254db85a --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/AllServicesStateResponseTest.java @@ -0,0 +1,45 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link AllServicesStateResponse}. */ +public class AllServicesStateResponseTest { + + @Test + void testAllServicesStateResponse_properties() { + List states = Collections.emptyList(); + AllServicesStateResponse response = new AllServicesStateResponse(states); + + assertThat(response.getServiceStates()).isEqualTo(states); + } + + @Test + void testJsonDeserialization() { + // Simulating a JSON object wrapping the list of service states. + String json = "{ \"serviceStates\": [] }"; + + Gson gson = new Gson(); + AllServicesStateResponse response = gson.fromJson(json, AllServicesStateResponse.class); + + assertThat(response).isNotNull(); + assertThat(response.getServiceStates()).isEmpty(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/AllTldsDowntimeTest.java b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/AllTldsDowntimeTest.java new file mode 100644 index 00000000000..eb09f8990c4 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/AllTldsDowntimeTest.java @@ -0,0 +1,45 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link AllTldsDowntime}. */ +public class AllTldsDowntimeTest { + + @Test + void testAllTldsDowntime_properties() { + List downtimes = Collections.emptyList(); + AllTldsDowntime wrapper = new AllTldsDowntime(downtimes); + + assertThat(wrapper.getAllDowntimes()).isEqualTo(downtimes); + } + + @Test + void testJsonDeserialization() { + String json = "{ \"allDowntimes\": [] }"; + + Gson gson = new Gson(); + AllTldsDowntime wrapper = gson.fromJson(json, AllTldsDowntime.class); + + assertThat(wrapper).isNotNull(); + assertThat(wrapper.getAllDowntimes()).isEmpty(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/IncidentSummaryTest.java b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/IncidentSummaryTest.java new file mode 100644 index 00000000000..c82e5777584 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/IncidentSummaryTest.java @@ -0,0 +1,80 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link IncidentSummary}. */ +public class IncidentSummaryTest { + @Test + void testIncidentSummary_properties() { + String incidentId = "1495811850.1700"; + long startTime = 1495811850L; + boolean falsePositive = false; + String state = "Active"; + Long endTime = null; + + IncidentSummary summary = + new IncidentSummary(incidentId, startTime, falsePositive, state, endTime); + + assertThat(summary.getIncidentID()).isEqualTo(incidentId); + assertThat(summary.getStartTime()).isEqualTo(startTime); + assertThat(summary.isFalsePositive()).isFalse(); + assertThat(summary.getState()).isEqualTo(state); + assertThat(summary.getEndTime()).isNull(); + } + + @Test + void testJsonDeserialization_resolvedIncident() { + String json = + "{" + + "\"incidentID\": \"1495811850.1700\"," + + "\"startTime\": 1495811850," + + "\"falsePositive\": true," + + "\"state\": \"Resolved\"," + + "\"endTime\": 1495812000" + + "}"; + + Gson gson = new Gson(); + IncidentSummary summary = gson.fromJson(json, IncidentSummary.class); + + assertThat(summary.getIncidentID()).isEqualTo("1495811850.1700"); + assertThat(summary.getStartTime()).isEqualTo(1495811850L); + assertThat(summary.isFalsePositive()).isTrue(); + assertThat(summary.getState()).isEqualTo("Resolved"); + assertThat(summary.getEndTime()).isEqualTo(1495812000L); + } + + @Test + void testJsonDeserialization_activeIncident() { + String json = + "{" + + "\"incidentID\": \"1495811850.1700\"," + + "\"startTime\": 1495811850," + + "\"falsePositive\": false," + + "\"state\": \"Active\"," + + "\"endTime\": null" + + "}"; + + Gson gson = new Gson(); + IncidentSummary summary = gson.fromJson(json, IncidentSummary.class); + + assertThat(summary.getState()).isEqualTo("Active"); + assertThat(summary.getEndTime()).isNull(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ServiceAlarmTest.java b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ServiceAlarmTest.java new file mode 100644 index 00000000000..3ec6ad4327c --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ServiceAlarmTest.java @@ -0,0 +1,53 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ServiceAlarm}. */ +public class ServiceAlarmTest { + @Test + void testServiceAlarm_properties() { + int version = 2; + long lastUpdate = 1422492450L; + String alarmed = "Yes"; + + ServiceAlarm serviceAlarm = new ServiceAlarm(version, lastUpdate, alarmed); + + assertThat(serviceAlarm.getVersion()).isEqualTo(version); + assertThat(serviceAlarm.getLastUpdateApiDatabase()).isEqualTo(lastUpdate); + assertThat(serviceAlarm.getAlarmed()).isEqualTo(alarmed); + } + + @Test + void testJsonDeserialization() { + String json = + "{" + + "\"version\": 2," + + "\"lastUpdateApiDatabase\": 1422492450," + + "\"alarmed\": \"No\"" + + "}"; + + Gson gson = new Gson(); + ServiceAlarm serviceAlarm = gson.fromJson(json, ServiceAlarm.class); + + assertThat(serviceAlarm.getVersion()).isEqualTo(2); + assertThat(serviceAlarm.getLastUpdateApiDatabase()).isEqualTo(1422492450L); + assertThat(serviceAlarm.getAlarmed()).isEqualTo("No"); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ServiceDowntimeTest.java b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ServiceDowntimeTest.java new file mode 100644 index 00000000000..dde2900d29b --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ServiceDowntimeTest.java @@ -0,0 +1,75 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ServiceDowntime}. */ +public class ServiceDowntimeTest { + + @Test + void testServiceDowntime_properties() { + int version = 2; + long lastUpdate = 1422492450L; + int downtime = 935; + boolean disabled = false; + + ServiceDowntime serviceDowntime = new ServiceDowntime(version, lastUpdate, downtime, disabled); + + assertThat(serviceDowntime.getVersion()).isEqualTo(version); + assertThat(serviceDowntime.getLastUpdateApiDatabase()).isEqualTo(lastUpdate); + assertThat(serviceDowntime.getDowntime()).isEqualTo(downtime); + assertThat(serviceDowntime.getDisabledMonitoring()).isFalse(); + } + + @Test + void testJsonDeserialization() { + // JSON structure based on the example in Specification Section 5.3 + String json = + "{" + + "\"version\": 2," + + "\"lastUpdateApiDatabase\": 1422492450," + + "\"downtime\": 935" + + "}"; + + Gson gson = new Gson(); + ServiceDowntime serviceDowntime = gson.fromJson(json, ServiceDowntime.class); + + assertThat(serviceDowntime.getVersion()).isEqualTo(2); + assertThat(serviceDowntime.getLastUpdateApiDatabase()).isEqualTo(1422492450L); + assertThat(serviceDowntime.getDowntime()).isEqualTo(935); + // disabledMonitoring is not in the standard spec response, so it should be null/default + assertThat(serviceDowntime.getDisabledMonitoring()).isNull(); + } + + @Test + void testJsonDeserialization_withDisabledMonitoring() { + String json = + "{" + + "\"version\": 2," + + "\"lastUpdateApiDatabase\": 1422492450," + + "\"downtime\": 0," + + "\"disabledMonitoring\": true" + + "}"; + + Gson gson = new Gson(); + ServiceDowntime serviceDowntime = gson.fromJson(json, ServiceDowntime.class); + + assertThat(serviceDowntime.getDisabledMonitoring()).isTrue(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ServiceStateSummaryTest.java b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ServiceStateSummaryTest.java new file mode 100644 index 00000000000..872bb9250e5 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ServiceStateSummaryTest.java @@ -0,0 +1,74 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ServiceStateSummary}. */ +public class ServiceStateSummaryTest { + + @Test + void testServiceStateSummary_properties() { + String tld = "example"; + String status = "Down"; + List incidents = Collections.emptyList(); + + ServiceStateSummary summary = new ServiceStateSummary(tld, status, incidents); + + assertThat(summary.getTld()).isEqualTo(tld); + assertThat(summary.getOverallStatus()).isEqualTo(status); + assertThat(summary.getActiveIncidents()).isEqualTo(incidents); + } + + @Test + void testJsonDeserialization() { + String json = + "{" + + "\"tld\": \"example\"," + + "\"overallStatus\": \"Up\"," + + "\"activeIncidents\": []" + + "}"; + + Gson gson = new Gson(); + ServiceStateSummary summary = gson.fromJson(json, ServiceStateSummary.class); + + assertThat(summary.getTld()).isEqualTo("example"); + assertThat(summary.getOverallStatus()).isEqualTo("Up"); + assertThat(summary.getActiveIncidents()).isEmpty(); + } + + /** + * Verifies deserialization when the optional {@code activeIncidents} field is missing (null). + * + * @see ICANN MoSAPI Specification, + * Section 5.1 + */ + @Test + void testJsonDeserialization_nullIncidents() { + String json = "{" + "\"tld\": \"example\"," + "\"overallStatus\": \"Up\"" + "}"; + + Gson gson = new Gson(); + ServiceStateSummary summary = gson.fromJson(json, ServiceStateSummary.class); + + assertThat(summary.getTld()).isEqualTo("example"); + assertThat(summary.getOverallStatus()).isEqualTo("Up"); + assertThat(summary.getActiveIncidents()).isNull(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ServiceStatusTest.java b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ServiceStatusTest.java new file mode 100644 index 00000000000..2b32bc629a9 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/ServiceStatusTest.java @@ -0,0 +1,52 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ServiceStatus}. */ +public class ServiceStatusTest { + + @Test + void testServiceStatus_properties() { + String status = "Down"; + double threshold = 10.0; + List incidents = Collections.emptyList(); + + ServiceStatus serviceStatus = new ServiceStatus(status, threshold, incidents); + + assertThat(serviceStatus.getStatus()).isEqualTo(status); + assertThat(serviceStatus.getEmergencyThreshold()).isEqualTo(threshold); + assertThat(serviceStatus.getIncidents()).isEqualTo(incidents); + } + + @Test + void testJsonDeserialization() { + // JSON structure based on the example in Specification Section 5.1 + String json = + "{" + "\"status\": \"Down\"," + "\"emergencyThreshold\": 10.0," + "\"incidents\": []" + "}"; + + Gson gson = new Gson(); + ServiceStatus serviceStatus = gson.fromJson(json, ServiceStatus.class); + + assertThat(serviceStatus.getStatus()).isEqualTo("Down"); + assertThat(serviceStatus.getEmergencyThreshold()).isEqualTo(10.0); + assertThat(serviceStatus.getIncidents()).isEmpty(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/TldServiceStateTest.java b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/TldServiceStateTest.java new file mode 100644 index 00000000000..55582d6292b --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/TldServiceStateTest.java @@ -0,0 +1,69 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link TldServiceState}. */ +public class TldServiceStateTest { + + @Test + void testServiceState_properties() { + String tld = "example"; + long lastUpdate = 1496923082L; + String status = "Down"; + Map services = ImmutableMap.of(); + + TldServiceState state = new TldServiceState(tld, lastUpdate, status, services); + + assertThat(state.getTld()).isEqualTo(tld); + assertThat(state.getLastUpdateApiDatabase()).isEqualTo(lastUpdate); + assertThat(state.getStatus()).isEqualTo(status); + assertThat(state.getServiceStatuses()).isEqualTo(services); + } + + /** + * Verifies that the object can be correctly deserialized from JSON. + * + * @see ICANN MoSAPI Specification, + * Section 5.1 + */ + @Test + void testJsonDeserialization() { + String json = + "{" + + "\"tld\": \"example\"," + + "\"lastUpdateApiDatabase\": 1496923082," + + "\"status\": \"Down\"," + + "\"testedServices\": {" + + " \"DNS\": {}," + + " \"RDDS\": {}" + + "}" + + "}"; + + Gson gson = new Gson(); + TldServiceState state = gson.fromJson(json, TldServiceState.class); + + assertThat(state.getTld()).isEqualTo("example"); + assertThat(state.getLastUpdateApiDatabase()).isEqualTo(1496923082L); + assertThat(state.getStatus()).isEqualTo("Down"); + assertThat(state.getServiceStatuses()).containsKey("DNS"); + assertThat(state.getServiceStatuses()).containsKey("RDDS"); + } +} diff --git a/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/TldServicesDowntimeTest.java b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/TldServicesDowntimeTest.java new file mode 100644 index 00000000000..2ab37d079a9 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/dto/servicemonitoring/TldServicesDowntimeTest.java @@ -0,0 +1,56 @@ +// Copyright 2025 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.mosapi.dto.servicemonitoring; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link TldServicesDowntime}. */ +public class TldServicesDowntimeTest { + + @Test + void testTldServicesDowntime_properties() { + String tld = "example"; + Map downtimeMap = ImmutableMap.of(); + TldServicesDowntime tldDowntime = new TldServicesDowntime(tld, downtimeMap); + assertThat(tldDowntime.getTld()).isEqualTo(tld); + assertThat(tldDowntime.getServiceDowntime()).isEqualTo(downtimeMap); + } + + @Test + void testJsonDeserialization() { + // JSON structure simulating the aggregation of service downtimes for a TLD + String json = + "{" + + "\"tld\": \"example\"," + + "\"serviceDowntime\": {" + + " \"DNS\": {" + + " \"version\": 2," + + " \"downtime\": 10" + + " }" + + "}" + + "}"; + + Gson gson = new Gson(); + TldServicesDowntime tldDowntime = gson.fromJson(json, TldServicesDowntime.class); + + assertThat(tldDowntime.getTld()).isEqualTo("example"); + assertThat(tldDowntime.getServiceDowntime()).containsKey("DNS"); + assertThat(tldDowntime.getServiceDowntime().get("DNS").getDowntime()).isEqualTo(10); + } +} diff --git a/core/src/test/java/google/registry/mosapi/module/MosApiModuleTest.java b/core/src/test/java/google/registry/mosapi/module/MosApiModuleTest.java new file mode 100644 index 00000000000..2908b308b86 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/module/MosApiModuleTest.java @@ -0,0 +1,59 @@ +// Copyright 2025 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.mosapi.module; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import google.registry.privileges.secretmanager.SecretManagerClient; +import java.util.Optional; +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 MosApiModule}. */ +@ExtendWith(MockitoExtension.class) +public class MosApiModuleTest { + @Mock private SecretManagerClient secretManagerClient; + + @Test + void provideMosapiTlsCert_returnsSecretValue() { + String secretName = "nomulus-dot-foo_tls-client-dot-crt-dot-pem"; + String secretValue = "-----BEGIN CERTIFICATE-----..."; + + when(secretManagerClient.getSecretData(secretName, Optional.of("latest"))) + .thenReturn(secretValue); + + String result = MosApiModule.provideMosapiTlsCert(secretManagerClient); + + assertThat(result).isEqualTo(secretValue); + verify(secretManagerClient).getSecretData(secretName, Optional.of("latest")); + } + + @Test + void provideMosapiTlsKey_returnsSecretValue() { + String secretName = "nomulus-dot-foo_tls-client-dot-key"; + String secretValue = "-----BEGIN PRIVATE KEY-----..."; + + when(secretManagerClient.getSecretData(secretName, Optional.of("latest"))) + .thenReturn(secretValue); + + String result = MosApiModule.provideMosapiTlsKey(secretManagerClient); + + assertThat(result).isEqualTo(secretValue); + verify(secretManagerClient).getSecretData(secretName, Optional.of("latest")); + } +} diff --git a/core/src/test/java/google/registry/mosapi/module/MosApiRequestModuleTest.java b/core/src/test/java/google/registry/mosapi/module/MosApiRequestModuleTest.java new file mode 100644 index 00000000000..36b85a9fe48 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/module/MosApiRequestModuleTest.java @@ -0,0 +1,93 @@ +// Copyright 2025 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.mosapi.module; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import jakarta.servlet.http.HttpServletRequest; +import java.time.LocalDate; +import java.util.Optional; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link MosApiRequestModule}. */ +public class MosApiRequestModuleTest { + // Explicitly create the mock to ensure it is not null + private final HttpServletRequest req = mock(HttpServletRequest.class); + + @Test + void provideDate_validDate_returnsOptionalDate() { + when(req.getParameter("date")).thenReturn("2025-01-01"); + Optional result = MosApiRequestModule.provideDate(req); + assertThat(result).hasValue(LocalDate.of(2025, 1, 1)); + } + + @Test + void provideDate_invalidFormat_returnsEmpty() { + when(req.getParameter("date")).thenReturn("not-a-date"); + Optional result = MosApiRequestModule.provideDate(req); + assertThat(result).isEmpty(); + } + + @Test + void provideDate_missingParameter_returnsEmpty() { + when(req.getParameter("date")).thenReturn(null); + Optional result = MosApiRequestModule.provideDate(req); + assertThat(result).isEmpty(); + } + + @Test + void provideTld_present_returnsOptionalString() { + when(req.getParameter("tld")).thenReturn("example"); + Optional result = MosApiRequestModule.provideTld(req); + assertThat(result).hasValue("example"); + } + + @Test + void provideTld_missing_returnsEmpty() { + when(req.getParameter("tld")).thenReturn(""); + Optional result = MosApiRequestModule.provideTld(req); + assertThat(result).isEmpty(); + } + + @Test + void provideStartDate_validDate_returnsOptionalDate() { + when(req.getParameter("startDate")).thenReturn("2025-05-01"); + Optional result = MosApiRequestModule.provideStartDate(req); + assertThat(result).hasValue(LocalDate.of(2025, 5, 1)); + } + + @Test + void provideStartDate_invalidFormat_returnsEmpty() { + when(req.getParameter("startDate")).thenReturn("2025/05/01"); // Wrong format + Optional result = MosApiRequestModule.provideStartDate(req); + assertThat(result).isEmpty(); + } + + @Test + void provideEndDate_validDate_returnsOptionalDate() { + when(req.getParameter("endDate")).thenReturn("2025-12-31"); + Optional result = MosApiRequestModule.provideEndDate(req); + assertThat(result).hasValue(LocalDate.of(2025, 12, 31)); + } + + @Test + void provideEndDate_missingParameter_returnsEmpty() { + when(req.getParameter("endDate")).thenReturn(null); + Optional result = MosApiRequestModule.provideEndDate(req); + assertThat(result).isEmpty(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/service/MosApiAlarmServiceTest.java b/core/src/test/java/google/registry/mosapi/service/MosApiAlarmServiceTest.java new file mode 100644 index 00000000000..26b4e6605d5 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/service/MosApiAlarmServiceTest.java @@ -0,0 +1,95 @@ +// Copyright 2025 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.mosapi.service; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import google.registry.mosapi.client.ServiceMonitoringClient; +import google.registry.mosapi.dto.servicemonitoring.AlarmResponse; +import google.registry.mosapi.dto.servicemonitoring.AlarmStatus; +import google.registry.mosapi.dto.servicemonitoring.ServiceAlarm; +import google.registry.mosapi.exception.MosApiException; +import google.registry.mosapi.services.MosApiAlarmService; +import java.util.List; +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 MosApiAlarmService}. */ +@ExtendWith(MockitoExtension.class) +public class MosApiAlarmServiceTest { + @Mock private ServiceMonitoringClient serviceMonitoringClient; + + private MosApiAlarmService alarmService; + private final List tlds = ImmutableList.of("example", "test"); + private final List services = ImmutableList.of("dns", "rdds"); + + @BeforeEach + void setUp() { + alarmService = new MosApiAlarmService(serviceMonitoringClient, tlds, services); + } + + @Test + void checkAllAlarms_success() throws Exception { + // Mock responses: 'example' has alarms on both, 'test' has no alarms + ServiceAlarm alarmed = new ServiceAlarm(2, 100L, "Yes"); + ServiceAlarm notAlarmed = new ServiceAlarm(2, 100L, "No"); + + when(serviceMonitoringClient.serviceAlarmed(eq("example"), anyString())).thenReturn(alarmed); + when(serviceMonitoringClient.serviceAlarmed(eq("test"), anyString())).thenReturn(notAlarmed); + + AlarmResponse response = alarmService.checkAllAlarms(); + + assertThat(response.getAlarmStatuses()).hasSize(4); // 2 TLDs * 2 Services + + AlarmStatus exampleDns = response.getAlarmStatuses().get(0); + assertThat(exampleDns.getTld()).isEqualTo("example"); + assertThat(exampleDns.getService()).isEqualTo("dns"); + assertThat(exampleDns.getStatus()).isEqualTo("Yes"); + assertThat(exampleDns.getErrorMessage()).isNull(); + + // Verify 'test' statuses + AlarmStatus testRdds = response.getAlarmStatuses().get(3); + assertThat(testRdds.getTld()).isEqualTo("test"); + assertThat(testRdds.getStatus()).isEqualTo("No"); + } + + @Test + void checkAllAlarms_handlesException() throws Exception { + when(serviceMonitoringClient.serviceAlarmed(anyString(), anyString())) + .thenReturn(new ServiceAlarm(2, 100L, "No")); + when(serviceMonitoringClient.serviceAlarmed("example", "dns")) + .thenThrow(new MosApiException("Connection refused")); + + AlarmResponse response = alarmService.checkAllAlarms(); + + assertThat(response.getAlarmStatuses()).hasSize(4); + + // Find the failed status + AlarmStatus failedStatus = + response.getAlarmStatuses().stream() + .filter(s -> s.getTld().equals("example") && s.getService().equals("dns")) + .findFirst() + .orElseThrow(); + + assertThat(failedStatus.getStatus()).isEqualTo("ERROR"); + assertThat(failedStatus.getErrorMessage()).isEqualTo("Connection refused"); + } +} diff --git a/core/src/test/java/google/registry/mosapi/service/MosApiDowntimeServiceTest.java b/core/src/test/java/google/registry/mosapi/service/MosApiDowntimeServiceTest.java new file mode 100644 index 00000000000..78b5673b997 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/service/MosApiDowntimeServiceTest.java @@ -0,0 +1,88 @@ +// Copyright 2025 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.mosapi.service; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import google.registry.mosapi.client.ServiceMonitoringClient; +import google.registry.mosapi.dto.servicemonitoring.AllTldsDowntime; +import google.registry.mosapi.dto.servicemonitoring.ServiceDowntime; +import google.registry.mosapi.dto.servicemonitoring.TldServicesDowntime; +import google.registry.mosapi.exception.MosApiException; +import google.registry.mosapi.services.MosApiDowntimeService; +import java.util.List; +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 MosApiDowntimeService}. */ +@ExtendWith(MockitoExtension.class) +public class MosApiDowntimeServiceTest { + @Mock private ServiceMonitoringClient serviceMonitoringClient; + + private MosApiDowntimeService downtimeService; + private final List tlds = ImmutableList.of("example", "test"); + private final List services = ImmutableList.of("dns", "rdds"); + + @BeforeEach + void setUp() { + downtimeService = new MosApiDowntimeService(serviceMonitoringClient, tlds, services); + } + + @Test + void getDowntimeForTld_success() throws Exception { + ServiceDowntime dnsDowntime = new ServiceDowntime(2, 100L, 50, false); + ServiceDowntime rddsDowntime = new ServiceDowntime(2, 100L, 0, false); + + when(serviceMonitoringClient.getDowntime("example", "dns")).thenReturn(dnsDowntime); + when(serviceMonitoringClient.getDowntime("example", "rdds")).thenReturn(rddsDowntime); + + TldServicesDowntime result = downtimeService.getDowntimeForTld("example"); + + assertThat(result.getTld()).isEqualTo("example"); + assertThat(result.getServiceDowntime()).containsEntry("dns", dnsDowntime); + assertThat(result.getServiceDowntime()).containsEntry("rdds", rddsDowntime); + } + + @Test + void getDowntimeForTld_handlesException() throws Exception { + ServiceDowntime dnsDowntime = new ServiceDowntime(2, 100L, 50, false); + + when(serviceMonitoringClient.getDowntime("example", "dns")).thenReturn(dnsDowntime); + when(serviceMonitoringClient.getDowntime("example", "rdds")) + .thenThrow(new MosApiException("Fetch failed")); + + TldServicesDowntime result = downtimeService.getDowntimeForTld("example"); + + assertThat(result.getServiceDowntime()).containsEntry("dns", dnsDowntime); + assertThat(result.getServiceDowntime()).doesNotContainKey("rdds"); + } + + @Test + void getDowntimeForAllTlds_success() throws Exception { + ServiceDowntime dummy = new ServiceDowntime(2, 100L, 10, false); + when(serviceMonitoringClient.getDowntime(anyString(), anyString())).thenReturn(dummy); + + AllTldsDowntime result = downtimeService.getDowntimeForAllTlds(); + + assertThat(result.getAllDowntimes()).hasSize(2); // 2 TLDs + assertThat(result.getAllDowntimes().get(0).getTld()).isEqualTo("example"); + assertThat(result.getAllDowntimes().get(1).getTld()).isEqualTo("test"); + } +} diff --git a/core/src/test/java/google/registry/mosapi/service/MosApiExceptionTest.java b/core/src/test/java/google/registry/mosapi/service/MosApiExceptionTest.java new file mode 100644 index 00000000000..ed1f037d49a --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/service/MosApiExceptionTest.java @@ -0,0 +1,97 @@ +// Copyright 2025 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.mosapi.service; + +import static com.google.common.truth.Truth.assertThat; + +import google.registry.mosapi.dto.MosApiErrorResponse; +import google.registry.mosapi.exception.MosApiException; +import google.registry.mosapi.exception.MosApiException.MosApiAuthorizationException; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link MosApiException}. */ +public class MosApiExceptionTest { + @Test + void testConstructor_messageOnly() { + MosApiException exception = new MosApiException("Something went wrong"); + assertThat(exception).hasMessageThat().isEqualTo("Something went wrong"); + } + + @Test + void testConstructor_messageAndCause() { + Throwable cause = new RuntimeException("Root cause"); + MosApiException exception = new MosApiException("Wrapper message", cause); + + assertThat(exception).hasMessageThat().isEqualTo("Wrapper message"); + assertThat(exception).hasCauseThat().isEqualTo(cause); + } + + @Test + void testAuthorizationException() { + MosApiAuthorizationException exception = + new MosApiAuthorizationException("Unauthorized access"); + + assertThat(exception).isInstanceOf(MosApiException.class); + assertThat(exception).hasMessageThat().isEqualTo("Unauthorized access"); + } + + @Test + void testCreate_code2012_dateOrderInvalid() { + // Code 2012: Date order is invalid + MosApiErrorResponse error = + new MosApiErrorResponse("2012", "The endDate is before startDate", "Description"); + + MosApiException exception = MosApiException.create(error); + + assertThat(exception).hasMessageThat().startsWith("Date order is invalid:"); + assertThat(exception).hasMessageThat().contains("The endDate is before startDate"); + } + + @Test + void testCreate_code2013_dateSyntaxInvalid() { + // Code 2013: Date syntax is invalid + MosApiErrorResponse error = + new MosApiErrorResponse("2013", "Invalid format YYYY", "Description"); + + MosApiException exception = MosApiException.create(error); + + assertThat(exception).hasMessageThat().startsWith("Date syntax is invalid:"); + assertThat(exception).hasMessageThat().contains("Invalid format YYYY"); + } + + @Test + void testCreate_code2014_dateSyntaxInvalid() { + // Code 2014: Also Date syntax invalid + MosApiErrorResponse error = + new MosApiErrorResponse("2014", "Invalid characters", "Description"); + + MosApiException exception = MosApiException.create(error); + + assertThat(exception).hasMessageThat().startsWith("Date syntax is invalid:"); + assertThat(exception).hasMessageThat().contains("Invalid characters"); + } + + @Test + void testCreate_defaultCode_genericMessage() { + // Default case: "Bad Request (code: ...): ..." + MosApiErrorResponse error = + new MosApiErrorResponse("400", "Generic bad request", "Description"); + + MosApiException exception = MosApiException.create(error); + + assertThat(exception) + .hasMessageThat() + .isEqualTo("Bad Request (code: 400): Generic bad request"); + } +} diff --git a/core/src/test/java/google/registry/mosapi/service/MosApiMetricaServiceTest.java b/core/src/test/java/google/registry/mosapi/service/MosApiMetricaServiceTest.java new file mode 100644 index 00000000000..b33d29e563d --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/service/MosApiMetricaServiceTest.java @@ -0,0 +1,85 @@ +// Copyright 2025 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.mosapi.service; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import google.registry.mosapi.client.DomainMetricaClient; +import google.registry.mosapi.dto.domainmetrica.MetricaReport; +import google.registry.mosapi.dto.domainmetrica.MetricaReportInfo; +import google.registry.mosapi.services.MosApiMetricaService; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +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 MosApiMetricaService}. */ +@ExtendWith(MockitoExtension.class) +public class MosApiMetricaServiceTest { + @Mock private DomainMetricaClient metricaClient; + + private MosApiMetricaService metricaService; + private final String tld = "example"; + + @BeforeEach + void setUp() { + metricaService = new MosApiMetricaService(metricaClient); + } + + @Test + void getReport_withDate_callsClientForDate() throws Exception { + LocalDate date = LocalDate.of(2025, 5, 20); + MetricaReport mockReport = new MetricaReport(2, tld, "2025-05-20", 0, ImmutableList.of()); + + when(metricaClient.getMetricaReportForDate(tld, date)).thenReturn(mockReport); + + MetricaReport result = metricaService.getReport(tld, Optional.of(date)); + + assertThat(result).isEqualTo(mockReport); + verify(metricaClient).getMetricaReportForDate(tld, date); + } + + @Test + void getReport_withoutDate_callsClientForLatest() throws Exception { + MetricaReport mockReport = new MetricaReport(2, tld, "2025-05-20", 0, ImmutableList.of()); + + when(metricaClient.getLatestMetricaReport(tld)).thenReturn(mockReport); + + MetricaReport result = metricaService.getReport(tld, Optional.empty()); + + assertThat(result).isEqualTo(mockReport); + verify(metricaClient).getLatestMetricaReport(tld); + } + + @Test + void listAvailableReports_delegatesToClient() throws Exception { + LocalDate start = LocalDate.of(2025, 1, 1); + List expectedList = ImmutableList.of(); + + when(metricaClient.listAvailableMetricaReports(tld, start, null)).thenReturn(expectedList); + + List result = + metricaService.listAvailableReports(tld, Optional.of(start), Optional.empty()); + + assertThat(result).isSameInstanceAs(expectedList); + verify(metricaClient).listAvailableMetricaReports(tld, start, null); + } +} diff --git a/core/src/test/java/google/registry/mosapi/service/MosApiStateServiceTest.java b/core/src/test/java/google/registry/mosapi/service/MosApiStateServiceTest.java new file mode 100644 index 00000000000..3133a0577a9 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/service/MosApiStateServiceTest.java @@ -0,0 +1,111 @@ +// Copyright 2025 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.mosapi.service; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import google.registry.mosapi.client.ServiceMonitoringClient; +import google.registry.mosapi.dto.servicemonitoring.ActiveIncidentsSummary; +import google.registry.mosapi.dto.servicemonitoring.AllServicesStateResponse; +import google.registry.mosapi.dto.servicemonitoring.IncidentSummary; +import google.registry.mosapi.dto.servicemonitoring.ServiceStateSummary; +import google.registry.mosapi.dto.servicemonitoring.ServiceStatus; +import google.registry.mosapi.dto.servicemonitoring.TldServiceState; +import google.registry.mosapi.exception.MosApiException; +import google.registry.mosapi.services.MosApiStateService; +import java.util.List; +import java.util.Map; +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 MosApiStateService}. */ +@ExtendWith(MockitoExtension.class) +public class MosApiStateServiceTest { + @Mock private ServiceMonitoringClient serviceMonitoringClient; + + private MosApiStateService stateService; + private final List tlds = ImmutableList.of("example", "test"); + + @BeforeEach + void setUp() { + stateService = new MosApiStateService(serviceMonitoringClient, tlds); + } + + @Test + void getServiceStateSummary_success_statusUp() throws Exception { + TldServiceState state = new TldServiceState("example", 100L, "Up", ImmutableMap.of()); + + when(serviceMonitoringClient.getServiceState("example")).thenReturn(state); + + ServiceStateSummary summary = stateService.getServiceStateSummary("example"); + + assertThat(summary.getTld()).isEqualTo("example"); + assertThat(summary.getOverallStatus()).isEqualTo("Up"); + assertThat(summary.getActiveIncidents()).isNull(); + } + + @Test + void getServiceStateSummary_success_statusDown_withIncidents() throws Exception { + IncidentSummary incident = new IncidentSummary("123", 1000L, false, "Active", null); + ServiceStatus dnsStatus = new ServiceStatus("Down", 50.0, ImmutableList.of(incident)); + Map serviceMap = ImmutableMap.of("DNS", dnsStatus); + + TldServiceState state = new TldServiceState("example", 100L, "Down", serviceMap); + + when(serviceMonitoringClient.getServiceState("example")).thenReturn(state); + + ServiceStateSummary summary = stateService.getServiceStateSummary("example"); + + assertThat(summary.getTld()).isEqualTo("example"); + assertThat(summary.getOverallStatus()).isEqualTo("Down"); + assertThat(summary.getActiveIncidents()).hasSize(1); + + ActiveIncidentsSummary activeIncident = summary.getActiveIncidents().get(0); + assertThat(activeIncident.getService()).isEqualTo("DNS"); + assertThat(activeIncident.getEmergencyThreshold()).isEqualTo(50.0); + assertThat(activeIncident.getIncidents()).containsExactly(incident); + } + + @Test + void getAllServiceStateSummaries_aggregatesAndHandlesErrors() throws Exception { + // TLD 1: Success + TldServiceState state = new TldServiceState("example", 100L, "Up", ImmutableMap.of()); + when(serviceMonitoringClient.getServiceState("example")).thenReturn(state); + + // TLD 2: Failure (simulated exception) + when(serviceMonitoringClient.getServiceState("test")) + .thenThrow(new MosApiException("Network error")); + + AllServicesStateResponse response = stateService.getAllServiceStateSummaries(); + + assertThat(response.getServiceStates()).hasSize(2); + + // Verify successful summary + ServiceStateSummary successSummary = response.getServiceStates().get(0); + assertThat(successSummary.getTld()).isEqualTo("example"); + assertThat(successSummary.getOverallStatus()).isEqualTo("Up"); + + // Verify error summary + ServiceStateSummary errorSummary = response.getServiceStates().get(1); + assertThat(errorSummary.getTld()).isEqualTo("test"); + assertThat(errorSummary.getOverallStatus()).isEqualTo("ERROR"); + assertThat(errorSummary.getActiveIncidents()).isNull(); + } +} diff --git a/core/src/test/resources/google/registry/module/routing.txt b/core/src/test/resources/google/registry/module/routing.txt index ae365ca2dc5..f4be2787d3a 100644 --- a/core/src/test/resources/google/registry/module/routing.txt +++ b/core/src/test/resources/google/registry/module/routing.txt @@ -13,6 +13,11 @@ BACKEND /_dr/admin/verifyOte VerifyOteAction BACKEND /_dr/cron/fanout TldFanoutAction GET y APP ADMIN BACKEND /_dr/epptool EppToolAction POST n APP ADMIN BACKEND /_dr/loadtest LoadTestAction POST y APP ADMIN +BACKEND /_dr/mosapi/checkalarm GetAlarmsStateAction GET n APP ADMIN +BACKEND /_dr/mosapi/getMetricaReport GetMetricaReportAction GET n APP ADMIN +BACKEND /_dr/mosapi/getServiceDowntime GetServiceDowntimeAction GET n APP ADMIN +BACKEND /_dr/mosapi/getServiceState GetServiceStateAction GET n APP ADMIN +BACKEND /_dr/mosapi/listMetricaReports ListMetricaReportsAction GET n APP ADMIN BACKEND /_dr/task/brdaCopy BrdaCopyAction POST y APP ADMIN BACKEND /_dr/task/bsaDownload BsaDownloadAction GET,POST n APP ADMIN BACKEND /_dr/task/bsaRefresh BsaRefreshAction GET,POST n APP ADMIN diff --git a/jetty/kubernetes/gateway/nomulus-route-backend.yaml b/jetty/kubernetes/gateway/nomulus-route-backend.yaml index ef8e5564fb3..edb6a60994d 100644 --- a/jetty/kubernetes/gateway/nomulus-route-backend.yaml +++ b/jetty/kubernetes/gateway/nomulus-route-backend.yaml @@ -26,6 +26,9 @@ spec: - path: type: PathPrefix value: /_dr/loadtest + - path: + type: PathPrefix + value: /_dr/mosapi backendRefs: - group: net.gke.io kind: ServiceImport @@ -62,6 +65,12 @@ spec: headers: - name: "canary" value: "true" + - path: + type: PathPrefix + value: /_dr/mosapi + headers: + - name: "canary" + value: "true" backendRefs: - group: net.gke.io kind: ServiceImport diff --git a/util/src/main/java/google/registry/util/HttpModule.java b/util/src/main/java/google/registry/util/HttpModule.java new file mode 100644 index 00000000000..216008b056a --- /dev/null +++ b/util/src/main/java/google/registry/util/HttpModule.java @@ -0,0 +1,102 @@ +// Copyright 2025 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.util; + +import dagger.Module; +import dagger.Provides; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import java.io.ByteArrayInputStream; +import java.net.http.HttpClient; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; + +@Module +public class HttpModule { + @Provides + static HttpClient.Builder provideHttpClientBuilder() { + return HttpClient.newBuilder(); + } + + @Provides + @Singleton + static HttpClient provideHttpClient() { + return HttpClient.newHttpClient(); + } + + @Provides + @Singleton + @Named("mosapiHttpClient") + static HttpClient provideMosapiHttpClient( + @Named("mosapiTlsCert") String tlsCert, @Named("mosapiTlsKey") String tlsKey) { + try { + // 1. Parse the Certificate first + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Certificate certificate = + cf.generateCertificate( + new ByteArrayInputStream(tlsCert.getBytes(StandardCharsets.UTF_8))); + + // The certificate explicitly knows if it is RSA or EC. + // We ask the cert for the algorithm, then use that to create the KeyFactory. + String detectedAlgo = certificate.getPublicKey().getAlgorithm(); + // ------------------------------------------- + + // 2. Parse the Private Key + // This regex cleans up PKCS#1, PKCS#8, and all newlines/spaces safely. + String privateKeyPem = + tlsKey + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replace("-----BEGIN RSA PRIVATE KEY-----", "") + .replace("-----END RSA PRIVATE KEY-----", "") + .replace("-----BEGIN EC PRIVATE KEY-----", "") + .replace("-----END EC PRIVATE KEY-----", "") + .replaceAll("\\s+", ""); + + byte[] encoded = Base64.getDecoder().decode(privateKeyPem); + + // 3. Use the DETECTED algorithm + KeyFactory keyFactory = KeyFactory.getInstance(detectedAlgo); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded); + PrivateKey privateKey = keyFactory.generatePrivate(keySpec); + + // 4. Create KeyStore + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(null, null); + keyStore.setKeyEntry("client", privateKey, new char[0], new Certificate[] {certificate}); + + // 5. Create SSLContext + KeyManagerFactory kmf = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, new char[0]); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(kmf.getKeyManagers(), null, null); + + return HttpClient.newBuilder().sslContext(sslContext).build(); + + } catch (Exception e) { + throw new RuntimeException("Failed to initialize MoSAPI mTLS HttpClient", e); + } + } +} diff --git a/util/src/main/java/google/registry/util/HttpUtils.java b/util/src/main/java/google/registry/util/HttpUtils.java new file mode 100644 index 00000000000..433b3adc3a3 --- /dev/null +++ b/util/src/main/java/google/registry/util/HttpUtils.java @@ -0,0 +1,171 @@ +// Copyright 2025 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.util; + +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodySubscriber; +import java.net.http.HttpResponse.BodySubscribers; +import java.net.http.HttpResponse.ResponseInfo; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.zip.GZIPInputStream; + +public class HttpUtils { + /** Private constructor to prevent instantiation. */ + private HttpUtils() {} + + /** + * Sends an HTTP GET request to the specified URL without any custom headers. + * + * @param httpClient the {@link HttpClient} to use for sending the request + * @param url the target URL + * @return the {@link HttpResponse} as a String + */ + public static HttpResponse sendGetRequest(HttpClient httpClient, String url) { + return sendGetRequest(httpClient, url, ImmutableMap.of()); + } + + /** + * Sends an HTTP GET request with custom headers to the specified URL. + * + * @param httpClient the {@link HttpClient} to use for sending the request + * @param url the target URL + * @param headers a {@link Map} of header keys and values to add to the request + * @return the {@link HttpResponse} as a String + */ + public static HttpResponse sendGetRequest( + HttpClient httpClient, String url, Map headers) { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(URI.create(url)).GET(); + for (Map.Entry header : headers.entrySet()) { + requestBuilder.header(header.getKey(), header.getValue()); + } + return send(httpClient, requestBuilder.build()); + } + + /** + * Sends an HTTP POST request with an empty body to the specified URL. + * + * @param httpClient the {@link HttpClient} to use for sending the request + * @param url the target URL + * @return the {@link HttpResponse} as a String + */ + public static HttpResponse sendPostRequest(HttpClient httpClient, String url) { + return sendPostRequest(httpClient, url, ImmutableMap.of()); + } + + /** + * Sends an HTTP POST request with an empty body and custom headers to the specified URL. + * + * @param httpClient the {@link HttpClient} to use for sending the request + * @param url the target URL + * @param headers a {@link Map} of header keys and values to add to the request + * @return the {@link HttpResponse} as a String + */ + public static HttpResponse sendPostRequest( + HttpClient httpClient, String url, Map headers) { + HttpRequest.Builder requestBuilder = + HttpRequest.newBuilder().uri(URI.create(url)).POST(HttpRequest.BodyPublishers.noBody()); + for (Map.Entry header : headers.entrySet()) { + requestBuilder.header(header.getKey(), header.getValue()); + } + return send(httpClient, requestBuilder.build()); + } + + /** + * Sends an HTTP POST request with a String body and custom headers to the specified URL. + * + * @param httpClient the {@link HttpClient} to use for sending the request + * @param url the target URL + * @param headers a {@link Map} of header keys and values to add to the request + * @param body the String request body to send (can be null or empty for no body) + * @return the {@link HttpResponse} as a String + */ + public static HttpResponse sendPostRequest( + HttpClient httpClient, String url, Map headers, String body) { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(URI.create(url)); + + if (body == null || body.isEmpty()) { + requestBuilder.POST(HttpRequest.BodyPublishers.noBody()); + } else { + requestBuilder.POST(HttpRequest.BodyPublishers.ofString(body)); + } + + for (Map.Entry header : headers.entrySet()) { + requestBuilder.header(header.getKey(), header.getValue()); + } + return send(httpClient, requestBuilder.build()); + } + + public static HttpResponse sendGetRequestWithDecompression( + HttpClient httpClient, String url, Map headers) { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(URI.create(url)).GET(); + for (Map.Entry header : headers.entrySet()) { + requestBuilder.header(header.getKey(), header.getValue()); + } + return sendWithDecompression(httpClient, requestBuilder.build()); + } + + private static HttpResponse sendWithDecompression( + HttpClient httpClient, HttpRequest request) { + try { + return httpClient.send(request, createDecompressingBodyHandler()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static BodyHandler createDecompressingBodyHandler() { + return (ResponseInfo responseInfo) -> { + String encoding = responseInfo.headers().firstValue("Content-Encoding").orElse(""); + if (encoding.equalsIgnoreCase("gzip")) { + BodySubscriber upstream = BodySubscribers.ofInputStream(); + return BodySubscribers.mapping( + upstream, + (InputStream is) -> { + try (GZIPInputStream gzipIs = new GZIPInputStream(is)) { + return new String(gzipIs.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } else { + return BodySubscribers.ofString(StandardCharsets.UTF_8); + } + }; + } + + /** + * Sends a pre-built {@link HttpRequest} and handles exceptions. + * + * @param httpClient the client + * @param request the request + * @return the {@link HttpResponse} + */ + private static HttpResponse send(HttpClient httpClient, HttpRequest request) { + try { + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +}