LazyIgnoralForNumbers3730Test.java

package com.fasterxml.jackson.databind.deser.lazy;

import java.math.BigDecimal;

import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;

import com.fasterxml.jackson.core.io.NumberInput;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.json.JsonMapper;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mockStatic;

/**
 * Tests to verify that skipping of unknown/unmapped works such that
 * "expensive" numbers (all floating-point, {@code BigInteger}) is avoided.
 */
public class LazyIgnoralForNumbers3730Test
{
    static class ExtractFieldsNoDefaultConstructor3730 {
        private final String s;
        private final int i;

        @JsonCreator
        public ExtractFieldsNoDefaultConstructor3730(@JsonProperty("s") String s, @JsonProperty("i") int i) {
            this.s = s;
            this.i = i;
        }

        public String getS() {
            return s;
        }

        public int getI() {
            return i;
        }
    }

    // Another class to test that we do actually call parse method -- just not
    // eagerly. But MUST use "@JsonUnwrapped" to force buffering; creator not enough
    static class UnwrappedWithNumber {
        @JsonUnwrapped
        public Values values;

        static class Values {
            public String s;
            public int i;
            public Number n;
        }
    }

    // Same as above
    static class UnwrappedWithBigDecimal {
        @JsonUnwrapped
        public Values values;

        static class Values {
            public String s;
            public int i;
            public BigDecimal n;
        }
    }

    // And same here
    static class UnwrappedWithDouble {
        @JsonUnwrapped
        public Values values;

        static class Values {
            public String s;
            public int i;
            public double n;
        }
    }

    /*
    /**********************************************************
    /* Test methods
    /**********************************************************
     */

    private final ObjectMapper MAPPER = JsonMapper.builder()
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .build();

    private final ObjectMapper STRICT_MAPPER = JsonMapper.builder()
            .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .build();

    @SuppressWarnings("deprecation")
    @Test
    public void testIgnoreBigInteger() throws Exception
    {
        try (MockedStatic<NumberInput> mocked = mockStatic(NumberInput.class)) {
            // Set up, mock NumberInput.parseBigInteger() to throw exception
            final String MOCK_MSG = "mock: deliberate failure for parseBigInteger";
            mocked.when(() -> NumberInput.parseBigInteger(Mockito.anyString()))
                    .thenThrow(new IllegalStateException(MOCK_MSG));
            mocked.when(() -> NumberInput.parseBigInteger(Mockito.anyString(), Mockito.anyBoolean()))
                    .thenThrow(new IllegalStateException(MOCK_MSG));

            // Then start testing!
            StringBuilder stringBuilder = new StringBuilder();
            for (int i = 0; i < 999; i++) {
                stringBuilder.append(7);
            }
            final String testBigInteger = stringBuilder.toString();
            final String json = genJson(testBigInteger);
            ExtractFieldsNoDefaultConstructor3730 ef =
                    MAPPER.readValue(json, ExtractFieldsNoDefaultConstructor3730.class);
            assertNotNull(ef);
            // Ok but then let's ensure method IS called, if field is actually mapped,
            // first to Number
            try {
                Object ob = STRICT_MAPPER.readValue(json, UnwrappedWithNumber.class);
                fail("Should throw exception with mocking: instead got: "+MAPPER.writeValueAsString(ob));
            } catch (DatabindException e) {
                verifyMockException(e, MOCK_MSG);
            }
        }
    }

    @SuppressWarnings("deprecation")
    @Test
    public void testIgnoreFPValuesDefault() throws Exception
    {
        try (MockedStatic<NumberInput> mocked = mockStatic(NumberInput.class)) {
            // Set up, mock NumberInput.parseDouble() to throw exception
            final String MOCK_MSG = "mock: deliberate failure for parseDouble";
            // With default settings we would parse Doubles, so check
            mocked.when(() -> NumberInput.parseDouble(Mockito.anyString()))
                .thenThrow(new IllegalStateException(MOCK_MSG));
            mocked.when(() -> NumberInput.parseDouble(Mockito.anyString(), Mockito.anyBoolean()))
                .thenThrow(new IllegalStateException(MOCK_MSG));

            // Then start testing!
            final String json = genJson("0.25");
            ExtractFieldsNoDefaultConstructor3730 ef =
                MAPPER.readValue(json, ExtractFieldsNoDefaultConstructor3730.class);
            assertNotNull(ef);

            // Ok but then let's ensure method IS called, if field is actually mapped
            // First, to "Number"
            try {
                STRICT_MAPPER.readValue(json, UnwrappedWithNumber.class);
                fail("Should throw exception with mocking!");
            } catch (DatabindException e) {
                verifyMockException(e, MOCK_MSG);
            }

            // And then to "double"
            // 01-Feb-2023, tatu: Not quite working, yet:
            try {
                STRICT_MAPPER.readValue(json, UnwrappedWithDouble.class);
                fail("Should throw exception with mocking!");
            } catch (DatabindException e) {
                e.printStackTrace();
                verifyMockException(e, MOCK_MSG);
            }
        }
    }

    @SuppressWarnings("deprecation")
    @Test
    public void testIgnoreFPValuesBigDecimal() throws Exception
    {
        try (MockedStatic<NumberInput> mock = mockStatic(NumberInput.class)) {
            // Set up, mock NumberInput.parseBigDecimal() to throw exception
            // Now should get calls to `parseBigDecimal`... eventually
            final String MOCK_MSG = "mock: deliberate failure for parseBigDecimal";
            // With default settings we would parse Doubles, so check
            mock.when(() -> NumberInput.parseBigDecimal(Mockito.anyString()))
                .thenThrow(new IllegalStateException(MOCK_MSG));
            mock.when(() -> NumberInput.parseBigDecimal(Mockito.anyString(), Mockito.anyBoolean()))
                .thenThrow(new IllegalStateException(MOCK_MSG));

            // Then start testing!
            ObjectReader reader = MAPPER
            .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
            .readerFor(ExtractFieldsNoDefaultConstructor3730.class);

            final String json = genJson("0.25");
            ExtractFieldsNoDefaultConstructor3730 ef =
                    reader.readValue(genJson(json));
            assertNotNull(ef);

            // But then ensure we'll fail with unknown (except does it work with unwrapped?)
            reader = reader.with(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

            // Ok but then let's ensure method IS called, if field is actually mapped
            // First to Number
            try {
                reader.forType(UnwrappedWithNumber.class).readValue(json);
                fail("Should throw exception with mocking!");
            } catch (DatabindException e) {
                verifyMockException(e, MOCK_MSG);
            }

            // And then to "BigDecimal"
            // 01-Feb-2023, tatu: Not quite working, yet:
            try {
                reader.forType(UnwrappedWithBigDecimal.class).readValue(json);
                fail("Should throw exception with mocking!");
            } catch (DatabindException e) {
                verifyMockException(e, MOCK_MSG);
            }
        }
    }

    private String genJson(String num) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder
                .append("{\"s\":\"s\",\"n\":")
                .append(num)
                .append(",\"i\":1}");
        return stringBuilder.toString();
    }

    private void verifyMockException(DatabindException e, String expMsg) {
        Throwable cause = e.getCause();
        assertEquals(IllegalStateException.class, cause.getClass());
        assertEquals(expMsg, cause.getMessage());
    }
}