diff --git a/core/src/main/java/google/registry/flows/picker/FlowPicker.java b/core/src/main/java/google/registry/flows/picker/FlowPicker.java index ff458487c..b11e43a87 100644 --- a/core/src/main/java/google/registry/flows/picker/FlowPicker.java +++ b/core/src/main/java/google/registry/flows/picker/FlowPicker.java @@ -14,6 +14,7 @@ package google.registry.flows.picker; +import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableTable; @@ -255,6 +256,17 @@ public class FlowPicker { if (innerCommand == null && !(eppInput.getCommandWrapper() instanceof Hello)) { throw new MissingCommandException(); } + if (innerCommand instanceof ResourceCommandWrapper resourceCommandWrapper) { + ResourceCommand resourceCommand = resourceCommandWrapper.getResourceCommand(); + if (resourceCommand != null) { + String wrapperName = innerCommand.getClass().getSimpleName(); + String commandName = resourceCommand.getClass().getSimpleName(); + if (!wrapperName.equals(commandName)) { + throw new MismatchedCommandException( + Ascii.toLowerCase(wrapperName), Ascii.toLowerCase(commandName)); + } + } + } // Try the FlowProviders until we find a match. The order matters because it's possible to // match multiple FlowProviders and so more specific matches are tried first. for (FlowProvider flowProvider : FLOW_PROVIDERS) { @@ -279,4 +291,14 @@ public class FlowPicker { super("Command missing"); } } + + /** Command wrapper and inner resource command do not match. */ + static class MismatchedCommandException extends SyntaxErrorException { + public MismatchedCommandException(String wrapperName, String commandName) { + super( + String.format( + "EPP command wrapper <%s> does not match resource command <%s>", + wrapperName, commandName)); + } + } } diff --git a/core/src/test/java/google/registry/flows/picker/FlowPickerTest.java b/core/src/test/java/google/registry/flows/picker/FlowPickerTest.java new file mode 100644 index 000000000..913d1279e --- /dev/null +++ b/core/src/test/java/google/registry/flows/picker/FlowPickerTest.java @@ -0,0 +1,96 @@ +// Copyright 2026 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.flows.picker; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import google.registry.flows.Flow; +import google.registry.flows.domain.DomainCheckFlow; +import google.registry.flows.domain.DomainCreateFlow; +import google.registry.model.domain.DomainCommand; +import google.registry.model.eppcommon.EppXmlTransformer; +import google.registry.model.eppinput.EppInput; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link FlowPicker}. */ +class FlowPickerTest { + + @Test + void testGetFlowClass_matchingWrapperAndCommand_returnsFlow() throws Exception { + EppInput checkEppInput = EppInput.create(EppInput.Check.create(new DomainCommand.Check())); + Class checkFlow = FlowPicker.getFlowClass(checkEppInput); + assertThat(checkFlow).isEqualTo(DomainCheckFlow.class); + + EppInput createEppInput = EppInput.create(EppInput.Create.create(new DomainCommand.Create())); + Class createFlow = FlowPicker.getFlowClass(createEppInput); + assertThat(createFlow).isEqualTo(DomainCreateFlow.class); + } + + @Test + void testGetFlowClass_mismatchedWrapperAndCommand_throwsSyntaxErrorException() { + EppInput mismatchedEppInput1 = + EppInput.create(EppInput.Check.create(new DomainCommand.Create())); + assertThat( + assertThrows( + FlowPicker.MismatchedCommandException.class, + () -> FlowPicker.getFlowClass(mismatchedEppInput1))) + .hasMessageThat() + .isEqualTo("EPP command wrapper does not match resource command "); + + EppInput mismatchedEppInput2 = + EppInput.create(EppInput.Create.create(new DomainCommand.Check())); + assertThat( + assertThrows( + FlowPicker.MismatchedCommandException.class, + () -> FlowPicker.getFlowClass(mismatchedEppInput2))) + .hasMessageThat() + .contains("EPP command wrapper does not match resource command "); + } + + @Test + void testGetFlowClass_mismatchedXml_throwsMismatchedCommandException() throws Exception { + String mismatchedXml = + """ + + + + + + example.com + + fooBAR123 + + + + ABC-12345 + + + """; + + // Verify JAXB successfully unmarshals the mismatched XML without throwing any errors + EppInput eppInput = EppXmlTransformer.unmarshal(EppInput.class, mismatchedXml.getBytes(UTF_8)); + assertThat(eppInput).isNotNull(); + + // Verify that FlowPicker intercepts the unmarshalled input and blocks it + assertThat( + assertThrows( + FlowPicker.MismatchedCommandException.class, + () -> FlowPicker.getFlowClass(eppInput))) + .hasMessageThat() + .contains("EPP command wrapper does not match resource command "); + } +}