NamingStrategyLocaleTest.java

package tools.jackson.databind.util;

import java.util.Locale;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.api.parallel.Resources;

import tools.jackson.databind.EnumNamingStrategies;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
 * Verifies that {@link NamingStrategyImpls#UPPER_SNAKE_CASE} and
 * {@link NamingStrategyImpls#LOWER_CASE} fold case using {@code Locale.ROOT}
 * rather than the JVM default locale, matching the existing pattern in
 * {@code EnumNamingStrategies}. Under Turkish / Azerbaijani locales the
 * Latin letter {@code i} maps to {@code \u0130} (U+0130) rather than {@code I}
 * (U+0049), which silently breaks property-name matching for identifiers
 * containing {@code i} or {@code I}.
 *
 * @see <a href="https://github.com/FasterXML/jackson-databind/issues/953">#953</a>
 * @see <a href="https://github.com/FasterXML/jackson-databind/issues/3238">#3238</a>
 */
// Mutates the global default Locale, so must not run concurrently with any
// other test: acquire JUnit's built-in lock on the shared LOCALE resource.
@ResourceLock(Resources.LOCALE)
public class NamingStrategyLocaleTest extends DatabindTestUtil
{
    private static final Locale TURKISH = Locale.forLanguageTag("tr-TR");

    private final ObjectMapper MAPPER = objectMapper();
    
    private Locale previousDefault;

    @BeforeEach
    public void switchToTurkishLocale() {
        previousDefault = Locale.getDefault();
        Locale.setDefault(TURKISH);
    }

    @AfterEach
    public void restoreLocale() {
        Locale.setDefault(previousDefault);
    }

    @Test
    public void testUpperSnakeCaseUsesRootLocale() {
        // Precondition: under the active (Turkish) locale, default-locale folding
        // turns "i" into dotted capital I (U+0130); otherwise this test is vacuous
        assertEquals("\u0130", "i".toUpperCase(),
                "Test requires a default locale where \"i\".toUpperCase() yields U+0130");
        // ...so "isAdmin" would wrongly become "\u0130S_ADM\u0130N" without Locale.ROOT:
        assertEquals("IS_ADMIN",
                NamingStrategyImpls.UPPER_SNAKE_CASE.translate("isAdmin"));
    }

    @Test
    public void testLowerCaseUsesRootLocale() {
        // Precondition: under the active (Turkish) locale, default-locale folding
        // turns "I" into dotless lowercase i (U+0131); otherwise this test is vacuous
        assertEquals("\u0131", "I".toLowerCase(),
                "Test requires a default locale where \"I\".toLowerCase() yields U+0131");
        // ...so "ClientID" would wrongly become "client\u0131d" without Locale.ROOT:
        assertEquals("clientid",
                NamingStrategyImpls.LOWER_CASE.translate("ClientID"));
    }

    // Enum naming strategies share the same hazard: EnumNamingStrategies.normalizeWord()
    // lower-cases the tail of each word, which must also use Locale.ROOT. Note the "I" must
    // be a non-leading letter ("ADMIN"), since a word's first letter is folded with the
    // locale-independent Character.toUpperCase(char).

    @Test
    public void testEnumLowerCamelCaseUsesRootLocale() {
        // Precondition: under the active (Turkish) locale, "I" folds to dotless "\u0131"
        assertEquals("\u0131", "I".toLowerCase(),
                "Test requires a default locale where \"I\".toLowerCase() yields U+0131");
        // ...so "IS_ADMIN" would wrongly become "isAdm\u0131n" without Locale.ROOT:
        assertEquals("isAdmin",
                EnumNamingStrategies.LOWER_CAMEL_CASE.convertEnumToExternalName(MAPPER.deserializationConfig(),
                        null,
                        "IS_ADMIN"));
    }

    @Test
    public void testEnumUpperCamelCaseUsesRootLocale() {
        assertEquals("\u0131", "I".toLowerCase(),
                "Test requires a default locale where \"I\".toLowerCase() yields U+0131");
        // ...so "IS_ADMIN" would wrongly become "IsAdm\u0131n" without Locale.ROOT:
        assertEquals("IsAdmin",
                EnumNamingStrategies.UPPER_CAMEL_CASE.convertEnumToExternalName(MAPPER.deserializationConfig(),
                        null,
                        "IS_ADMIN"));
    }
}