From e79fc94c2747760766bba1f27150864bcf6cd4d6 Mon Sep 17 00:00:00 2001 From: Nilay Shah Date: Wed, 10 Dec 2025 09:57:14 +0000 Subject: [PATCH 1/5] Add GetServiceState action for MoSAPI service monitoring Implements the `/api/mosapi/getServiceState` endpoint to retrieve service health summaries for TLDs from the MoSAPI system. - Introduces `GetServiceStateAction` to fetch TLD service status. - Implements `MosApiStateService` to transform raw MoSAPI responses into a curated `ServiceStateSummary`. - Uses concurrent processing with a fixed thread pool to fetch states for all configured TLDs efficiently while respecting MoSAPI rate limits. junit test added --- .../registry/config/RegistryConfig.java | 6 + .../config/RegistryConfigSettings.java | 1 + .../registry/config/files/default-config.yaml | 4 + .../registry/module/RequestComponent.java | 5 + .../mosapi/GetServiceStateAction.java | 72 +++++++++ .../registry/mosapi/MosApiStateService.java | 114 ++++++++++++++ .../mosapi/ServiceMonitoringClient.java | 58 +++++++ .../model/AllServicesStateResponse.java | 43 ++++++ .../mosapi/model/IncidentSummary.java | 77 ++++++++++ .../mosapi/model/ServiceStateSummary.java | 69 +++++++++ .../registry/mosapi/model/ServiceStatus.java | 62 ++++++++ .../mosapi/model/TldServiceState.java | 71 +++++++++ .../registry/mosapi/module/MosApiModule.java | 19 +++ .../mosapi/module/MosApiRequestModule.java | 33 ++++ .../registry/module/TestRequestComponent.java | 2 + .../mosapi/GetServiceStateActionTest.java | 87 +++++++++++ .../mosapi/MosApiStateServiceTest.java | 136 ++++++++++++++++ .../mosapi/ServiceMonitoringClientTest.java | 145 ++++++++++++++++++ .../model/AllServicesStateResponseTest.java | 73 +++++++++ .../mosapi/model/IncidentSummaryTest.java | 85 ++++++++++ .../mosapi/model/ServiceStateSummaryTest.java | 93 +++++++++++ .../mosapi/model/ServiceStatusTest.java | 83 ++++++++++ .../mosapi/model/TldServiceStateTest.java | 93 +++++++++++ .../module/MosApiRequestModuleTest.java | 57 +++++++ .../google/registry/module/routing.txt | 1 + 25 files changed, 1489 insertions(+) create mode 100644 core/src/main/java/google/registry/mosapi/GetServiceStateAction.java create mode 100644 core/src/main/java/google/registry/mosapi/MosApiStateService.java create mode 100644 core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java create mode 100644 core/src/main/java/google/registry/mosapi/model/AllServicesStateResponse.java create mode 100644 core/src/main/java/google/registry/mosapi/model/IncidentSummary.java create mode 100644 core/src/main/java/google/registry/mosapi/model/ServiceStateSummary.java create mode 100644 core/src/main/java/google/registry/mosapi/model/ServiceStatus.java create mode 100644 core/src/main/java/google/registry/mosapi/model/TldServiceState.java create mode 100644 core/src/main/java/google/registry/mosapi/module/MosApiRequestModule.java create mode 100644 core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java create mode 100644 core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java create mode 100644 core/src/test/java/google/registry/mosapi/ServiceMonitoringClientTest.java create mode 100644 core/src/test/java/google/registry/mosapi/model/AllServicesStateResponseTest.java create mode 100644 core/src/test/java/google/registry/mosapi/model/IncidentSummaryTest.java create mode 100644 core/src/test/java/google/registry/mosapi/model/ServiceStateSummaryTest.java create mode 100644 core/src/test/java/google/registry/mosapi/model/ServiceStatusTest.java create mode 100644 core/src/test/java/google/registry/mosapi/model/TldServiceStateTest.java create mode 100644 core/src/test/java/google/registry/mosapi/module/MosApiRequestModuleTest.java diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 9ea98c4bf13..14ba4c82aac 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -1462,6 +1462,12 @@ public static ImmutableSet provideMosapiServices(RegistryConfigSettings return ImmutableSet.copyOf(config.mosapi.services); } + @Provides + @Config("mosapiTldThreadCnt") + public static Integer provideMosapiTldThreads(RegistryConfigSettings config) { + return config.mosapi.tldThreadCnt; + } + 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 1d94be17149..820510f9e25 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -272,5 +272,6 @@ public static class MosApi { public String entityType; public List tlds; public List services; + public Integer tldThreadCnt; } } 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 060665e9f25..fd1938b2a3a 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 @@ -642,4 +642,8 @@ mosapi: - "epp" - "dnssec" + #Provides a fixed thread pool for parallel TLD processing. + # @see + # ICANN MoSAPI Specification, Section 12.3 + tldThreadCnt: 4 diff --git a/core/src/main/java/google/registry/module/RequestComponent.java b/core/src/main/java/google/registry/module/RequestComponent.java index 2444567c8ee..8ff3a3ae492 100644 --- a/core/src/main/java/google/registry/module/RequestComponent.java +++ b/core/src/main/java/google/registry/module/RequestComponent.java @@ -62,6 +62,8 @@ import google.registry.module.ReadinessProbeAction.ReadinessProbeActionPubApi; import google.registry.module.ReadinessProbeAction.ReadinessProbeConsoleAction; import google.registry.monitoring.whitebox.WhiteboxModule; +import google.registry.mosapi.GetServiceStateAction; +import google.registry.mosapi.module.MosApiRequestModule; import google.registry.rdap.RdapAutnumAction; import google.registry.rdap.RdapDomainAction; import google.registry.rdap.RdapDomainSearchAction; @@ -151,6 +153,7 @@ EppToolModule.class, IcannReportingModule.class, LoadTestModule.class, + MosApiRequestModule.class, RdapModule.class, RdeModule.class, ReportingModule.class, @@ -232,6 +235,8 @@ interface RequestComponent { GenerateZoneFilesAction generateZoneFilesAction(); + GetServiceStateAction getServiceStateAction(); + IcannReportingStagingAction icannReportingStagingAction(); IcannReportingUploadAction icannReportingUploadAction(); diff --git a/core/src/main/java/google/registry/mosapi/GetServiceStateAction.java b/core/src/main/java/google/registry/mosapi/GetServiceStateAction.java new file mode 100644 index 00000000000..3bb37818deb --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/GetServiceStateAction.java @@ -0,0 +1,72 @@ +// 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; + +import com.google.common.flogger.FluentLogger; +import com.google.common.net.MediaType; +import com.google.gson.Gson; +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 or all TLDs. */ +@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/MosApiStateService.java b/core/src/main/java/google/registry/mosapi/MosApiStateService.java new file mode 100644 index 00000000000..2f24c9c3194 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/MosApiStateService.java @@ -0,0 +1,114 @@ +// 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; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.flogger.FluentLogger; +import google.registry.config.RegistryConfig.Config; +import google.registry.mosapi.model.AllServicesStateResponse; +import google.registry.mosapi.model.ServiceStateSummary; +import google.registry.mosapi.model.ServiceStatus; +import google.registry.mosapi.model.TldServiceState; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +/** 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 ExecutorService tldExecutor; + + private final ImmutableSet tlds; + + private static final String DOWN_STATUS = "Down"; + private static final String FETCH_ERROR_STATUS = "ERROR"; + + @Inject + public MosApiStateService( + ServiceMonitoringClient serviceMonitoringClient, + @Config("mosapiTlds") ImmutableSet tlds, + @Named("mosapiTldExecutor") ExecutorService tldExecutor) { + this.serviceMonitoringClient = serviceMonitoringClient; + this.tlds = tlds; + this.tldExecutor = tldExecutor; + } + + /** Shared internal logic to fetch raw data from ICANN MoSAPI state monitoring. */ + private TldServiceState fetchRawState(String tld) throws MosApiException { + return serviceMonitoringClient.getTldServiceState(tld); + } + + /** Fetches and transforms the service state for a given TLD into a summary. */ + public ServiceStateSummary getServiceStateSummary(String tld) throws MosApiException { + return transformToSummary(fetchRawState(tld)); + } + + /** Fetches and transforms the service state for all configured TLDs. */ + public AllServicesStateResponse getAllServiceStateSummaries() { + ImmutableList> futures = + tlds.stream() + .map( + tld -> + CompletableFuture.supplyAsync( + () -> { + try { + return getServiceStateSummary(tld); + } catch (MosApiException e) { + logger.atWarning().withCause(e).log( + "Failed to get service state for TLD %s.", tld); + // we don't want to throw exception if fetch failed + return new ServiceStateSummary(tld, FETCH_ERROR_STATUS, null); + } + }, + tldExecutor)) + .collect(ImmutableList.toImmutableList()); + + ImmutableList summaries = + futures.stream() + .map(CompletableFuture::join) // Waits for all tasks to complete + .collect(toImmutableList()); + + return new AllServicesStateResponse(summaries); + } + + private ServiceStateSummary transformToSummary(TldServiceState rawState) { + List activeIncidents = null; + if (DOWN_STATUS.equalsIgnoreCase(rawState.getStatus())) { + activeIncidents = + rawState.getServiceStatuses().entrySet().stream() + .filter( + entry -> { + ServiceStatus serviceStatus = entry.getValue(); + return serviceStatus.getIncidents() != null + && !serviceStatus.getIncidents().isEmpty(); + }) + .map( + entry -> + new ServiceStatus( + // key is the service name + entry.getKey(), + entry.getValue().getEmergencyThreshold(), + entry.getValue().getIncidents())) + .collect(toImmutableList()); + } + return new ServiceStateSummary(rawState.getTld(), rawState.getStatus(), activeIncidents); + } +} diff --git a/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java b/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java new file mode 100644 index 00000000000..5991fc2fc76 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java @@ -0,0 +1,58 @@ +// 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; + +import com.google.gson.Gson; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; +import google.registry.mosapi.model.MosApiErrorResponse; +import google.registry.mosapi.model.TldServiceState; +import jakarta.inject.Inject; +import java.util.Collections; +import okhttp3.Response; + +/** 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 getTldServiceState(String tld) throws MosApiException { + String endpoint = "v2/monitoring/state"; + try (Response response = + mosApiClient.sendGetRequest( + tld, endpoint, Collections.emptyMap(), Collections.emptyMap())) { + if (!response.isSuccessful()) { + throw MosApiException.create( + gson.fromJson(response.body().charStream(), MosApiErrorResponse.class)); + } + return gson.fromJson(response.body().charStream(), TldServiceState.class); + } catch (JsonIOException | JsonSyntaxException e) { + // Catch Gson's runtime exceptions (parsing errors) and wrap them + throw new MosApiException("Failed to parse TLD service state response", e); + } + } +} diff --git a/core/src/main/java/google/registry/mosapi/model/AllServicesStateResponse.java b/core/src/main/java/google/registry/mosapi/model/AllServicesStateResponse.java new file mode 100644 index 00000000000..2c223c4e694 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/model/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.model; + +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/model/IncidentSummary.java b/core/src/main/java/google/registry/mosapi/model/IncidentSummary.java new file mode 100644 index 00000000000..03993d4077a --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/model/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.model; + +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/model/ServiceStateSummary.java b/core/src/main/java/google/registry/mosapi/model/ServiceStateSummary.java new file mode 100644 index 00000000000..be690185b1f --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/model/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.model; + +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/model/ServiceStatus.java b/core/src/main/java/google/registry/mosapi/model/ServiceStatus.java new file mode 100644 index 00000000000..389ec10abcc --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/model/ServiceStatus.java @@ -0,0 +1,62 @@ +// 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.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +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 + */ + @Expose + @SerializedName("status") + 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. + @Expose + @SerializedName("emergencyThreshold") + private double emergencyThreshold; + + @Expose + @SerializedName("incidents") + 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/model/TldServiceState.java b/core/src/main/java/google/registry/mosapi/model/TldServiceState.java new file mode 100644 index 00000000000..4cb2501976a --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/model/TldServiceState.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.model; + +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 + @SerializedName("tld") + 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 + @SerializedName("status") + 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/module/MosApiModule.java b/core/src/main/java/google/registry/mosapi/module/MosApiModule.java index 69f331f17b4..31ef9dd4a62 100644 --- a/core/src/main/java/google/registry/mosapi/module/MosApiModule.java +++ b/core/src/main/java/google/registry/mosapi/module/MosApiModule.java @@ -32,6 +32,8 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; @@ -184,4 +186,21 @@ static OkHttpClient provideMosapiHttpClient( .sslSocketFactory(sslContext.getSocketFactory(), trustManager) .build(); } + + /** + * Provides a fixed thread pool for parallel TLD processing. + * + *

Strictly bound to 4 threads to comply with MoSAPI session limits (4 concurrent sessions per + * certificate). This is used by MosApiStateService to fetch data in parallel. + * + * @see ICANN MoSAPI Specification, + * Section 12.3 + */ + @Provides + @Singleton + @Named("mosapiTldExecutor") + static ExecutorService provideMosapiTldExecutor( + @Config("mosapiTldThreadCnt") int threadPoolSize) { + return Executors.newFixedThreadPool(threadPoolSize); + } } 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..4c8c7db0fb4 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/module/MosApiRequestModule.java @@ -0,0 +1,33 @@ +// 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.util.Optional; + +/** Dagger module for MoSAPI requests. */ +@Module +public final class MosApiRequestModule { + @Provides + @Parameter("tld") + static Optional provideTld(HttpServletRequest req) { + return extractOptionalParameter(req, "tld"); + } +} 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/GetServiceStateActionTest.java b/core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java new file mode 100644 index 00000000000..4acccd17374 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java @@ -0,0 +1,87 @@ +// 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; + +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.model.AllServicesStateResponse; +import google.registry.mosapi.model.ServiceStateSummary; +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", null)) + .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/MosApiStateServiceTest.java b/core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java new file mode 100644 index 00000000000..95abb918278 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java @@ -0,0 +1,136 @@ +// 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.MoreExecutors; +import google.registry.mosapi.model.AllServicesStateResponse; +import google.registry.mosapi.model.IncidentSummary; +import google.registry.mosapi.model.ServiceStateSummary; +import google.registry.mosapi.model.ServiceStatus; +import google.registry.mosapi.model.TldServiceState; +import java.util.concurrent.ExecutorService; +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) +class MosApiStateServiceTest { + + @Mock private ServiceMonitoringClient client; + + private final ExecutorService executor = MoreExecutors.newDirectExecutorService(); + + private MosApiStateService service; + + @BeforeEach + void setUp() { + service = new MosApiStateService(client, ImmutableSet.of("tld1", "tld2"), executor); + } + + @Test + void testGetServiceStateSummary_upStatus_returnsEmptyIncidents() throws Exception { + TldServiceState rawState = new TldServiceState("tld1", 12345L, "Up", ImmutableMap.of()); + when(client.getTldServiceState("tld1")).thenReturn(rawState); + + ServiceStateSummary result = service.getServiceStateSummary("tld1"); + + assertThat(result.getTld()).isEqualTo("tld1"); + assertThat(result.getOverallStatus()).isEqualTo("Up"); + assertThat(result.getActiveIncidents()).isNull(); + } + + @Test + void testGetServiceStateSummary_downStatus_filtersActiveIncidents() throws Exception { + IncidentSummary dnsIncident = new IncidentSummary("inc-1", 100L, false, "Open", null); + ServiceStatus dnsService = new ServiceStatus("Down", 50.0, ImmutableList.of(dnsIncident)); + + ServiceStatus rdapService = new ServiceStatus("Up", 0.0, ImmutableList.of()); + + TldServiceState rawState = + new TldServiceState( + "tld1", 12345L, "Down", ImmutableMap.of("DNS", dnsService, "RDAP", rdapService)); + + when(client.getTldServiceState("tld1")).thenReturn(rawState); + + ServiceStateSummary result = service.getServiceStateSummary("tld1"); + + assertThat(result.getOverallStatus()).isEqualTo("Down"); + assertThat(result.getActiveIncidents()).hasSize(1); + + ServiceStatus incidentSummary = result.getActiveIncidents().get(0); + assertThat(incidentSummary.getStatus()).isEqualTo("DNS"); + assertThat(incidentSummary.getIncidents()).containsExactly(dnsIncident); + } + + @Test + void testGetServiceStateSummary_throwsException_whenClientFails() throws Exception { + when(client.getTldServiceState("tld1")).thenThrow(new MosApiException("Network error", null)); + + assertThrows(MosApiException.class, () -> service.getServiceStateSummary("tld1")); + } + + @Test + void testGetAllServiceStateSummaries_success() throws Exception { + TldServiceState state1 = new TldServiceState("tld1", 1L, "Up", ImmutableMap.of()); + TldServiceState state2 = new TldServiceState("tld2", 2L, "Up", ImmutableMap.of()); + + when(client.getTldServiceState("tld1")).thenReturn(state1); + when(client.getTldServiceState("tld2")).thenReturn(state2); + + AllServicesStateResponse response = service.getAllServiceStateSummaries(); + + assertThat(response.getServiceStates()).hasSize(2); + assertThat(response.getServiceStates().stream().map(ServiceStateSummary::getTld)) + .containsExactly("tld1", "tld2"); + } + + @Test + void testGetAllServiceStateSummaries_partialFailure_returnsErrorState() throws Exception { + TldServiceState state1 = new TldServiceState("tld1", 1L, "Up", ImmutableMap.of()); + when(client.getTldServiceState("tld1")).thenReturn(state1); + + when(client.getTldServiceState("tld2")).thenThrow(new MosApiException("Failure", null)); + + AllServicesStateResponse response = service.getAllServiceStateSummaries(); + + assertThat(response.getServiceStates()).hasSize(2); + + ServiceStateSummary summary1 = + response.getServiceStates().stream() + .filter(s -> s.getTld().equals("tld1")) + .findFirst() + .get(); + assertThat(summary1.getOverallStatus()).isEqualTo("Up"); + + ServiceStateSummary summary2 = + response.getServiceStates().stream() + .filter(s -> s.getTld().equals("tld2")) + .findFirst() + .get(); + + assertThat(summary2.getOverallStatus()).isEqualTo("ERROR"); + assertThat(summary2.getActiveIncidents()).isNull(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/ServiceMonitoringClientTest.java b/core/src/test/java/google/registry/mosapi/ServiceMonitoringClientTest.java new file mode 100644 index 00000000000..d749817e919 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/ServiceMonitoringClientTest.java @@ -0,0 +1,145 @@ +// 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import google.registry.mosapi.model.TldServiceState; +import java.io.IOException; +import java.io.Reader; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ServiceMonitoringClientTest { + + private final MosApiClient mosApiClient = mock(MosApiClient.class); + private final Gson gson = new Gson(); + private ServiceMonitoringClient client; + + @BeforeEach + void setUp() { + client = new ServiceMonitoringClient(mosApiClient, gson); + } + + @Test + void getTldServiceState_success() throws Exception { + String json = "{ \"service\": \"RDAP\", \"status\": \"ACTIVE\" }"; + + Response realResponse = createResponse(200, json); + + when(mosApiClient.sendGetRequest(anyString(), anyString(), anyMap(), anyMap())) + .thenReturn(realResponse); + + TldServiceState result = client.getTldServiceState("example"); + + assertNotNull(result); + } + + @Test + void getTldServiceState_apiErrorResponse_throwsMosApiException() throws Exception { + String errorJson = "{ \"code\": 400, \"message\": \"Invalid TLD\" }"; + + Response realResponse = createResponse(400, errorJson); + + when(mosApiClient.sendGetRequest(anyString(), anyString(), anyMap(), anyMap())) + .thenReturn(realResponse); + + MosApiException thrown = + assertThrows( + MosApiException.class, + () -> { + client.getTldServiceState("invalid"); + }); + assertThat(thrown).hasMessageThat().contains("Invalid TLD"); + } + + @Test + void getTldServiceState_malformedJson_throwsMosApiException() throws Exception { + String garbage = "Gateway Timeout"; + Response realResponse = createResponse(200, garbage); + + when(mosApiClient.sendGetRequest(anyString(), anyString(), anyMap(), anyMap())) + .thenReturn(realResponse); + + MosApiException thrown = + assertThrows( + MosApiException.class, + () -> { + client.getTldServiceState("example"); + }); + + assertEquals("Failed to parse TLD service state response", thrown.getMessage()); + assertEquals(JsonSyntaxException.class, thrown.getCause().getClass()); + } + + @Test + void getTldServiceState_networkFailureDuringRead_throwsMosApiException() throws Exception { + + ResponseBody mockBody = mock(ResponseBody.class); + Reader mockReader = mock(Reader.class); + + Response mixedResponse = + new Response.Builder() + .request(new Request.Builder().url("http://localhost/").build()) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(mockBody) + .build(); + + when(mosApiClient.sendGetRequest(anyString(), anyString(), anyMap(), anyMap())) + .thenReturn(mixedResponse); + when(mockBody.charStream()).thenReturn(mockReader); + + when(mockReader.read(any(char[].class), anyInt(), anyInt())) + .thenThrow(new IOException("Network failure during read")); + + MosApiException thrown = + assertThrows( + MosApiException.class, + () -> { + client.getTldServiceState("example"); + }); + + assertEquals("Failed to parse TLD service state response", thrown.getMessage()); + assertEquals("Network failure during read", thrown.getCause().getCause().getMessage()); + } + + private Response createResponse(int code, String jsonBody) { + return new Response.Builder() + .request(new Request.Builder().url("http://localhost/").build()) + .protocol(Protocol.HTTP_1_1) + .code(code) + .message(code == 200 ? "OK" : "Error") + .body(ResponseBody.create(jsonBody, MediaType.get("application/json"))) + .build(); + } +} diff --git a/core/src/test/java/google/registry/mosapi/model/AllServicesStateResponseTest.java b/core/src/test/java/google/registry/mosapi/model/AllServicesStateResponseTest.java new file mode 100644 index 00000000000..eb2241fc309 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/model/AllServicesStateResponseTest.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.model; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link AllServicesStateResponse}. */ +public class AllServicesStateResponseTest { + private final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + + @Test + void testConstructorAndGetter() { + ServiceStateSummary summary = new ServiceStateSummary("tld", "Up", Collections.emptyList()); + ImmutableList summaries = ImmutableList.of(summary); + + AllServicesStateResponse response = new AllServicesStateResponse(summaries); + + assertThat(response.getServiceStates()).containsExactly(summary); + } + + @Test + void testJsonSerialization_setsCorrectFieldName() { + ServiceStateSummary summary = new ServiceStateSummary("test.tld", "Down", null); + AllServicesStateResponse response = new AllServicesStateResponse(ImmutableList.of(summary)); + + String json = gson.toJson(response); + + // Verify the JSON structure contains the specific key + assertThat(json).contains("\"serviceStates\":"); + assertThat(json).contains("\"tld\":\"test.tld\""); + assertThat(json).contains("\"overallStatus\":\"Down\""); + } + + @Test + void testJsonDeserialization_readsCorrectFieldName() { + String json = + "{\"serviceStates\": [{\"tld\": \"example.tld\", " + + "\"overallStatus\": \"Up\", \"activeIncidents\": []}]}"; + + AllServicesStateResponse response = gson.fromJson(json, AllServicesStateResponse.class); + + assertThat(response.getServiceStates()).hasSize(1); + assertThat(response.getServiceStates().get(0).getTld()).isEqualTo("example.tld"); + assertThat(response.getServiceStates().get(0).getOverallStatus()).isEqualTo("Up"); + } + + @Test + void testJsonSerialization_handlesEmptyList() { + AllServicesStateResponse response = new AllServicesStateResponse(ImmutableList.of()); + + String json = gson.toJson(response); + + assertThat(json).isEqualTo("{\"serviceStates\":[]}"); + } +} diff --git a/core/src/test/java/google/registry/mosapi/model/IncidentSummaryTest.java b/core/src/test/java/google/registry/mosapi/model/IncidentSummaryTest.java new file mode 100644 index 00000000000..81064a5c131 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/model/IncidentSummaryTest.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.model; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link IncidentSummary}. */ +public class IncidentSummaryTest { + + // Use GsonBuilder to respect @Expose annotations if needed, though default new Gson() works too. + private final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + + @Test + void testConstructorAndGetters_allFieldsPopulated() { + IncidentSummary incident = + new IncidentSummary("INC-001", 1672531200000L, false, "Open", 1672617600000L); + + assertThat(incident.getIncidentID()).isEqualTo("INC-001"); + assertThat(incident.getStartTime()).isEqualTo(1672531200000L); + assertThat(incident.isFalsePositive()).isFalse(); + assertThat(incident.getState()).isEqualTo("Open"); + assertThat(incident.getEndTime()).isEqualTo(1672617600000L); + } + + @Test + void testConstructorAndGetters_nullEndTime() { + // Tests that endTime can be null (e.g. for an ongoing incident) + IncidentSummary incident = new IncidentSummary("INC-002", 1672531200000L, true, "Closed", null); + + assertThat(incident.getIncidentID()).isEqualTo("INC-002"); + assertThat(incident.isFalsePositive()).isTrue(); + assertThat(incident.getEndTime()).isNull(); + } + + @Test + void testJsonSerialization() { + IncidentSummary incident = + new IncidentSummary("INC-001", 1234567890000L, false, "Active", 1234569990000L); + + String json = gson.toJson(incident); + + // Verify fields are present and correctly named via @SerializedName + assertThat(json).contains("\"incidentID\":\"INC-001\""); + assertThat(json).contains("\"startTime\":1234567890000"); + assertThat(json).contains("\"falsePositive\":false"); + assertThat(json).contains("\"state\":\"Active\""); + assertThat(json).contains("\"endTime\":1234569990000"); + } + + @Test + void testJsonDeserialization() { + String json = + "{" + + "\"incidentID\": \"INC-999\"," + + "\"startTime\": 1000000," + + "\"falsePositive\": true," + + "\"state\": \"Resolved\"," + + "\"endTime\": 2000000" + + "}"; + + IncidentSummary incident = gson.fromJson(json, IncidentSummary.class); + + assertThat(incident.getIncidentID()).isEqualTo("INC-999"); + assertThat(incident.getStartTime()).isEqualTo(1000000L); + assertThat(incident.isFalsePositive()).isTrue(); + assertThat(incident.getState()).isEqualTo("Resolved"); + assertThat(incident.getEndTime()).isEqualTo(2000000L); + } +} diff --git a/core/src/test/java/google/registry/mosapi/model/ServiceStateSummaryTest.java b/core/src/test/java/google/registry/mosapi/model/ServiceStateSummaryTest.java new file mode 100644 index 00000000000..fa1aa93794f --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/model/ServiceStateSummaryTest.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.model; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ServiceStateSummary}. */ +public class ServiceStateSummaryTest { + + private final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + + @Test + void testConstructorAndGetters_withIncidents() { + ServiceStatus status = new ServiceStatus("Down", 100.0, Collections.emptyList()); + ServiceStateSummary summary = + new ServiceStateSummary("example.tld", "Down", ImmutableList.of(status)); + + assertThat(summary.getTld()).isEqualTo("example.tld"); + assertThat(summary.getOverallStatus()).isEqualTo("Down"); + assertThat(summary.getActiveIncidents()).containsExactly(status); + } + + @Test + void testConstructorAndGetters_nullIncidents() { + ServiceStateSummary summary = new ServiceStateSummary("example.tld", "Up", null); + + assertThat(summary.getTld()).isEqualTo("example.tld"); + assertThat(summary.getOverallStatus()).isEqualTo("Up"); + assertThat(summary.getActiveIncidents()).isNull(); + } + + @Test + void testJsonSerialization_includesAllFields() { + ServiceStatus status = new ServiceStatus("Down", 50.0, null); + ServiceStateSummary summary = + new ServiceStateSummary("test.tld", "Down", ImmutableList.of(status)); + + String json = gson.toJson(summary); + + assertThat(json).contains("\"tld\":\"test.tld\""); + assertThat(json).contains("\"overallStatus\":\"Down\""); + assertThat(json).contains("\"activeIncidents\":"); + } + + @Test + void testJsonSerialization_excludesNullIncidents_ifNotConfiguredToSerializeNulls() { + + ServiceStateSummary summary = new ServiceStateSummary("test.tld", "Up", null); + + String json = gson.toJson(summary); + + assertThat(json).contains("\"tld\":\"test.tld\""); + assertThat(json).contains("\"overallStatus\":\"Up\""); + assertThat(json).doesNotContain("\"activeIncidents\""); + } + + @Test + void testJsonDeserialization() { + String json = + "{" + + "\"tld\": \"example.tld\"," + + "\"overallStatus\": \"Down\"," + + "\"activeIncidents\": [" + + " {\"status\": \"Down\", \"emergencyThreshold\": 100.0, \"incidents\": []}" + + "]" + + "}"; + + ServiceStateSummary summary = gson.fromJson(json, ServiceStateSummary.class); + + assertThat(summary.getTld()).isEqualTo("example.tld"); + assertThat(summary.getOverallStatus()).isEqualTo("Down"); + assertThat(summary.getActiveIncidents()).hasSize(1); + assertThat(summary.getActiveIncidents().get(0).getStatus()).isEqualTo("Down"); + } +} diff --git a/core/src/test/java/google/registry/mosapi/model/ServiceStatusTest.java b/core/src/test/java/google/registry/mosapi/model/ServiceStatusTest.java new file mode 100644 index 00000000000..6175010576b --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/model/ServiceStatusTest.java @@ -0,0 +1,83 @@ +// 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.model; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ServiceStatus}. */ +public class ServiceStatusTest { + + private final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + + @Test + void testConstructorAndGetters_emptyIncidents() { + ServiceStatus serviceStatus = new ServiceStatus("Up", 0.0, Collections.emptyList()); + + assertThat(serviceStatus.getStatus()).isEqualTo("Up"); + assertThat(serviceStatus.getEmergencyThreshold()).isEqualTo(0.0); + assertThat(serviceStatus.getIncidents()).isEmpty(); + } + + @Test + void testConstructorAndGetters_withIncidents() { + IncidentSummary incident = new IncidentSummary("I1", 100L, false, "Open", null); + ServiceStatus serviceStatus = new ServiceStatus("Down", 50.5, ImmutableList.of(incident)); + + assertThat(serviceStatus.getStatus()).isEqualTo("Down"); + assertThat(serviceStatus.getEmergencyThreshold()).isEqualTo(50.5); + assertThat(serviceStatus.getIncidents()).containsExactly(incident); + } + + @Test + void testJsonSerialization() { + IncidentSummary incident = new IncidentSummary("I1", 100L, false, "Open", null); + ServiceStatus serviceStatus = new ServiceStatus("Down", 99.9, ImmutableList.of(incident)); + + String json = gson.toJson(serviceStatus); + + assertThat(json).contains("\"status\":\"Down\""); + assertThat(json).contains("\"emergencyThreshold\":99.9"); + assertThat(json).contains("\"incidents\":"); + assertThat(json).contains("\"incidentID\":\"I1\""); + } + + @Test + void testJsonDeserialization() { + String json = + "{" + + "\"status\": \"Disabled\"," + + "\"emergencyThreshold\": 10.0," + + "\"incidents\": [{" + + " \"incidentID\": \"I2\"," + + " \"startTime\": 200," + + " \"falsePositive\": true," + + " \"state\": \"Closed\"" + + "}]" + + "}"; + + ServiceStatus serviceStatus = gson.fromJson(json, ServiceStatus.class); + + assertThat(serviceStatus.getStatus()).isEqualTo("Disabled"); + assertThat(serviceStatus.getEmergencyThreshold()).isEqualTo(10.0); + assertThat(serviceStatus.getIncidents()).hasSize(1); + assertThat(serviceStatus.getIncidents().get(0).getIncidentID()).isEqualTo("I2"); + } +} diff --git a/core/src/test/java/google/registry/mosapi/model/TldServiceStateTest.java b/core/src/test/java/google/registry/mosapi/model/TldServiceStateTest.java new file mode 100644 index 00000000000..dcd8eebf5bf --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/model/TldServiceStateTest.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.model; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link TldServiceState}. */ +public class TldServiceStateTest { + + private final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + + @Test + void testConstructorAndGetters_allFieldsPopulated() { + ServiceStatus dnsStatus = new ServiceStatus("Up", 0.0, Collections.emptyList()); + Map services = ImmutableMap.of("DNS", dnsStatus); + + TldServiceState state = new TldServiceState("example.tld", 123456L, "Up", services); + + assertThat(state.getTld()).isEqualTo("example.tld"); + assertThat(state.getLastUpdateApiDatabase()).isEqualTo(123456L); + assertThat(state.getStatus()).isEqualTo("Up"); + assertThat(state.getServiceStatuses()).containsEntry("DNS", dnsStatus); + } + + @Test + void testJsonSerialization() { + ServiceStatus rddsStatus = new ServiceStatus("Down", 100.0, ImmutableList.of()); + Map services = ImmutableMap.of("RDDS", rddsStatus); + + TldServiceState state = new TldServiceState("test.tld", 99999L, "Down", services); + + String json = gson.toJson(state); + + // Verify annotated fields are present + assertThat(json).contains("\"tld\":\"test.tld\""); + assertThat(json).contains("\"status\":\"Down\""); + assertThat(json).contains("\"testedServices\":"); + assertThat(json).contains("\"RDDS\":"); + + // Verify unannotated field (lastUpdateApiDatabase) is EXCLUDED + assertThat(json).doesNotContain("lastUpdateApiDatabase"); + assertThat(json).doesNotContain("99999"); + } + + @Test + void testJsonDeserialization() { + String json = + "{" + + "\"tld\": \"example.tld\"," + + "\"status\": \"Up\"," + // Note: lastUpdateApiDatabase is usually ignored if missing @Expose in strict mode + + "\"lastUpdateApiDatabase\": 55555," + + "\"testedServices\": {" + + " \"EPP\": {" + + " \"status\": \"Up\"," + + " \"emergencyThreshold\": 0.0," + + " \"incidents\": []" + + " }" + + "}" + + "}"; + + TldServiceState state = gson.fromJson(json, TldServiceState.class); + + assertThat(state.getTld()).isEqualTo("example.tld"); + assertThat(state.getStatus()).isEqualTo("Up"); + + // Check map deserialization + assertThat(state.getServiceStatuses()).containsKey("EPP"); + assertThat(state.getServiceStatuses().get("EPP").getStatus()).isEqualTo("Up"); + + assertThat(state.getLastUpdateApiDatabase()).isEqualTo(0L); + } +} 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..a8f59ac097a --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/module/MosApiRequestModuleTest.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 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.util.Optional; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link MosApiRequestModule}. */ +public class MosApiRequestModuleTest { + + @Test + void testProvideTld_paramPresent() { + HttpServletRequest req = mock(HttpServletRequest.class); + when(req.getParameter("tld")).thenReturn("example.tld"); + + Optional result = MosApiRequestModule.provideTld(req); + + assertThat(result).hasValue("example.tld"); + } + + @Test + void testProvideTld_paramMissing() { + HttpServletRequest req = mock(HttpServletRequest.class); + when(req.getParameter("tld")).thenReturn(null); + + Optional result = MosApiRequestModule.provideTld(req); + + assertThat(result).isEmpty(); + } + + @Test + void testProvideTld_paramEmptyString() { + HttpServletRequest req = mock(HttpServletRequest.class); + when(req.getParameter("tld")).thenReturn(""); + + Optional result = MosApiRequestModule.provideTld(req); + + assertThat(result).isEmpty(); + } +} diff --git a/core/src/test/resources/google/registry/module/routing.txt b/core/src/test/resources/google/registry/module/routing.txt index 10048de4b26..db704bf75a9 100644 --- a/core/src/test/resources/google/registry/module/routing.txt +++ b/core/src/test/resources/google/registry/module/routing.txt @@ -13,6 +13,7 @@ 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/getServiceState GetServiceStateAction 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 From 198e83245b8b9b3e990f12791bc366dd811b31b0 Mon Sep 17 00:00:00 2001 From: njshah301 Date: Mon, 22 Dec 2025 10:31:37 +0000 Subject: [PATCH 2/5] Refactor MoSAPI models to records and address review nits - Convert model classes to Java records for conciseness and immutability. - Update unit tests to use Java text blocks for improved JSON readability. - Simplify service and action layers by removing redundant logic and logging. - Fix configuration nits regarding primitive types and comment formatting. --- .../registry/config/RegistryConfig.java | 2 +- .../config/RegistryConfigSettings.java | 2 +- .../registry/config/files/default-config.yaml | 2 +- .../mosapi/GetServiceStateAction.java | 4 - .../registry/mosapi/MosApiStateService.java | 15 ++-- .../mosapi/ServiceMonitoringClient.java | 1 + .../model/AllServicesStateResponse.java | 17 ++-- .../mosapi/model/IncidentSummary.java | 57 ++----------- .../mosapi/model/ServiceStateSummary.java | 42 ++-------- .../registry/mosapi/model/ServiceStatus.java | 54 ++++-------- .../mosapi/model/TldServiceState.java | 51 +++--------- .../mosapi/GetServiceStateActionTest.java | 1 + .../mosapi/MosApiStateServiceTest.java | 38 ++++----- .../model/AllServicesStateResponseTest.java | 48 ++++++++--- .../mosapi/model/IncidentSummaryTest.java | 74 +++++++++++------ .../mosapi/model/ServiceStateSummaryTest.java | 82 +++++++++++++------ .../mosapi/model/ServiceStatusTest.java | 70 ++++++++++------ .../mosapi/model/TldServiceStateTest.java | 71 ++++++++++------ 18 files changed, 309 insertions(+), 322 deletions(-) diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 14ba4c82aac..7d1da470e05 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -1464,7 +1464,7 @@ public static ImmutableSet provideMosapiServices(RegistryConfigSettings @Provides @Config("mosapiTldThreadCnt") - public static Integer provideMosapiTldThreads(RegistryConfigSettings config) { + public static int provideMosapiTldThreads(RegistryConfigSettings config) { return config.mosapi.tldThreadCnt; } diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index 820510f9e25..7cdb42ee45b 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -272,6 +272,6 @@ public static class MosApi { public String entityType; public List tlds; public List services; - public Integer tldThreadCnt; + public int tldThreadCnt; } } 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 fd1938b2a3a..99f532df1cd 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 @@ -642,7 +642,7 @@ mosapi: - "epp" - "dnssec" - #Provides a fixed thread pool for parallel TLD processing. + # Provides a fixed thread pool for parallel TLD processing. # @see # ICANN MoSAPI Specification, Section 12.3 tldThreadCnt: 4 diff --git a/core/src/main/java/google/registry/mosapi/GetServiceStateAction.java b/core/src/main/java/google/registry/mosapi/GetServiceStateAction.java index 3bb37818deb..a72b8977b29 100644 --- a/core/src/main/java/google/registry/mosapi/GetServiceStateAction.java +++ b/core/src/main/java/google/registry/mosapi/GetServiceStateAction.java @@ -14,7 +14,6 @@ package google.registry.mosapi; -import com.google.common.flogger.FluentLogger; import com.google.common.net.MediaType; import com.google.gson.Gson; import google.registry.request.Action; @@ -32,7 +31,6 @@ 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"; @@ -64,8 +62,6 @@ public void run() { 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/MosApiStateService.java b/core/src/main/java/google/registry/mosapi/MosApiStateService.java index 2f24c9c3194..74a73f82487 100644 --- a/core/src/main/java/google/registry/mosapi/MosApiStateService.java +++ b/core/src/main/java/google/registry/mosapi/MosApiStateService.java @@ -32,6 +32,7 @@ /** 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 ExecutorService tldExecutor; @@ -91,24 +92,24 @@ public AllServicesStateResponse getAllServiceStateSummaries() { private ServiceStateSummary transformToSummary(TldServiceState rawState) { List activeIncidents = null; - if (DOWN_STATUS.equalsIgnoreCase(rawState.getStatus())) { + if (DOWN_STATUS.equalsIgnoreCase(rawState.status())) { activeIncidents = - rawState.getServiceStatuses().entrySet().stream() + rawState.serviceStatuses().entrySet().stream() .filter( entry -> { ServiceStatus serviceStatus = entry.getValue(); - return serviceStatus.getIncidents() != null - && !serviceStatus.getIncidents().isEmpty(); + return serviceStatus.incidents() != null + && !serviceStatus.incidents().isEmpty(); }) .map( entry -> new ServiceStatus( // key is the service name entry.getKey(), - entry.getValue().getEmergencyThreshold(), - entry.getValue().getIncidents())) + entry.getValue().emergencyThreshold(), + entry.getValue().incidents())) .collect(toImmutableList()); } - return new ServiceStateSummary(rawState.getTld(), rawState.getStatus(), activeIncidents); + return new ServiceStateSummary(rawState.tld(), rawState.status(), activeIncidents); } } diff --git a/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java b/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java index 5991fc2fc76..c29dc3da669 100644 --- a/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java +++ b/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java @@ -25,6 +25,7 @@ /** Facade for MoSAPI's service monitoring endpoints. */ public class ServiceMonitoringClient { + private final MosApiClient mosApiClient; private final Gson gson; diff --git a/core/src/main/java/google/registry/mosapi/model/AllServicesStateResponse.java b/core/src/main/java/google/registry/mosapi/model/AllServicesStateResponse.java index 2c223c4e694..f1a4323fd29 100644 --- a/core/src/main/java/google/registry/mosapi/model/AllServicesStateResponse.java +++ b/core/src/main/java/google/registry/mosapi/model/AllServicesStateResponse.java @@ -26,18 +26,11 @@ * @see ICANN MoSAPI Specification, Section * 5.1 */ -public final class AllServicesStateResponse { +public record AllServicesStateResponse( + // A list of state summaries for each monitored service (e.g. DNS, RDDS, etc.) + @Expose @SerializedName("serviceStates") List serviceStates) { - // 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; + public AllServicesStateResponse { + serviceStates = (serviceStates == null) ? List.of() : List.copyOf(serviceStates); } } diff --git a/core/src/main/java/google/registry/mosapi/model/IncidentSummary.java b/core/src/main/java/google/registry/mosapi/model/IncidentSummary.java index 03993d4077a..c33c68d0307 100644 --- a/core/src/main/java/google/registry/mosapi/model/IncidentSummary.java +++ b/core/src/main/java/google/registry/mosapi/model/IncidentSummary.java @@ -24,54 +24,9 @@ * @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; - } -} +public record IncidentSummary( + @Expose @SerializedName("incidentID") String incidentID, + @Expose @SerializedName("startTime") long startTime, + @Expose @SerializedName("falsePositive") boolean falsePositive, + @Expose @SerializedName("state") String state, + @Expose @SerializedName("endTime") @Nullable Long endTime) {} diff --git a/core/src/main/java/google/registry/mosapi/model/ServiceStateSummary.java b/core/src/main/java/google/registry/mosapi/model/ServiceStateSummary.java index be690185b1f..37251a3cc28 100644 --- a/core/src/main/java/google/registry/mosapi/model/ServiceStateSummary.java +++ b/core/src/main/java/google/registry/mosapi/model/ServiceStateSummary.java @@ -28,42 +28,12 @@ * @see ICANN MoSAPI Specification, Section * 5.1 */ -public final class ServiceStateSummary { - @Expose - @SerializedName("tld") - private final String tld; +public record ServiceStateSummary( + @Expose @SerializedName("tld") String tld, + @Expose @SerializedName("overallStatus") String overallStatus, + @Expose @SerializedName("activeIncidents") @Nullable List activeIncidents) { - // 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; + public ServiceStateSummary { + activeIncidents = activeIncidents == null ? null : List.copyOf(activeIncidents); } } diff --git a/core/src/main/java/google/registry/mosapi/model/ServiceStatus.java b/core/src/main/java/google/registry/mosapi/model/ServiceStatus.java index 389ec10abcc..e9fa47a2572 100644 --- a/core/src/main/java/google/registry/mosapi/model/ServiceStatus.java +++ b/core/src/main/java/google/registry/mosapi/model/ServiceStatus.java @@ -19,44 +19,20 @@ 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 - */ - @Expose - @SerializedName("status") - 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. - @Expose - @SerializedName("emergencyThreshold") - private double emergencyThreshold; - - @Expose - @SerializedName("incidents") - 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; +public record 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. + */ + @Expose @SerializedName("status") 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. + @Expose @SerializedName("emergencyThreshold") double emergencyThreshold, + @Expose @SerializedName("incidents") List incidents) { + + public ServiceStatus { + incidents = incidents == null ? List.of() : List.copyOf(incidents); } } diff --git a/core/src/main/java/google/registry/mosapi/model/TldServiceState.java b/core/src/main/java/google/registry/mosapi/model/TldServiceState.java index 4cb2501976a..7422a2a0065 100644 --- a/core/src/main/java/google/registry/mosapi/model/TldServiceState.java +++ b/core/src/main/java/google/registry/mosapi/model/TldServiceState.java @@ -24,48 +24,19 @@ * @see ICANN MoSAPI Specification, Section * 5.1 */ -public final class TldServiceState { - @Expose - @SerializedName("tld") - private final String tld; +public record TldServiceState( + @Expose @SerializedName("tld") String tld, + long lastUpdateApiDatabase, - private final long lastUpdateApiDatabase; + // A JSON string that contains the status of the TLD as seen from the monitoring system + @Expose @SerializedName("status") String status, - // A JSON string that contains the status of the TLD as seen from the monitoring system - @Expose - @SerializedName("status") - 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") Map serviceStatuses) { - // 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; + public TldServiceState { + serviceStatuses = (serviceStatuses == null) ? Map.of() : Map.copyOf(serviceStatuses); } } diff --git a/core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java b/core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java index 4acccd17374..5e7f192e6ab 100644 --- a/core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java +++ b/core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java @@ -36,6 +36,7 @@ /** 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(); diff --git a/core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java b/core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java index 95abb918278..d3ebfa36247 100644 --- a/core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java +++ b/core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java @@ -56,9 +56,9 @@ void testGetServiceStateSummary_upStatus_returnsEmptyIncidents() throws Exceptio ServiceStateSummary result = service.getServiceStateSummary("tld1"); - assertThat(result.getTld()).isEqualTo("tld1"); - assertThat(result.getOverallStatus()).isEqualTo("Up"); - assertThat(result.getActiveIncidents()).isNull(); + assertThat(result.tld()).isEqualTo("tld1"); + assertThat(result.overallStatus()).isEqualTo("Up"); + assertThat(result.activeIncidents()).isNull(); } @Test @@ -76,12 +76,12 @@ void testGetServiceStateSummary_downStatus_filtersActiveIncidents() throws Excep ServiceStateSummary result = service.getServiceStateSummary("tld1"); - assertThat(result.getOverallStatus()).isEqualTo("Down"); - assertThat(result.getActiveIncidents()).hasSize(1); + assertThat(result.overallStatus()).isEqualTo("Down"); + assertThat(result.activeIncidents()).hasSize(1); - ServiceStatus incidentSummary = result.getActiveIncidents().get(0); - assertThat(incidentSummary.getStatus()).isEqualTo("DNS"); - assertThat(incidentSummary.getIncidents()).containsExactly(dnsIncident); + ServiceStatus incidentSummary = result.activeIncidents().get(0); + assertThat(incidentSummary.status()).isEqualTo("DNS"); + assertThat(incidentSummary.incidents()).containsExactly(dnsIncident); } @Test @@ -101,8 +101,8 @@ void testGetAllServiceStateSummaries_success() throws Exception { AllServicesStateResponse response = service.getAllServiceStateSummaries(); - assertThat(response.getServiceStates()).hasSize(2); - assertThat(response.getServiceStates().stream().map(ServiceStateSummary::getTld)) + assertThat(response.serviceStates()).hasSize(2); + assertThat(response.serviceStates().stream().map(ServiceStateSummary::tld)) .containsExactly("tld1", "tld2"); } @@ -115,22 +115,16 @@ void testGetAllServiceStateSummaries_partialFailure_returnsErrorState() throws E AllServicesStateResponse response = service.getAllServiceStateSummaries(); - assertThat(response.getServiceStates()).hasSize(2); + assertThat(response.serviceStates()).hasSize(2); ServiceStateSummary summary1 = - response.getServiceStates().stream() - .filter(s -> s.getTld().equals("tld1")) - .findFirst() - .get(); - assertThat(summary1.getOverallStatus()).isEqualTo("Up"); + response.serviceStates().stream().filter(s -> s.tld().equals("tld1")).findFirst().get(); + assertThat(summary1.overallStatus()).isEqualTo("Up"); ServiceStateSummary summary2 = - response.getServiceStates().stream() - .filter(s -> s.getTld().equals("tld2")) - .findFirst() - .get(); + response.serviceStates().stream().filter(s -> s.tld().equals("tld2")).findFirst().get(); - assertThat(summary2.getOverallStatus()).isEqualTo("ERROR"); - assertThat(summary2.getActiveIncidents()).isNull(); + assertThat(summary2.overallStatus()).isEqualTo("ERROR"); + assertThat(summary2.activeIncidents()).isNull(); } } diff --git a/core/src/test/java/google/registry/mosapi/model/AllServicesStateResponseTest.java b/core/src/test/java/google/registry/mosapi/model/AllServicesStateResponseTest.java index eb2241fc309..a7ea61ab0f0 100644 --- a/core/src/test/java/google/registry/mosapi/model/AllServicesStateResponseTest.java +++ b/core/src/test/java/google/registry/mosapi/model/AllServicesStateResponseTest.java @@ -24,6 +24,7 @@ /** Unit tests for {@link AllServicesStateResponse}. */ public class AllServicesStateResponseTest { + private final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); @Test @@ -33,7 +34,7 @@ void testConstructorAndGetter() { AllServicesStateResponse response = new AllServicesStateResponse(summaries); - assertThat(response.getServiceStates()).containsExactly(summary); + assertThat(response.serviceStates()).containsExactly(summary); } @Test @@ -44,22 +45,45 @@ void testJsonSerialization_setsCorrectFieldName() { String json = gson.toJson(response); // Verify the JSON structure contains the specific key - assertThat(json).contains("\"serviceStates\":"); - assertThat(json).contains("\"tld\":\"test.tld\""); - assertThat(json).contains("\"overallStatus\":\"Down\""); + assertThat(json) + .contains( + """ + "serviceStates":\ + """); + assertThat(json) + .contains( + """ + "tld":"test.tld"\ + """ + .trim()); + assertThat(json) + .contains( + """ + "overallStatus":"Down"\ + """ + .trim()); } @Test void testJsonDeserialization_readsCorrectFieldName() { String json = - "{\"serviceStates\": [{\"tld\": \"example.tld\", " - + "\"overallStatus\": \"Up\", \"activeIncidents\": []}]}"; + """ + { + "serviceStates": [ + { + "tld": "example.tld", + "overallStatus": "Up", + "activeIncidents": [] + } + ] + } + """; AllServicesStateResponse response = gson.fromJson(json, AllServicesStateResponse.class); - assertThat(response.getServiceStates()).hasSize(1); - assertThat(response.getServiceStates().get(0).getTld()).isEqualTo("example.tld"); - assertThat(response.getServiceStates().get(0).getOverallStatus()).isEqualTo("Up"); + assertThat(response.serviceStates()).hasSize(1); + assertThat(response.serviceStates().get(0).tld()).isEqualTo("example.tld"); + assertThat(response.serviceStates().get(0).overallStatus()).isEqualTo("Up"); } @Test @@ -68,6 +92,10 @@ void testJsonSerialization_handlesEmptyList() { String json = gson.toJson(response); - assertThat(json).isEqualTo("{\"serviceStates\":[]}"); + assertThat(json) + .isEqualTo( + """ + {"serviceStates":[]}\ + """); } } diff --git a/core/src/test/java/google/registry/mosapi/model/IncidentSummaryTest.java b/core/src/test/java/google/registry/mosapi/model/IncidentSummaryTest.java index 81064a5c131..3238958c9c0 100644 --- a/core/src/test/java/google/registry/mosapi/model/IncidentSummaryTest.java +++ b/core/src/test/java/google/registry/mosapi/model/IncidentSummaryTest.java @@ -31,11 +31,11 @@ void testConstructorAndGetters_allFieldsPopulated() { IncidentSummary incident = new IncidentSummary("INC-001", 1672531200000L, false, "Open", 1672617600000L); - assertThat(incident.getIncidentID()).isEqualTo("INC-001"); - assertThat(incident.getStartTime()).isEqualTo(1672531200000L); - assertThat(incident.isFalsePositive()).isFalse(); - assertThat(incident.getState()).isEqualTo("Open"); - assertThat(incident.getEndTime()).isEqualTo(1672617600000L); + assertThat(incident.incidentID()).isEqualTo("INC-001"); + assertThat(incident.startTime()).isEqualTo(1672531200000L); + assertThat(incident.falsePositive()).isFalse(); + assertThat(incident.state()).isEqualTo("Open"); + assertThat(incident.endTime()).isEqualTo(1672617600000L); } @Test @@ -43,9 +43,9 @@ void testConstructorAndGetters_nullEndTime() { // Tests that endTime can be null (e.g. for an ongoing incident) IncidentSummary incident = new IncidentSummary("INC-002", 1672531200000L, true, "Closed", null); - assertThat(incident.getIncidentID()).isEqualTo("INC-002"); - assertThat(incident.isFalsePositive()).isTrue(); - assertThat(incident.getEndTime()).isNull(); + assertThat(incident.incidentID()).isEqualTo("INC-002"); + assertThat(incident.falsePositive()).isTrue(); + assertThat(incident.endTime()).isNull(); } @Test @@ -56,30 +56,54 @@ void testJsonSerialization() { String json = gson.toJson(incident); // Verify fields are present and correctly named via @SerializedName - assertThat(json).contains("\"incidentID\":\"INC-001\""); - assertThat(json).contains("\"startTime\":1234567890000"); - assertThat(json).contains("\"falsePositive\":false"); - assertThat(json).contains("\"state\":\"Active\""); - assertThat(json).contains("\"endTime\":1234569990000"); + assertThat(json) + .contains( + """ + "incidentID":"INC-001"\ + """ + .trim()); + assertThat(json) + .contains( + """ + "startTime":1234567890000\ + """); + assertThat(json) + .contains( + """ + "falsePositive":false\ + """); + assertThat(json) + .contains( + """ + "state":"Active"\ + """ + .trim()); + assertThat(json) + .contains( + """ + "endTime":1234569990000\ + """); } @Test void testJsonDeserialization() { String json = - "{" - + "\"incidentID\": \"INC-999\"," - + "\"startTime\": 1000000," - + "\"falsePositive\": true," - + "\"state\": \"Resolved\"," - + "\"endTime\": 2000000" - + "}"; + """ + { + "incidentID": "INC-999", + "startTime": 1000000, + "falsePositive": true, + "state": "Resolved", + "endTime": 2000000 + } + """; IncidentSummary incident = gson.fromJson(json, IncidentSummary.class); - assertThat(incident.getIncidentID()).isEqualTo("INC-999"); - assertThat(incident.getStartTime()).isEqualTo(1000000L); - assertThat(incident.isFalsePositive()).isTrue(); - assertThat(incident.getState()).isEqualTo("Resolved"); - assertThat(incident.getEndTime()).isEqualTo(2000000L); + assertThat(incident.incidentID()).isEqualTo("INC-999"); + assertThat(incident.startTime()).isEqualTo(1000000L); + assertThat(incident.falsePositive()).isTrue(); + assertThat(incident.state()).isEqualTo("Resolved"); + assertThat(incident.endTime()).isEqualTo(2000000L); } } diff --git a/core/src/test/java/google/registry/mosapi/model/ServiceStateSummaryTest.java b/core/src/test/java/google/registry/mosapi/model/ServiceStateSummaryTest.java index fa1aa93794f..fae6a1b83ae 100644 --- a/core/src/test/java/google/registry/mosapi/model/ServiceStateSummaryTest.java +++ b/core/src/test/java/google/registry/mosapi/model/ServiceStateSummaryTest.java @@ -33,18 +33,18 @@ void testConstructorAndGetters_withIncidents() { ServiceStateSummary summary = new ServiceStateSummary("example.tld", "Down", ImmutableList.of(status)); - assertThat(summary.getTld()).isEqualTo("example.tld"); - assertThat(summary.getOverallStatus()).isEqualTo("Down"); - assertThat(summary.getActiveIncidents()).containsExactly(status); + assertThat(summary.tld()).isEqualTo("example.tld"); + assertThat(summary.overallStatus()).isEqualTo("Down"); + assertThat(summary.activeIncidents()).containsExactly(status); } @Test void testConstructorAndGetters_nullIncidents() { ServiceStateSummary summary = new ServiceStateSummary("example.tld", "Up", null); - assertThat(summary.getTld()).isEqualTo("example.tld"); - assertThat(summary.getOverallStatus()).isEqualTo("Up"); - assertThat(summary.getActiveIncidents()).isNull(); + assertThat(summary.tld()).isEqualTo("example.tld"); + assertThat(summary.overallStatus()).isEqualTo("Up"); + assertThat(summary.activeIncidents()).isNull(); } @Test @@ -55,9 +55,24 @@ void testJsonSerialization_includesAllFields() { String json = gson.toJson(summary); - assertThat(json).contains("\"tld\":\"test.tld\""); - assertThat(json).contains("\"overallStatus\":\"Down\""); - assertThat(json).contains("\"activeIncidents\":"); + assertThat(json) + .contains( + """ + "tld":"test.tld" + """ + .trim()); + assertThat(json) + .contains( + """ + "overallStatus":"Down" + """ + .trim()); + assertThat(json) + .contains( + """ + "activeIncidents" + """ + .trim()); } @Test @@ -67,27 +82,48 @@ void testJsonSerialization_excludesNullIncidents_ifNotConfiguredToSerializeNulls String json = gson.toJson(summary); - assertThat(json).contains("\"tld\":\"test.tld\""); - assertThat(json).contains("\"overallStatus\":\"Up\""); - assertThat(json).doesNotContain("\"activeIncidents\""); + assertThat(json) + .contains( + """ + "tld":"test.tld" + """ + .trim()); + assertThat(json) + .contains( + """ + "overallStatus":"Up" + """ + .trim()); + assertThat(json) + .doesNotContain( + """ + "activeIncidents" + """ + .trim()); } @Test void testJsonDeserialization() { String json = - "{" - + "\"tld\": \"example.tld\"," - + "\"overallStatus\": \"Down\"," - + "\"activeIncidents\": [" - + " {\"status\": \"Down\", \"emergencyThreshold\": 100.0, \"incidents\": []}" - + "]" - + "}"; + """ + { + "tld": "example.tld", + "overallStatus": "Down", + "activeIncidents": [ + { + "status": "Down", + "emergencyThreshold": 100.0, + "incidents": [] + } + ] + } + """; ServiceStateSummary summary = gson.fromJson(json, ServiceStateSummary.class); - assertThat(summary.getTld()).isEqualTo("example.tld"); - assertThat(summary.getOverallStatus()).isEqualTo("Down"); - assertThat(summary.getActiveIncidents()).hasSize(1); - assertThat(summary.getActiveIncidents().get(0).getStatus()).isEqualTo("Down"); + assertThat(summary.tld()).isEqualTo("example.tld"); + assertThat(summary.overallStatus()).isEqualTo("Down"); + assertThat(summary.activeIncidents()).hasSize(1); + assertThat(summary.activeIncidents().get(0).status()).isEqualTo("Down"); } } diff --git a/core/src/test/java/google/registry/mosapi/model/ServiceStatusTest.java b/core/src/test/java/google/registry/mosapi/model/ServiceStatusTest.java index 6175010576b..bc999946291 100644 --- a/core/src/test/java/google/registry/mosapi/model/ServiceStatusTest.java +++ b/core/src/test/java/google/registry/mosapi/model/ServiceStatusTest.java @@ -31,9 +31,9 @@ public class ServiceStatusTest { void testConstructorAndGetters_emptyIncidents() { ServiceStatus serviceStatus = new ServiceStatus("Up", 0.0, Collections.emptyList()); - assertThat(serviceStatus.getStatus()).isEqualTo("Up"); - assertThat(serviceStatus.getEmergencyThreshold()).isEqualTo(0.0); - assertThat(serviceStatus.getIncidents()).isEmpty(); + assertThat(serviceStatus.status()).isEqualTo("Up"); + assertThat(serviceStatus.emergencyThreshold()).isEqualTo(0.0); + assertThat(serviceStatus.incidents()).isEmpty(); } @Test @@ -41,9 +41,9 @@ void testConstructorAndGetters_withIncidents() { IncidentSummary incident = new IncidentSummary("I1", 100L, false, "Open", null); ServiceStatus serviceStatus = new ServiceStatus("Down", 50.5, ImmutableList.of(incident)); - assertThat(serviceStatus.getStatus()).isEqualTo("Down"); - assertThat(serviceStatus.getEmergencyThreshold()).isEqualTo(50.5); - assertThat(serviceStatus.getIncidents()).containsExactly(incident); + assertThat(serviceStatus.status()).isEqualTo("Down"); + assertThat(serviceStatus.emergencyThreshold()).isEqualTo(50.5); + assertThat(serviceStatus.incidents()).containsExactly(incident); } @Test @@ -53,31 +53,53 @@ void testJsonSerialization() { String json = gson.toJson(serviceStatus); - assertThat(json).contains("\"status\":\"Down\""); - assertThat(json).contains("\"emergencyThreshold\":99.9"); - assertThat(json).contains("\"incidents\":"); - assertThat(json).contains("\"incidentID\":\"I1\""); + assertThat(json) + .contains( + """ + "status":"Down"\ + """ + .trim()); + assertThat(json) + .contains( + """ + "emergencyThreshold":99.9\ + """); + assertThat(json) + .contains( + """ + "incidents":\ + """); + assertThat(json) + .contains( + """ + "incidentID":"I1"\ + """ + .trim()); } @Test void testJsonDeserialization() { String json = - "{" - + "\"status\": \"Disabled\"," - + "\"emergencyThreshold\": 10.0," - + "\"incidents\": [{" - + " \"incidentID\": \"I2\"," - + " \"startTime\": 200," - + " \"falsePositive\": true," - + " \"state\": \"Closed\"" - + "}]" - + "}"; + """ + { + "status": "Disabled", + "emergencyThreshold": 10.0, + "incidents": [ + { + "incidentID": "I2", + "startTime": 200, + "falsePositive": true, + "state": "Closed" + } + ] + } + """; ServiceStatus serviceStatus = gson.fromJson(json, ServiceStatus.class); - assertThat(serviceStatus.getStatus()).isEqualTo("Disabled"); - assertThat(serviceStatus.getEmergencyThreshold()).isEqualTo(10.0); - assertThat(serviceStatus.getIncidents()).hasSize(1); - assertThat(serviceStatus.getIncidents().get(0).getIncidentID()).isEqualTo("I2"); + assertThat(serviceStatus.status()).isEqualTo("Disabled"); + assertThat(serviceStatus.emergencyThreshold()).isEqualTo(10.0); + assertThat(serviceStatus.incidents()).hasSize(1); + assertThat(serviceStatus.incidents().get(0).incidentID()).isEqualTo("I2"); } } diff --git a/core/src/test/java/google/registry/mosapi/model/TldServiceStateTest.java b/core/src/test/java/google/registry/mosapi/model/TldServiceStateTest.java index dcd8eebf5bf..9a2f708ec2c 100644 --- a/core/src/test/java/google/registry/mosapi/model/TldServiceStateTest.java +++ b/core/src/test/java/google/registry/mosapi/model/TldServiceStateTest.java @@ -36,10 +36,10 @@ void testConstructorAndGetters_allFieldsPopulated() { TldServiceState state = new TldServiceState("example.tld", 123456L, "Up", services); - assertThat(state.getTld()).isEqualTo("example.tld"); - assertThat(state.getLastUpdateApiDatabase()).isEqualTo(123456L); - assertThat(state.getStatus()).isEqualTo("Up"); - assertThat(state.getServiceStatuses()).containsEntry("DNS", dnsStatus); + assertThat(state.tld()).isEqualTo("example.tld"); + assertThat(state.lastUpdateApiDatabase()).isEqualTo(123456L); + assertThat(state.status()).isEqualTo("Up"); + assertThat(state.serviceStatuses()).containsEntry("DNS", dnsStatus); } @Test @@ -52,10 +52,28 @@ void testJsonSerialization() { String json = gson.toJson(state); // Verify annotated fields are present - assertThat(json).contains("\"tld\":\"test.tld\""); - assertThat(json).contains("\"status\":\"Down\""); - assertThat(json).contains("\"testedServices\":"); - assertThat(json).contains("\"RDDS\":"); + assertThat(json) + .contains( + """ + "tld":"test.tld"\ + """ + .trim()); + assertThat(json) + .contains( + """ + "status":"Down"\ + """ + .trim()); + assertThat(json) + .contains( + """ + "testedServices":\ + """); + assertThat(json) + .contains( + """ + "RDDS":\ + """); // Verify unannotated field (lastUpdateApiDatabase) is EXCLUDED assertThat(json).doesNotContain("lastUpdateApiDatabase"); @@ -65,29 +83,30 @@ void testJsonSerialization() { @Test void testJsonDeserialization() { String json = - "{" - + "\"tld\": \"example.tld\"," - + "\"status\": \"Up\"," - // Note: lastUpdateApiDatabase is usually ignored if missing @Expose in strict mode - + "\"lastUpdateApiDatabase\": 55555," - + "\"testedServices\": {" - + " \"EPP\": {" - + " \"status\": \"Up\"," - + " \"emergencyThreshold\": 0.0," - + " \"incidents\": []" - + " }" - + "}" - + "}"; + """ + { + "tld": "example.tld", + "status": "Up", + "lastUpdateApiDatabase": 55555, + "testedServices": { + "EPP": { + "status": "Up", + "emergencyThreshold": 0.0, + "incidents": [] + } + } + } + """; TldServiceState state = gson.fromJson(json, TldServiceState.class); - assertThat(state.getTld()).isEqualTo("example.tld"); - assertThat(state.getStatus()).isEqualTo("Up"); + assertThat(state.tld()).isEqualTo("example.tld"); + assertThat(state.status()).isEqualTo("Up"); // Check map deserialization - assertThat(state.getServiceStatuses()).containsKey("EPP"); - assertThat(state.getServiceStatuses().get("EPP").getStatus()).isEqualTo("Up"); + assertThat(state.serviceStatuses()).containsKey("EPP"); + assertThat(state.serviceStatuses().get("EPP").status()).isEqualTo("Up"); - assertThat(state.getLastUpdateApiDatabase()).isEqualTo(0L); + assertThat(state.lastUpdateApiDatabase()).isEqualTo(0L); } } From e23e0adde625a2ae46873acd312205768946e655 Mon Sep 17 00:00:00 2001 From: njshah301 Date: Mon, 29 Dec 2025 11:05:40 +0000 Subject: [PATCH 3/5] Consolidate MoSAPI models and enhance null-safety - Moves model records into a single MosApiModels.java file. - Switches to ImmutableList/ImmutableMap with non-null defaults in constructors. - Removes redundant pass-through methods in MosApiStateService. - Updates tests to use Java Text Blocks and non-null collection assertions. --- .../{model => }/MosApiErrorResponse.java | 2 +- .../registry/mosapi/MosApiException.java | 1 - .../google/registry/mosapi/MosApiModels.java | 122 +++++++++++++ .../registry/mosapi/MosApiStateService.java | 19 +- .../mosapi/ServiceMonitoringClient.java | 3 +- .../model/AllServicesStateResponse.java | 36 ---- .../mosapi/model/IncidentSummary.java | 32 ---- .../mosapi/model/ServiceStateSummary.java | 39 ---- .../registry/mosapi/model/ServiceStatus.java | 38 ---- .../mosapi/model/TldServiceState.java | 42 ----- .../mosapi/GetServiceStateActionTest.java | 4 +- .../{model => }/MosApiErrorResponseTest.java | 2 +- .../registry/mosapi/MosApiExceptionTest.java | 1 - .../registry/mosapi/MosApiModelsTest.java | 172 ++++++++++++++++++ .../mosapi/MosApiStateServiceTest.java | 14 +- .../mosapi/ServiceMonitoringClientTest.java | 2 +- .../model/AllServicesStateResponseTest.java | 101 ---------- .../mosapi/model/IncidentSummaryTest.java | 109 ----------- .../mosapi/model/ServiceStateSummaryTest.java | 129 ------------- .../mosapi/model/ServiceStatusTest.java | 105 ----------- .../mosapi/model/TldServiceStateTest.java | 112 ------------ 21 files changed, 314 insertions(+), 771 deletions(-) rename core/src/main/java/google/registry/mosapi/{model => }/MosApiErrorResponse.java (96%) create mode 100644 core/src/main/java/google/registry/mosapi/MosApiModels.java delete mode 100644 core/src/main/java/google/registry/mosapi/model/AllServicesStateResponse.java delete mode 100644 core/src/main/java/google/registry/mosapi/model/IncidentSummary.java delete mode 100644 core/src/main/java/google/registry/mosapi/model/ServiceStateSummary.java delete mode 100644 core/src/main/java/google/registry/mosapi/model/ServiceStatus.java delete mode 100644 core/src/main/java/google/registry/mosapi/model/TldServiceState.java rename core/src/test/java/google/registry/mosapi/{model => }/MosApiErrorResponseTest.java (97%) create mode 100644 core/src/test/java/google/registry/mosapi/MosApiModelsTest.java delete mode 100644 core/src/test/java/google/registry/mosapi/model/AllServicesStateResponseTest.java delete mode 100644 core/src/test/java/google/registry/mosapi/model/IncidentSummaryTest.java delete mode 100644 core/src/test/java/google/registry/mosapi/model/ServiceStateSummaryTest.java delete mode 100644 core/src/test/java/google/registry/mosapi/model/ServiceStatusTest.java delete mode 100644 core/src/test/java/google/registry/mosapi/model/TldServiceStateTest.java diff --git a/core/src/main/java/google/registry/mosapi/model/MosApiErrorResponse.java b/core/src/main/java/google/registry/mosapi/MosApiErrorResponse.java similarity index 96% rename from core/src/main/java/google/registry/mosapi/model/MosApiErrorResponse.java rename to core/src/main/java/google/registry/mosapi/MosApiErrorResponse.java index afd41e69e56..a859e4b4312 100644 --- a/core/src/main/java/google/registry/mosapi/model/MosApiErrorResponse.java +++ b/core/src/main/java/google/registry/mosapi/MosApiErrorResponse.java @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package google.registry.mosapi.model; +package google.registry.mosapi; /** * Represents the generic JSON error response from the MoSAPI service for a 400 Bad Request. diff --git a/core/src/main/java/google/registry/mosapi/MosApiException.java b/core/src/main/java/google/registry/mosapi/MosApiException.java index e38e0236e2c..6cbda069f97 100644 --- a/core/src/main/java/google/registry/mosapi/MosApiException.java +++ b/core/src/main/java/google/registry/mosapi/MosApiException.java @@ -17,7 +17,6 @@ import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import google.registry.mosapi.model.MosApiErrorResponse; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/core/src/main/java/google/registry/mosapi/MosApiModels.java b/core/src/main/java/google/registry/mosapi/MosApiModels.java new file mode 100644 index 00000000000..ff867eb3db5 --- /dev/null +++ b/core/src/main/java/google/registry/mosapi/MosApiModels.java @@ -0,0 +1,122 @@ +// 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; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +/** Data models for ICANN MoSAPI. */ +public final class MosApiModels { + + private MosApiModels() {} + + /** + * 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 record AllServicesStateResponse( + // A list of state summaries for each monitored service (e.g. DNS, RDDS, etc.) + @Expose @SerializedName("serviceStates") List serviceStates) { + + public AllServicesStateResponse { + serviceStates = (serviceStates == null) ? ImmutableList.of() : serviceStates; + } + } + + /** + * A summary of a service incident. + * + * @see ICANN MoSAPI Specification, + * Section 5.1 + */ + public record IncidentSummary( + @Expose @SerializedName("incidentID") String incidentID, + @Expose @SerializedName("startTime") long startTime, + @Expose @SerializedName("falsePositive") boolean falsePositive, + @Expose @SerializedName("state") String state, + @Expose @SerializedName("endTime") @Nullable Long endTime) {} + + /** + * 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 record ServiceStateSummary( + @Expose @SerializedName("tld") String tld, + @Expose @SerializedName("overallStatus") String overallStatus, + @Expose @SerializedName("activeIncidents") List activeIncidents) { + + public ServiceStateSummary { + activeIncidents = activeIncidents == null ? ImmutableList.of() : activeIncidents; + } + } + + /** Represents the status of a single monitored service. */ + public record 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. + */ + @Expose @SerializedName("status") 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. + @Expose @SerializedName("emergencyThreshold") double emergencyThreshold, + @Expose @SerializedName("incidents") List incidents) { + + public ServiceStatus { + incidents = incidents == null ? ImmutableList.of() : incidents; + } + } + + /** + * Represents the overall health of all monitored services for a TLD. + * + * @see ICANN MoSAPI Specification, + * Section 5.1 + */ + public record TldServiceState( + @Expose @SerializedName("tld") String tld, + long lastUpdateApiDatabase, + + // A JSON string that contains the status of the TLD as seen from the monitoring system + @Expose @SerializedName("status") String status, + + // A JSON object containing detailed information for each potential monitored service (i.e., + // DNS, + // RDDS, EPP, DNSSEC, RDAP). + @Expose @SerializedName("testedServices") Map serviceStatuses) { + + public TldServiceState { + serviceStatuses = (serviceStatuses == null) ? ImmutableMap.of() : serviceStatuses; + } + } +} diff --git a/core/src/main/java/google/registry/mosapi/MosApiStateService.java b/core/src/main/java/google/registry/mosapi/MosApiStateService.java index 74a73f82487..51cf34f29ea 100644 --- a/core/src/main/java/google/registry/mosapi/MosApiStateService.java +++ b/core/src/main/java/google/registry/mosapi/MosApiStateService.java @@ -20,13 +20,12 @@ import com.google.common.collect.ImmutableSet; import com.google.common.flogger.FluentLogger; import google.registry.config.RegistryConfig.Config; -import google.registry.mosapi.model.AllServicesStateResponse; -import google.registry.mosapi.model.ServiceStateSummary; -import google.registry.mosapi.model.ServiceStatus; -import google.registry.mosapi.model.TldServiceState; +import google.registry.mosapi.MosApiModels.AllServicesStateResponse; +import google.registry.mosapi.MosApiModels.ServiceStateSummary; +import google.registry.mosapi.MosApiModels.ServiceStatus; +import google.registry.mosapi.MosApiModels.TldServiceState; import jakarta.inject.Inject; import jakarta.inject.Named; -import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; @@ -52,14 +51,10 @@ public MosApiStateService( this.tldExecutor = tldExecutor; } - /** Shared internal logic to fetch raw data from ICANN MoSAPI state monitoring. */ - private TldServiceState fetchRawState(String tld) throws MosApiException { - return serviceMonitoringClient.getTldServiceState(tld); - } - /** Fetches and transforms the service state for a given TLD into a summary. */ public ServiceStateSummary getServiceStateSummary(String tld) throws MosApiException { - return transformToSummary(fetchRawState(tld)); + TldServiceState rawState = serviceMonitoringClient.getTldServiceState(tld); + return transformToSummary(rawState); } /** Fetches and transforms the service state for all configured TLDs. */ @@ -91,7 +86,7 @@ public AllServicesStateResponse getAllServiceStateSummaries() { } private ServiceStateSummary transformToSummary(TldServiceState rawState) { - List activeIncidents = null; + ImmutableList activeIncidents = ImmutableList.of(); if (DOWN_STATUS.equalsIgnoreCase(rawState.status())) { activeIncidents = rawState.serviceStatuses().entrySet().stream() diff --git a/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java b/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java index c29dc3da669..6d97206ce25 100644 --- a/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java +++ b/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java @@ -17,8 +17,7 @@ import com.google.gson.Gson; import com.google.gson.JsonIOException; import com.google.gson.JsonSyntaxException; -import google.registry.mosapi.model.MosApiErrorResponse; -import google.registry.mosapi.model.TldServiceState; +import google.registry.mosapi.MosApiModels.TldServiceState; import jakarta.inject.Inject; import java.util.Collections; import okhttp3.Response; diff --git a/core/src/main/java/google/registry/mosapi/model/AllServicesStateResponse.java b/core/src/main/java/google/registry/mosapi/model/AllServicesStateResponse.java deleted file mode 100644 index f1a4323fd29..00000000000 --- a/core/src/main/java/google/registry/mosapi/model/AllServicesStateResponse.java +++ /dev/null @@ -1,36 +0,0 @@ -// 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.model; - -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 record AllServicesStateResponse( - // A list of state summaries for each monitored service (e.g. DNS, RDDS, etc.) - @Expose @SerializedName("serviceStates") List serviceStates) { - - public AllServicesStateResponse { - serviceStates = (serviceStates == null) ? List.of() : List.copyOf(serviceStates); - } -} diff --git a/core/src/main/java/google/registry/mosapi/model/IncidentSummary.java b/core/src/main/java/google/registry/mosapi/model/IncidentSummary.java deleted file mode 100644 index c33c68d0307..00000000000 --- a/core/src/main/java/google/registry/mosapi/model/IncidentSummary.java +++ /dev/null @@ -1,32 +0,0 @@ -// 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.model; - -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 record IncidentSummary( - @Expose @SerializedName("incidentID") String incidentID, - @Expose @SerializedName("startTime") long startTime, - @Expose @SerializedName("falsePositive") boolean falsePositive, - @Expose @SerializedName("state") String state, - @Expose @SerializedName("endTime") @Nullable Long endTime) {} diff --git a/core/src/main/java/google/registry/mosapi/model/ServiceStateSummary.java b/core/src/main/java/google/registry/mosapi/model/ServiceStateSummary.java deleted file mode 100644 index 37251a3cc28..00000000000 --- a/core/src/main/java/google/registry/mosapi/model/ServiceStateSummary.java +++ /dev/null @@ -1,39 +0,0 @@ -// 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.model; - -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 record ServiceStateSummary( - @Expose @SerializedName("tld") String tld, - @Expose @SerializedName("overallStatus") String overallStatus, - @Expose @SerializedName("activeIncidents") @Nullable List activeIncidents) { - - public ServiceStateSummary { - activeIncidents = activeIncidents == null ? null : List.copyOf(activeIncidents); - } -} diff --git a/core/src/main/java/google/registry/mosapi/model/ServiceStatus.java b/core/src/main/java/google/registry/mosapi/model/ServiceStatus.java deleted file mode 100644 index e9fa47a2572..00000000000 --- a/core/src/main/java/google/registry/mosapi/model/ServiceStatus.java +++ /dev/null @@ -1,38 +0,0 @@ -// 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.model; - -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; -import java.util.List; - -/** Represents the status of a single monitored service. */ -public record 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. - */ - @Expose @SerializedName("status") 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. - @Expose @SerializedName("emergencyThreshold") double emergencyThreshold, - @Expose @SerializedName("incidents") List incidents) { - - public ServiceStatus { - incidents = incidents == null ? List.of() : List.copyOf(incidents); - } -} diff --git a/core/src/main/java/google/registry/mosapi/model/TldServiceState.java b/core/src/main/java/google/registry/mosapi/model/TldServiceState.java deleted file mode 100644 index 7422a2a0065..00000000000 --- a/core/src/main/java/google/registry/mosapi/model/TldServiceState.java +++ /dev/null @@ -1,42 +0,0 @@ -// 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.model; - -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 record TldServiceState( - @Expose @SerializedName("tld") String tld, - long lastUpdateApiDatabase, - - // A JSON string that contains the status of the TLD as seen from the monitoring system - @Expose @SerializedName("status") String status, - - // A JSON object containing detailed information for each potential monitored service (i.e., - // DNS, - // RDDS, EPP, DNSSEC, RDAP). - @Expose @SerializedName("testedServices") Map serviceStatuses) { - - public TldServiceState { - serviceStatuses = (serviceStatuses == null) ? Map.of() : Map.copyOf(serviceStatuses); - } -} diff --git a/core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java b/core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java index 5e7f192e6ab..584fe97360f 100644 --- a/core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java +++ b/core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java @@ -23,8 +23,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.net.MediaType; import com.google.gson.Gson; -import google.registry.mosapi.model.AllServicesStateResponse; -import google.registry.mosapi.model.ServiceStateSummary; +import google.registry.mosapi.MosApiModels.AllServicesStateResponse; +import google.registry.mosapi.MosApiModels.ServiceStateSummary; import google.registry.request.HttpException.ServiceUnavailableException; import google.registry.testing.FakeResponse; import java.util.Optional; diff --git a/core/src/test/java/google/registry/mosapi/model/MosApiErrorResponseTest.java b/core/src/test/java/google/registry/mosapi/MosApiErrorResponseTest.java similarity index 97% rename from core/src/test/java/google/registry/mosapi/model/MosApiErrorResponseTest.java rename to core/src/test/java/google/registry/mosapi/MosApiErrorResponseTest.java index 1c0078ed0b3..e71fb5788c8 100644 --- a/core/src/test/java/google/registry/mosapi/model/MosApiErrorResponseTest.java +++ b/core/src/test/java/google/registry/mosapi/MosApiErrorResponseTest.java @@ -11,7 +11,7 @@ // 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.model; +package google.registry.mosapi; import static com.google.common.truth.Truth.assertThat; diff --git a/core/src/test/java/google/registry/mosapi/MosApiExceptionTest.java b/core/src/test/java/google/registry/mosapi/MosApiExceptionTest.java index 38fb9d0ed3f..39d82dbd58a 100644 --- a/core/src/test/java/google/registry/mosapi/MosApiExceptionTest.java +++ b/core/src/test/java/google/registry/mosapi/MosApiExceptionTest.java @@ -20,7 +20,6 @@ import google.registry.mosapi.MosApiException.EndDateSyntaxInvalidException; import google.registry.mosapi.MosApiException.MosApiAuthorizationException; import google.registry.mosapi.MosApiException.StartDateSyntaxInvalidException; -import google.registry.mosapi.model.MosApiErrorResponse; import org.junit.jupiter.api.Test; /** Unit tests for {@link MosApiException}. */ diff --git a/core/src/test/java/google/registry/mosapi/MosApiModelsTest.java b/core/src/test/java/google/registry/mosapi/MosApiModelsTest.java new file mode 100644 index 00000000000..6b10f338cd3 --- /dev/null +++ b/core/src/test/java/google/registry/mosapi/MosApiModelsTest.java @@ -0,0 +1,172 @@ +// 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; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import google.registry.mosapi.MosApiModels.AllServicesStateResponse; +import google.registry.mosapi.MosApiModels.IncidentSummary; +import google.registry.mosapi.MosApiModels.ServiceStateSummary; +import google.registry.mosapi.MosApiModels.ServiceStatus; +import google.registry.mosapi.MosApiModels.TldServiceState; +import org.junit.Test; + +/** Tests for {@link MosApiModels}. */ +public final class MosApiModelsTest { + + private static final Gson gson = + new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + + @Test + public void testAllServicesStateResponse_nullCollection_initializedToEmpty() { + AllServicesStateResponse response = new AllServicesStateResponse(null); + assertThat(response.serviceStates()).isEmpty(); + assertThat(response.serviceStates()).isNotNull(); + } + + @Test + public void testServiceStateSummary_nullCollection_initializedToEmpty() { + ServiceStateSummary summary = new ServiceStateSummary("example", "Up", null); + assertThat(summary.activeIncidents()).isEmpty(); + assertThat(summary.activeIncidents()).isNotNull(); + } + + @Test + public void testServiceStatus_nullCollection_initializedToEmpty() { + ServiceStatus status = new ServiceStatus("Up", 0.0, null); + assertThat(status.incidents()).isEmpty(); + assertThat(status.incidents()).isNotNull(); + } + + @Test + public void testTldServiceState_nullCollection_initializedToEmpty() { + TldServiceState state = new TldServiceState("example", 123456L, "Up", null); + assertThat(state.serviceStatuses()).isEmpty(); + assertThat(state.serviceStatuses()).isNotNull(); + } + + @Test + public void testIncidentSummary_jsonSerialization() { + IncidentSummary incident = new IncidentSummary("inc-123", 1000L, false, "Active", 2000L); + String json = gson.toJson(incident); + // Using Text Blocks to avoid escaping quotes + assertThat(json) + .contains( + """ + "incidentID":"inc-123" + """ + .trim()); + assertThat(json) + .contains( + """ + "startTime":1000 + """ + .trim()); + assertThat(json) + .contains( + """ + "falsePositive":false + """ + .trim()); + assertThat(json) + .contains( + """ + "state":"Active" + """ + .trim()); + assertThat(json) + .contains( + """ + "endTime":2000 + """ + .trim()); + } + + @Test + public void testServiceStatus_jsonSerialization() { + IncidentSummary incident = new IncidentSummary("inc-1", 1000L, false, "Resolved", null); + ServiceStatus status = new ServiceStatus("Down", 75.5, ImmutableList.of(incident)); + String json = gson.toJson(status); + assertThat(json) + .contains( + """ + "status":"Down" + """ + .trim()); + assertThat(json) + .contains( + """ + "emergencyThreshold":75.5 + """ + .trim()); + assertThat(json) + .contains( + """ + "incidents":[ + """ + .trim()); + } + + @Test + public void testTldServiceState_jsonSerialization() { + ServiceStatus dnsStatus = new ServiceStatus("Up", 0.0, ImmutableList.of()); + TldServiceState state = + new TldServiceState("app", 1700000000L, "Up", ImmutableMap.of("DNS", dnsStatus)); + + String json = gson.toJson(state); + assertThat(json) + .contains( + """ + "tld":"app" + """ + .trim()); + assertThat(json) + .contains( + """ + "status":"Up" + """ + .trim()); + assertThat(json) + .contains( + """ + "testedServices":{"DNS":{ + """ + .trim()); + } + + @Test + public void testAllServicesStateResponse_jsonSerialization() { + ServiceStateSummary summary = new ServiceStateSummary("dev", "Up", ImmutableList.of()); + AllServicesStateResponse response = new AllServicesStateResponse(ImmutableList.of(summary)); + + String json = gson.toJson(response); + assertThat(json) + .contains( + """ + "serviceStates":[ + """ + .trim()); + assertThat(json) + .contains( + """ + "tld":"dev" + """ + .trim()); + } +} diff --git a/core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java b/core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java index d3ebfa36247..e4108a74cb7 100644 --- a/core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java +++ b/core/src/test/java/google/registry/mosapi/MosApiStateServiceTest.java @@ -22,11 +22,11 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.MoreExecutors; -import google.registry.mosapi.model.AllServicesStateResponse; -import google.registry.mosapi.model.IncidentSummary; -import google.registry.mosapi.model.ServiceStateSummary; -import google.registry.mosapi.model.ServiceStatus; -import google.registry.mosapi.model.TldServiceState; +import google.registry.mosapi.MosApiModels.AllServicesStateResponse; +import google.registry.mosapi.MosApiModels.IncidentSummary; +import google.registry.mosapi.MosApiModels.ServiceStateSummary; +import google.registry.mosapi.MosApiModels.ServiceStatus; +import google.registry.mosapi.MosApiModels.TldServiceState; import java.util.concurrent.ExecutorService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -58,7 +58,7 @@ void testGetServiceStateSummary_upStatus_returnsEmptyIncidents() throws Exceptio assertThat(result.tld()).isEqualTo("tld1"); assertThat(result.overallStatus()).isEqualTo("Up"); - assertThat(result.activeIncidents()).isNull(); + assertThat(result.activeIncidents()).isEmpty(); } @Test @@ -125,6 +125,6 @@ void testGetAllServiceStateSummaries_partialFailure_returnsErrorState() throws E response.serviceStates().stream().filter(s -> s.tld().equals("tld2")).findFirst().get(); assertThat(summary2.overallStatus()).isEqualTo("ERROR"); - assertThat(summary2.activeIncidents()).isNull(); + assertThat(summary2.activeIncidents()).isEmpty(); } } diff --git a/core/src/test/java/google/registry/mosapi/ServiceMonitoringClientTest.java b/core/src/test/java/google/registry/mosapi/ServiceMonitoringClientTest.java index d749817e919..95fcfc3db30 100644 --- a/core/src/test/java/google/registry/mosapi/ServiceMonitoringClientTest.java +++ b/core/src/test/java/google/registry/mosapi/ServiceMonitoringClientTest.java @@ -27,7 +27,7 @@ import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; -import google.registry.mosapi.model.TldServiceState; +import google.registry.mosapi.MosApiModels.TldServiceState; import java.io.IOException; import java.io.Reader; import okhttp3.MediaType; diff --git a/core/src/test/java/google/registry/mosapi/model/AllServicesStateResponseTest.java b/core/src/test/java/google/registry/mosapi/model/AllServicesStateResponseTest.java deleted file mode 100644 index a7ea61ab0f0..00000000000 --- a/core/src/test/java/google/registry/mosapi/model/AllServicesStateResponseTest.java +++ /dev/null @@ -1,101 +0,0 @@ -// 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.model; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.common.collect.ImmutableList; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import java.util.Collections; -import org.junit.jupiter.api.Test; - -/** Unit tests for {@link AllServicesStateResponse}. */ -public class AllServicesStateResponseTest { - - private final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); - - @Test - void testConstructorAndGetter() { - ServiceStateSummary summary = new ServiceStateSummary("tld", "Up", Collections.emptyList()); - ImmutableList summaries = ImmutableList.of(summary); - - AllServicesStateResponse response = new AllServicesStateResponse(summaries); - - assertThat(response.serviceStates()).containsExactly(summary); - } - - @Test - void testJsonSerialization_setsCorrectFieldName() { - ServiceStateSummary summary = new ServiceStateSummary("test.tld", "Down", null); - AllServicesStateResponse response = new AllServicesStateResponse(ImmutableList.of(summary)); - - String json = gson.toJson(response); - - // Verify the JSON structure contains the specific key - assertThat(json) - .contains( - """ - "serviceStates":\ - """); - assertThat(json) - .contains( - """ - "tld":"test.tld"\ - """ - .trim()); - assertThat(json) - .contains( - """ - "overallStatus":"Down"\ - """ - .trim()); - } - - @Test - void testJsonDeserialization_readsCorrectFieldName() { - String json = - """ - { - "serviceStates": [ - { - "tld": "example.tld", - "overallStatus": "Up", - "activeIncidents": [] - } - ] - } - """; - - AllServicesStateResponse response = gson.fromJson(json, AllServicesStateResponse.class); - - assertThat(response.serviceStates()).hasSize(1); - assertThat(response.serviceStates().get(0).tld()).isEqualTo("example.tld"); - assertThat(response.serviceStates().get(0).overallStatus()).isEqualTo("Up"); - } - - @Test - void testJsonSerialization_handlesEmptyList() { - AllServicesStateResponse response = new AllServicesStateResponse(ImmutableList.of()); - - String json = gson.toJson(response); - - assertThat(json) - .isEqualTo( - """ - {"serviceStates":[]}\ - """); - } -} diff --git a/core/src/test/java/google/registry/mosapi/model/IncidentSummaryTest.java b/core/src/test/java/google/registry/mosapi/model/IncidentSummaryTest.java deleted file mode 100644 index 3238958c9c0..00000000000 --- a/core/src/test/java/google/registry/mosapi/model/IncidentSummaryTest.java +++ /dev/null @@ -1,109 +0,0 @@ -// 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.model; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.junit.jupiter.api.Test; - -/** Unit tests for {@link IncidentSummary}. */ -public class IncidentSummaryTest { - - // Use GsonBuilder to respect @Expose annotations if needed, though default new Gson() works too. - private final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); - - @Test - void testConstructorAndGetters_allFieldsPopulated() { - IncidentSummary incident = - new IncidentSummary("INC-001", 1672531200000L, false, "Open", 1672617600000L); - - assertThat(incident.incidentID()).isEqualTo("INC-001"); - assertThat(incident.startTime()).isEqualTo(1672531200000L); - assertThat(incident.falsePositive()).isFalse(); - assertThat(incident.state()).isEqualTo("Open"); - assertThat(incident.endTime()).isEqualTo(1672617600000L); - } - - @Test - void testConstructorAndGetters_nullEndTime() { - // Tests that endTime can be null (e.g. for an ongoing incident) - IncidentSummary incident = new IncidentSummary("INC-002", 1672531200000L, true, "Closed", null); - - assertThat(incident.incidentID()).isEqualTo("INC-002"); - assertThat(incident.falsePositive()).isTrue(); - assertThat(incident.endTime()).isNull(); - } - - @Test - void testJsonSerialization() { - IncidentSummary incident = - new IncidentSummary("INC-001", 1234567890000L, false, "Active", 1234569990000L); - - String json = gson.toJson(incident); - - // Verify fields are present and correctly named via @SerializedName - assertThat(json) - .contains( - """ - "incidentID":"INC-001"\ - """ - .trim()); - assertThat(json) - .contains( - """ - "startTime":1234567890000\ - """); - assertThat(json) - .contains( - """ - "falsePositive":false\ - """); - assertThat(json) - .contains( - """ - "state":"Active"\ - """ - .trim()); - assertThat(json) - .contains( - """ - "endTime":1234569990000\ - """); - } - - @Test - void testJsonDeserialization() { - String json = - """ - { - "incidentID": "INC-999", - "startTime": 1000000, - "falsePositive": true, - "state": "Resolved", - "endTime": 2000000 - } - """; - - IncidentSummary incident = gson.fromJson(json, IncidentSummary.class); - - assertThat(incident.incidentID()).isEqualTo("INC-999"); - assertThat(incident.startTime()).isEqualTo(1000000L); - assertThat(incident.falsePositive()).isTrue(); - assertThat(incident.state()).isEqualTo("Resolved"); - assertThat(incident.endTime()).isEqualTo(2000000L); - } -} diff --git a/core/src/test/java/google/registry/mosapi/model/ServiceStateSummaryTest.java b/core/src/test/java/google/registry/mosapi/model/ServiceStateSummaryTest.java deleted file mode 100644 index fae6a1b83ae..00000000000 --- a/core/src/test/java/google/registry/mosapi/model/ServiceStateSummaryTest.java +++ /dev/null @@ -1,129 +0,0 @@ -// 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.model; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.common.collect.ImmutableList; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import java.util.Collections; -import org.junit.jupiter.api.Test; - -/** Unit tests for {@link ServiceStateSummary}. */ -public class ServiceStateSummaryTest { - - private final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); - - @Test - void testConstructorAndGetters_withIncidents() { - ServiceStatus status = new ServiceStatus("Down", 100.0, Collections.emptyList()); - ServiceStateSummary summary = - new ServiceStateSummary("example.tld", "Down", ImmutableList.of(status)); - - assertThat(summary.tld()).isEqualTo("example.tld"); - assertThat(summary.overallStatus()).isEqualTo("Down"); - assertThat(summary.activeIncidents()).containsExactly(status); - } - - @Test - void testConstructorAndGetters_nullIncidents() { - ServiceStateSummary summary = new ServiceStateSummary("example.tld", "Up", null); - - assertThat(summary.tld()).isEqualTo("example.tld"); - assertThat(summary.overallStatus()).isEqualTo("Up"); - assertThat(summary.activeIncidents()).isNull(); - } - - @Test - void testJsonSerialization_includesAllFields() { - ServiceStatus status = new ServiceStatus("Down", 50.0, null); - ServiceStateSummary summary = - new ServiceStateSummary("test.tld", "Down", ImmutableList.of(status)); - - String json = gson.toJson(summary); - - assertThat(json) - .contains( - """ - "tld":"test.tld" - """ - .trim()); - assertThat(json) - .contains( - """ - "overallStatus":"Down" - """ - .trim()); - assertThat(json) - .contains( - """ - "activeIncidents" - """ - .trim()); - } - - @Test - void testJsonSerialization_excludesNullIncidents_ifNotConfiguredToSerializeNulls() { - - ServiceStateSummary summary = new ServiceStateSummary("test.tld", "Up", null); - - String json = gson.toJson(summary); - - assertThat(json) - .contains( - """ - "tld":"test.tld" - """ - .trim()); - assertThat(json) - .contains( - """ - "overallStatus":"Up" - """ - .trim()); - assertThat(json) - .doesNotContain( - """ - "activeIncidents" - """ - .trim()); - } - - @Test - void testJsonDeserialization() { - String json = - """ - { - "tld": "example.tld", - "overallStatus": "Down", - "activeIncidents": [ - { - "status": "Down", - "emergencyThreshold": 100.0, - "incidents": [] - } - ] - } - """; - - ServiceStateSummary summary = gson.fromJson(json, ServiceStateSummary.class); - - assertThat(summary.tld()).isEqualTo("example.tld"); - assertThat(summary.overallStatus()).isEqualTo("Down"); - assertThat(summary.activeIncidents()).hasSize(1); - assertThat(summary.activeIncidents().get(0).status()).isEqualTo("Down"); - } -} diff --git a/core/src/test/java/google/registry/mosapi/model/ServiceStatusTest.java b/core/src/test/java/google/registry/mosapi/model/ServiceStatusTest.java deleted file mode 100644 index bc999946291..00000000000 --- a/core/src/test/java/google/registry/mosapi/model/ServiceStatusTest.java +++ /dev/null @@ -1,105 +0,0 @@ -// 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.model; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.common.collect.ImmutableList; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import java.util.Collections; -import org.junit.jupiter.api.Test; - -/** Unit tests for {@link ServiceStatus}. */ -public class ServiceStatusTest { - - private final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); - - @Test - void testConstructorAndGetters_emptyIncidents() { - ServiceStatus serviceStatus = new ServiceStatus("Up", 0.0, Collections.emptyList()); - - assertThat(serviceStatus.status()).isEqualTo("Up"); - assertThat(serviceStatus.emergencyThreshold()).isEqualTo(0.0); - assertThat(serviceStatus.incidents()).isEmpty(); - } - - @Test - void testConstructorAndGetters_withIncidents() { - IncidentSummary incident = new IncidentSummary("I1", 100L, false, "Open", null); - ServiceStatus serviceStatus = new ServiceStatus("Down", 50.5, ImmutableList.of(incident)); - - assertThat(serviceStatus.status()).isEqualTo("Down"); - assertThat(serviceStatus.emergencyThreshold()).isEqualTo(50.5); - assertThat(serviceStatus.incidents()).containsExactly(incident); - } - - @Test - void testJsonSerialization() { - IncidentSummary incident = new IncidentSummary("I1", 100L, false, "Open", null); - ServiceStatus serviceStatus = new ServiceStatus("Down", 99.9, ImmutableList.of(incident)); - - String json = gson.toJson(serviceStatus); - - assertThat(json) - .contains( - """ - "status":"Down"\ - """ - .trim()); - assertThat(json) - .contains( - """ - "emergencyThreshold":99.9\ - """); - assertThat(json) - .contains( - """ - "incidents":\ - """); - assertThat(json) - .contains( - """ - "incidentID":"I1"\ - """ - .trim()); - } - - @Test - void testJsonDeserialization() { - String json = - """ - { - "status": "Disabled", - "emergencyThreshold": 10.0, - "incidents": [ - { - "incidentID": "I2", - "startTime": 200, - "falsePositive": true, - "state": "Closed" - } - ] - } - """; - - ServiceStatus serviceStatus = gson.fromJson(json, ServiceStatus.class); - - assertThat(serviceStatus.status()).isEqualTo("Disabled"); - assertThat(serviceStatus.emergencyThreshold()).isEqualTo(10.0); - assertThat(serviceStatus.incidents()).hasSize(1); - assertThat(serviceStatus.incidents().get(0).incidentID()).isEqualTo("I2"); - } -} diff --git a/core/src/test/java/google/registry/mosapi/model/TldServiceStateTest.java b/core/src/test/java/google/registry/mosapi/model/TldServiceStateTest.java deleted file mode 100644 index 9a2f708ec2c..00000000000 --- a/core/src/test/java/google/registry/mosapi/model/TldServiceStateTest.java +++ /dev/null @@ -1,112 +0,0 @@ -// 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.model; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import java.util.Collections; -import java.util.Map; -import org.junit.jupiter.api.Test; - -/** Unit tests for {@link TldServiceState}. */ -public class TldServiceStateTest { - - private final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); - - @Test - void testConstructorAndGetters_allFieldsPopulated() { - ServiceStatus dnsStatus = new ServiceStatus("Up", 0.0, Collections.emptyList()); - Map services = ImmutableMap.of("DNS", dnsStatus); - - TldServiceState state = new TldServiceState("example.tld", 123456L, "Up", services); - - assertThat(state.tld()).isEqualTo("example.tld"); - assertThat(state.lastUpdateApiDatabase()).isEqualTo(123456L); - assertThat(state.status()).isEqualTo("Up"); - assertThat(state.serviceStatuses()).containsEntry("DNS", dnsStatus); - } - - @Test - void testJsonSerialization() { - ServiceStatus rddsStatus = new ServiceStatus("Down", 100.0, ImmutableList.of()); - Map services = ImmutableMap.of("RDDS", rddsStatus); - - TldServiceState state = new TldServiceState("test.tld", 99999L, "Down", services); - - String json = gson.toJson(state); - - // Verify annotated fields are present - assertThat(json) - .contains( - """ - "tld":"test.tld"\ - """ - .trim()); - assertThat(json) - .contains( - """ - "status":"Down"\ - """ - .trim()); - assertThat(json) - .contains( - """ - "testedServices":\ - """); - assertThat(json) - .contains( - """ - "RDDS":\ - """); - - // Verify unannotated field (lastUpdateApiDatabase) is EXCLUDED - assertThat(json).doesNotContain("lastUpdateApiDatabase"); - assertThat(json).doesNotContain("99999"); - } - - @Test - void testJsonDeserialization() { - String json = - """ - { - "tld": "example.tld", - "status": "Up", - "lastUpdateApiDatabase": 55555, - "testedServices": { - "EPP": { - "status": "Up", - "emergencyThreshold": 0.0, - "incidents": [] - } - } - } - """; - - TldServiceState state = gson.fromJson(json, TldServiceState.class); - - assertThat(state.tld()).isEqualTo("example.tld"); - assertThat(state.status()).isEqualTo("Up"); - - // Check map deserialization - assertThat(state.serviceStatuses()).containsKey("EPP"); - assertThat(state.serviceStatuses().get("EPP").status()).isEqualTo("Up"); - - assertThat(state.lastUpdateApiDatabase()).isEqualTo(0L); - } -} From 9f2416344a1965b05b1d18d98255698a17da1e59 Mon Sep 17 00:00:00 2001 From: njshah301 Date: Tue, 30 Dec 2025 11:48:19 +0000 Subject: [PATCH 4/5] Improve MoSAPI client error handling and clean up data models Refactors the MoSAPI monitoring client to be more robust against infrastructure failures --- .../registry/mosapi/MosApiErrorResponse.java | 5 +- .../registry/mosapi/MosApiException.java | 5 + .../google/registry/mosapi/MosApiModels.java | 28 ++-- .../mosapi/ServiceMonitoringClient.java | 38 ++++- .../mosapi/GetServiceStateActionTest.java | 14 +- .../mosapi/ServiceMonitoringClientTest.java | 153 +++++++++--------- 6 files changed, 139 insertions(+), 104 deletions(-) diff --git a/core/src/main/java/google/registry/mosapi/MosApiErrorResponse.java b/core/src/main/java/google/registry/mosapi/MosApiErrorResponse.java index a859e4b4312..34181115da4 100644 --- a/core/src/main/java/google/registry/mosapi/MosApiErrorResponse.java +++ b/core/src/main/java/google/registry/mosapi/MosApiErrorResponse.java @@ -14,10 +14,13 @@ package google.registry.mosapi; +import com.google.gson.annotations.Expose; + /** * 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) {} +public record MosApiErrorResponse( + @Expose String resultCode, @Expose String message, @Expose String description) {} diff --git a/core/src/main/java/google/registry/mosapi/MosApiException.java b/core/src/main/java/google/registry/mosapi/MosApiException.java index 6cbda069f97..fd0f7810b3d 100644 --- a/core/src/main/java/google/registry/mosapi/MosApiException.java +++ b/core/src/main/java/google/registry/mosapi/MosApiException.java @@ -41,6 +41,11 @@ public MosApiException(String message, Throwable cause) { this.errorResponse = null; } + public MosApiException(String message) { + super(message); + this.errorResponse = null; + } + public Optional getErrorResponse() { return Optional.ofNullable(errorResponse); } diff --git a/core/src/main/java/google/registry/mosapi/MosApiModels.java b/core/src/main/java/google/registry/mosapi/MosApiModels.java index ff867eb3db5..e94516042bc 100644 --- a/core/src/main/java/google/registry/mosapi/MosApiModels.java +++ b/core/src/main/java/google/registry/mosapi/MosApiModels.java @@ -38,7 +38,7 @@ private MosApiModels() {} */ public record AllServicesStateResponse( // A list of state summaries for each monitored service (e.g. DNS, RDDS, etc.) - @Expose @SerializedName("serviceStates") List serviceStates) { + @Expose List serviceStates) { public AllServicesStateResponse { serviceStates = (serviceStates == null) ? ImmutableList.of() : serviceStates; @@ -52,11 +52,11 @@ public record AllServicesStateResponse( * Section 5.1 */ public record IncidentSummary( - @Expose @SerializedName("incidentID") String incidentID, - @Expose @SerializedName("startTime") long startTime, - @Expose @SerializedName("falsePositive") boolean falsePositive, - @Expose @SerializedName("state") String state, - @Expose @SerializedName("endTime") @Nullable Long endTime) {} + @Expose String incidentID, + @Expose long startTime, + @Expose boolean falsePositive, + @Expose String state, + @Expose @Nullable Long endTime) {} /** * A curated summary of the service state for a TLD. @@ -69,9 +69,9 @@ public record IncidentSummary( * Section 5.1 */ public record ServiceStateSummary( - @Expose @SerializedName("tld") String tld, - @Expose @SerializedName("overallStatus") String overallStatus, - @Expose @SerializedName("activeIncidents") List activeIncidents) { + @Expose String tld, + @Expose String overallStatus, + @Expose List activeIncidents) { public ServiceStateSummary { activeIncidents = activeIncidents == null ? ImmutableList.of() : activeIncidents; @@ -84,13 +84,13 @@ public record 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. */ - @Expose @SerializedName("status") String status, + @Expose 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. - @Expose @SerializedName("emergencyThreshold") double emergencyThreshold, - @Expose @SerializedName("incidents") List incidents) { + @Expose double emergencyThreshold, + @Expose List incidents) { public ServiceStatus { incidents = incidents == null ? ImmutableList.of() : incidents; @@ -104,11 +104,11 @@ public record ServiceStatus( * Section 5.1 */ public record TldServiceState( - @Expose @SerializedName("tld") String tld, + @Expose String tld, long lastUpdateApiDatabase, // A JSON string that contains the status of the TLD as seen from the monitoring system - @Expose @SerializedName("status") String status, + @Expose String status, // A JSON object containing detailed information for each potential monitored service (i.e., // DNS, diff --git a/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java b/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java index 6d97206ce25..d066b00d125 100644 --- a/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java +++ b/core/src/main/java/google/registry/mosapi/ServiceMonitoringClient.java @@ -14,17 +14,20 @@ package google.registry.mosapi; +import com.google.common.base.Throwables; import com.google.gson.Gson; -import com.google.gson.JsonIOException; -import com.google.gson.JsonSyntaxException; +import com.google.gson.JsonParseException; import google.registry.mosapi.MosApiModels.TldServiceState; import jakarta.inject.Inject; +import java.io.IOException; import java.util.Collections; import okhttp3.Response; +import okhttp3.ResponseBody; /** Facade for MoSAPI's service monitoring endpoints. */ public class ServiceMonitoringClient { + private static final String MONITORING_STATE_ENDPOINT = "v2/monitoring/state"; private final MosApiClient mosApiClient; private final Gson gson; @@ -41,18 +44,37 @@ public ServiceMonitoringClient(MosApiClient mosApiClient, Gson gson) { * Section 5.1 */ public TldServiceState getTldServiceState(String tld) throws MosApiException { - String endpoint = "v2/monitoring/state"; try (Response response = mosApiClient.sendGetRequest( - tld, endpoint, Collections.emptyMap(), Collections.emptyMap())) { + tld, MONITORING_STATE_ENDPOINT, Collections.emptyMap(), Collections.emptyMap())) { + + ResponseBody responseBody = response.body(); + if (responseBody == null) { + throw new MosApiException( + String.format( + "MoSAPI Service Monitoring API " + "returned an empty body with status: %d", + response.code())); + } + String bodyString = responseBody.string(); if (!response.isSuccessful()) { - throw MosApiException.create( - gson.fromJson(response.body().charStream(), MosApiErrorResponse.class)); + throw parseErrorResponse(response.code(), bodyString); } - return gson.fromJson(response.body().charStream(), TldServiceState.class); - } catch (JsonIOException | JsonSyntaxException e) { + return gson.fromJson(bodyString, TldServiceState.class); + } catch (IOException | JsonParseException e) { + Throwables.throwIfInstanceOf(e, MosApiException.class); // Catch Gson's runtime exceptions (parsing errors) and wrap them throw new MosApiException("Failed to parse TLD service state response", e); } } + + /** Parses an unsuccessful MoSAPI response into a domain-specific {@link MosApiException}. */ + private MosApiException parseErrorResponse(int statusCode, String bodyString) { + try { + MosApiErrorResponse error = gson.fromJson(bodyString, MosApiErrorResponse.class); + return MosApiException.create(error); + } catch (JsonParseException e) { + return new MosApiException( + String.format("MoSAPI json parsing error (%d): %s", statusCode, bodyString), e); + } + } } diff --git a/core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java b/core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java index 584fe97360f..ed3b3bfb705 100644 --- a/core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java +++ b/core/src/test/java/google/registry/mosapi/GetServiceStateActionTest.java @@ -52,7 +52,12 @@ void testRun_singleTld_returnsStateForTld() throws Exception { action.run(); assertThat(response.getContentType()).isEqualTo(MediaType.JSON_UTF_8); - assertThat(response.getPayload()).contains("\"overallStatus\":\"Up\""); + assertThat(response.getPayload()) + .contains( + """ + "overallStatus":"Up" + """ + .trim()); verify(stateService).getServiceStateSummary("example"); } @@ -67,7 +72,12 @@ void testRun_noTld_returnsStateForAll() { action.run(); assertThat(response.getContentType()).isEqualTo(MediaType.JSON_UTF_8); - assertThat(response.getPayload()).contains("\"serviceStates\":[]"); + assertThat(response.getPayload()) + .contains( + """ + "serviceStates":[] + """ + .trim()); verify(stateService).getAllServiceStateSummaries(); } diff --git a/core/src/test/java/google/registry/mosapi/ServiceMonitoringClientTest.java b/core/src/test/java/google/registry/mosapi/ServiceMonitoringClientTest.java index 95fcfc3db30..ba01a3a17cc 100644 --- a/core/src/test/java/google/registry/mosapi/ServiceMonitoringClientTest.java +++ b/core/src/test/java/google/registry/mosapi/ServiceMonitoringClientTest.java @@ -15,21 +15,15 @@ package google.registry.mosapi; import static com.google.common.truth.Truth.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; import google.registry.mosapi.MosApiModels.TldServiceState; -import java.io.IOException; -import java.io.Reader; +import google.registry.tools.GsonUtils; import okhttp3.MediaType; import okhttp3.Protocol; import okhttp3.Request; @@ -40,106 +34,107 @@ public class ServiceMonitoringClientTest { + private static final String TLD = "example"; + private static final String ENDPOINT = "v2/monitoring/state"; private final MosApiClient mosApiClient = mock(MosApiClient.class); - private final Gson gson = new Gson(); + private final Gson gson = GsonUtils.provideGson(); private ServiceMonitoringClient client; @BeforeEach - void setUp() { + void beforeEach() { client = new ServiceMonitoringClient(mosApiClient, gson); } @Test - void getTldServiceState_success() throws Exception { - String json = "{ \"service\": \"RDAP\", \"status\": \"ACTIVE\" }"; - - Response realResponse = createResponse(200, json); - - when(mosApiClient.sendGetRequest(anyString(), anyString(), anyMap(), anyMap())) - .thenReturn(realResponse); - - TldServiceState result = client.getTldServiceState("example"); - - assertNotNull(result); + void testGetTldServiceState_success() throws Exception { + String jsonResponse = + """ + { + "tld": "example", + "services": [ + { + "service": "DNS", + "status": "OPERATIONAL" + } + ] + } + """; + + try (Response response = createMockResponse(200, jsonResponse)) { + when(mosApiClient.sendGetRequest(eq(TLD), eq(ENDPOINT), anyMap(), anyMap())) + .thenReturn(response); + + TldServiceState result = client.getTldServiceState(TLD); + assertThat(gson.toJson(result)).contains("example"); + } } @Test - void getTldServiceState_apiErrorResponse_throwsMosApiException() throws Exception { - String errorJson = "{ \"code\": 400, \"message\": \"Invalid TLD\" }"; - - Response realResponse = createResponse(400, errorJson); - - when(mosApiClient.sendGetRequest(anyString(), anyString(), anyMap(), anyMap())) - .thenReturn(realResponse); - - MosApiException thrown = - assertThrows( - MosApiException.class, - () -> { - client.getTldServiceState("invalid"); - }); - assertThat(thrown).hasMessageThat().contains("Invalid TLD"); + void testGetTldServiceState_apiError_throwsMosApiException() throws Exception { + String errorJson = + """ + { + "resultCode": "2011", + "message": "Invalid duration" + } + """; + + try (Response response = createMockResponse(400, errorJson)) { + when(mosApiClient.sendGetRequest(eq(TLD), eq(ENDPOINT), anyMap(), anyMap())) + .thenReturn(response); + + MosApiException thrown = + assertThrows(MosApiException.class, () -> client.getTldServiceState(TLD)); + assertThat(thrown.getMessage()).contains("2011"); + assertThat(thrown.getMessage()).contains("Invalid duration"); + } } @Test - void getTldServiceState_malformedJson_throwsMosApiException() throws Exception { - String garbage = "Gateway Timeout"; - Response realResponse = createResponse(200, garbage); - - when(mosApiClient.sendGetRequest(anyString(), anyString(), anyMap(), anyMap())) - .thenReturn(realResponse); - - MosApiException thrown = - assertThrows( - MosApiException.class, - () -> { - client.getTldServiceState("example"); - }); - - assertEquals("Failed to parse TLD service state response", thrown.getMessage()); - assertEquals(JsonSyntaxException.class, thrown.getCause().getClass()); + void testGetTldServiceState_nonJsonError_throwsMosApiException() throws Exception { + String htmlError = + """ + + 502 Bad Gateway + + """; + + try (Response response = createMockResponse(502, htmlError)) { + when(mosApiClient.sendGetRequest(eq(TLD), eq(ENDPOINT), anyMap(), anyMap())) + .thenReturn(response); + + MosApiException thrown = + assertThrows(MosApiException.class, () -> client.getTldServiceState(TLD)); + assertThat(thrown.getMessage()).contains("MoSAPI json parsing error (502)"); + assertThat(thrown.getMessage()).contains("502 Bad Gateway"); + } } @Test - void getTldServiceState_networkFailureDuringRead_throwsMosApiException() throws Exception { - - ResponseBody mockBody = mock(ResponseBody.class); - Reader mockReader = mock(Reader.class); - - Response mixedResponse = + void testGetTldServiceState_emptyBody_throwsMosApiException() throws Exception { + Response response = new Response.Builder() - .request(new Request.Builder().url("http://localhost/").build()) + .request(new Request.Builder().url("http://localhost").build()) .protocol(Protocol.HTTP_1_1) - .code(200) - .message("OK") - .body(mockBody) + .code(204) + .message("No Content") .build(); - when(mosApiClient.sendGetRequest(anyString(), anyString(), anyMap(), anyMap())) - .thenReturn(mixedResponse); - when(mockBody.charStream()).thenReturn(mockReader); - - when(mockReader.read(any(char[].class), anyInt(), anyInt())) - .thenThrow(new IOException("Network failure during read")); + when(mosApiClient.sendGetRequest(eq(TLD), eq(ENDPOINT), anyMap(), anyMap())) + .thenReturn(response); MosApiException thrown = - assertThrows( - MosApiException.class, - () -> { - client.getTldServiceState("example"); - }); - - assertEquals("Failed to parse TLD service state response", thrown.getMessage()); - assertEquals("Network failure during read", thrown.getCause().getCause().getMessage()); + assertThrows(MosApiException.class, () -> client.getTldServiceState(TLD)); + assertThat(thrown.getMessage()).contains("returned an empty body"); } - private Response createResponse(int code, String jsonBody) { + private Response createMockResponse(int code, String body) { return new Response.Builder() - .request(new Request.Builder().url("http://localhost/").build()) + .request(new Request.Builder().url("http://localhost").build()) .protocol(Protocol.HTTP_1_1) .code(code) .message(code == 200 ? "OK" : "Error") - .body(ResponseBody.create(jsonBody, MediaType.get("application/json"))) + .body(ResponseBody.create(body, MediaType.parse("application/json"))) .build(); } } From 562a6da99ec6ce8dbbfded4515945b4119eb76ad Mon Sep 17 00:00:00 2001 From: njshah301 Date: Wed, 31 Dec 2025 07:47:21 +0000 Subject: [PATCH 5/5] Refactor: use nullToEmptyImmutableCopy() for MoSAPI models Standardize null-handling in model classes by using the Nomulus `nullToEmptyImmutableCopy()` utility. This ensures consistent API responses with empty lists instead of omitted fields. --- .../java/google/registry/mosapi/MosApiModels.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/google/registry/mosapi/MosApiModels.java b/core/src/main/java/google/registry/mosapi/MosApiModels.java index e94516042bc..dba9598a177 100644 --- a/core/src/main/java/google/registry/mosapi/MosApiModels.java +++ b/core/src/main/java/google/registry/mosapi/MosApiModels.java @@ -14,8 +14,8 @@ package google.registry.mosapi; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy; + import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; import java.util.List; @@ -41,7 +41,7 @@ public record AllServicesStateResponse( @Expose List serviceStates) { public AllServicesStateResponse { - serviceStates = (serviceStates == null) ? ImmutableList.of() : serviceStates; + serviceStates = nullToEmptyImmutableCopy(serviceStates); } } @@ -74,7 +74,7 @@ public record ServiceStateSummary( @Expose List activeIncidents) { public ServiceStateSummary { - activeIncidents = activeIncidents == null ? ImmutableList.of() : activeIncidents; + activeIncidents = nullToEmptyImmutableCopy(activeIncidents); } } @@ -93,7 +93,7 @@ public record ServiceStatus( @Expose List incidents) { public ServiceStatus { - incidents = incidents == null ? ImmutableList.of() : incidents; + incidents = nullToEmptyImmutableCopy(incidents); } } @@ -116,7 +116,7 @@ public record TldServiceState( @Expose @SerializedName("testedServices") Map serviceStatuses) { public TldServiceState { - serviceStatuses = (serviceStatuses == null) ? ImmutableMap.of() : serviceStatuses; + serviceStatuses = nullToEmptyImmutableCopy(serviceStatuses); } } }