1
0
mirror of https://github.com/google/nomulus synced 2026-03-26 20:35:18 +00:00

Activate Fee tag normalization in Non-Prod (#2963)

For all flows that use Fee extensions, normalize the fee tags in all
non-prod environments.

For flows that do not use fee extensions but with fee tags in the
header, e.g., HostInfo flows, normalization is not performed.
This commit is contained in:
Weimin Yu
2026-02-19 20:04:27 +00:00
committed by GitHub
parent e4c4149033
commit 6747cc894d
4 changed files with 103 additions and 1 deletions

View File

@@ -20,9 +20,14 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.FeeExtensionXmlTagNormalizer;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.fee.FeeCheckResponseExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import google.registry.model.domain.fee06.FeeInfoResponseExtensionV06;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.eppoutput.EppResponse;
import google.registry.util.RegistryEnvironment;
import google.registry.xml.ValidationMode;
import google.registry.xml.XmlException;
@@ -98,8 +103,31 @@ public class EppXmlTransformer {
return byteArrayOutputStream.toByteArray();
}
private static boolean hasFeeExtension(EppOutput eppOutput) {
if (!eppOutput.isResponse()) {
return false;
}
return eppOutput.getResponse().getExtensions().stream()
.map(EppResponse.ResponseExtension::getClass)
.filter(EppXmlTransformer::isFeeExtension)
.findAny()
.isPresent();
}
@VisibleForTesting
static boolean isFeeExtension(Class<?> clazz) {
return FeeCheckResponseExtension.class.isAssignableFrom(clazz)
|| FeeTransformResponseExtension.class.isAssignableFrom(clazz)
|| FeeInfoResponseExtensionV06.class.isAssignableFrom(clazz);
}
public static byte[] marshal(EppOutput root, ValidationMode validation) throws XmlException {
return marshal(OUTPUT_TRANSFORMER, root, validation);
byte[] bytes = marshal(OUTPUT_TRANSFORMER, root, validation);
if (!RegistryEnvironment.PRODUCTION.equals(RegistryEnvironment.get())
&& hasFeeExtension(root)) {
return FeeExtensionXmlTagNormalizer.normalize(new String(bytes, UTF_8)).getBytes(UTF_8);
}
return bytes;
}
@VisibleForTesting

View File

@@ -17,8 +17,10 @@ package google.registry.flows;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.flows.FeeExtensionXmlTagNormalizer.feeExtensionInUseRegex;
import static google.registry.flows.FeeExtensionXmlTagNormalizer.normalize;
import static google.registry.flows.FlowTestCase.verifyFeeTagNormalized;
import static google.registry.model.eppcommon.EppXmlTransformer.validateOutput;
import static google.registry.testing.TestDataHelper.loadFile;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
@@ -41,6 +43,13 @@ class FeeExtensionXmlTagNormalizerTest {
assertThat(normalized).isEqualTo(xml);
}
@Test
void normalize_greetingUnchanged() throws Exception {
String xml = loadFile(getClass(), "greeting.xml");
String normalized = normalize(xml);
assertThat(normalized).isEqualTo(xml);
}
@ParameterizedTest(name = "normalize_withFeeExtension-{0}")
@MethodSource("provideTestCombinations")
@SuppressWarnings("unused") // Parameter 'name' is part of test case name
@@ -55,6 +64,28 @@ class FeeExtensionXmlTagNormalizerTest {
assertThat(normalized).isEqualTo(expected);
}
// Piggyback tests for FlowTestCase.verifyFeeTagNormalized here.
@ParameterizedTest(name = "verifyFeeTagNormalized-{0}")
@MethodSource("provideTestCombinations")
@SuppressWarnings("unused") // Parameter 'name' is part of test case name
void verifyFeeTagNormalized_success(
String name, String inputXmlFilename, String expectedXmlFilename) throws Exception {
String original = loadFile(getClass(), inputXmlFilename);
String expected = loadFile(getClass(), expectedXmlFilename);
if (name.equals("v06")) {
// Fee-06 already uses 'fee'. Non-normalized tags only appear in header.
verifyFeeTagNormalized(original);
} else {
assertThrows(
AssertionError.class,
() -> {
verifyFeeTagNormalized(original);
});
}
verifyFeeTagNormalized(expected);
}
@SuppressWarnings("unused")
static Stream<Arguments> provideTestCombinations() {
return Stream.of(

View File

@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Sets.difference;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static google.registry.flows.FlowUtils.marshalWithLenientRetry;
import static google.registry.model.eppcommon.EppXmlTransformer.marshal;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.stripBillingEventId;
@@ -55,6 +56,7 @@ import google.registry.xml.ValidationMode;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -284,6 +286,7 @@ public abstract class FlowTestCase<F extends Flow> {
Arrays.toString(marshal(output, ValidationMode.LENIENT))),
e);
}
verifyFeeTagNormalized(new String(marshalWithLenientRetry(output), UTF_8));
return output;
}
@@ -298,4 +301,14 @@ public abstract class FlowTestCase<F extends Flow> {
public EppOutput runFlowAssertResponse(String xml, String... ignoredPaths) throws Exception {
return runFlowAssertResponse(CommitMode.LIVE, UserPrivileges.NORMAL, xml, ignoredPaths);
}
// Pattern for non-normalized tags in use. Occurrences in namespace declarations ignored.
private static final Pattern NON_NORMALIZED_FEE_TAGS =
Pattern.compile("\\bfee11:|\\bfee12:|\\bfee_1_00:");
static void verifyFeeTagNormalized(String xml) {
assertWithMessage("Unexpected un-normalized Fee tags found in message.")
.that(NON_NORMALIZED_FEE_TAGS.matcher(xml).find())
.isFalse();
}
}

View File

@@ -15,18 +15,48 @@
package google.registry.model.eppcommon;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static google.registry.model.eppcommon.EppXmlTransformer.isFeeExtension;
import static google.registry.model.eppcommon.EppXmlTransformer.unmarshal;
import static google.registry.testing.TestDataHelper.loadBytes;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableSet;
import google.registry.model.domain.bulktoken.BulkTokenResponseExtension;
import google.registry.model.domain.launch.LaunchCheckResponseExtension;
import google.registry.model.domain.rgp.RgpInfoExtension;
import google.registry.model.domain.secdns.SecDnsInfoExtension;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.eppoutput.EppResponse;
import google.registry.util.RegistryEnvironment;
import jakarta.xml.bind.annotation.XmlElementRef;
import jakarta.xml.bind.annotation.XmlElementRefs;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
/** Tests for {@link EppXmlTransformer}. */
class EppXmlTransformerTest {
// Non-fee extensions allowed in {@code Response.extensions}.
private static final ImmutableSet<Class<?>> NON_FEE_EXTENSIONS =
ImmutableSet.of(
BulkTokenResponseExtension.class,
LaunchCheckResponseExtension.class,
RgpInfoExtension.class,
SecDnsInfoExtension.class);
@Test
void isFeeExtension_eppResponse() throws Exception {
var xmlRefs =
EppResponse.class.getDeclaredField("extensions").getAnnotation(XmlElementRefs.class);
Arrays.stream(xmlRefs.value())
.map(XmlElementRef::type)
.filter(type -> !NON_FEE_EXTENSIONS.contains(type))
.forEach(
type -> assertWithMessage(type.getSimpleName()).that(isFeeExtension(type)).isTrue());
}
@Test
void testUnmarshalingEppInput() throws Exception {
EppInput input = unmarshal(EppInput.class, loadBytes(getClass(), "contact_info.xml").read());