RegressionTest.java

package org.json.simple;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import org.json.simple.parser.ParseException;
import org.json.simple.parser.Yytoken;

import junit.framework.TestCase;

/**
 * Regression tests for the defects fixed in 1.1.2, plus assertions pinning the
 * behaviours listed as compatibility red lines in CLAUDE.md.
 *
 * Every test in the first half fails against 1.1.1.
 */
public class RegressionTest extends TestCase {

	/**
	 * An integer too large for a long used to escape as NumberFormatException,
	 * violating the declared contract of parseWithException.
	 */
	public void testOverflowingIntegerReportsParseException() throws Exception {
		try {
			JSONValue.parseWithException("99999999999999999999999999");
			fail("expected a ParseException");
		} catch (ParseException e) {
			assertEquals(ParseException.ERROR_UNEXPECTED_EXCEPTION, e.getErrorType());
			assertTrue(e.getUnexpectedObject() instanceof NumberFormatException);
		}
	}

	/**
	 * getArray() cast the result of the no-arg toArray(), which is always an
	 * Object[], so it threw ClassCastException for every input.
	 */
	public void testItemListGetArray() {
		String[] items = new ItemList("a,b,c").getArray();
		assertEquals(3, items.length);
		assertEquals("a", items[0]);
		assertEquals("b", items[1]);
		assertEquals("c", items[2]);
	}

	/**
	 * The two-argument constructor assigned the input string to the separator
	 * field, so toString() joined the items with the whole input.
	 */
	public void testItemListSeparatorConstructor() {
		ItemList list = new ItemList("a|b", "|");
		assertEquals(2, list.size());
		assertEquals("a", list.get(0));
		assertEquals("b", list.get(1));
		assertEquals("a|b", list.toString());
	}

	/**
	 * ParseException holds the offending token, which was not serializable, so
	 * serializing the exception threw NotSerializableException.
	 */
	public void testParseExceptionIsSerializable() throws Exception {
		ParseException original = null;
		try {
			JSONValue.parseWithException("[1,2}");
			fail("expected a ParseException");
		} catch (ParseException e) {
			original = e;
		}
		assertTrue(original.getUnexpectedObject() instanceof Yytoken);

		ByteArrayOutputStream bytes = new ByteArrayOutputStream();
		ObjectOutputStream out = new ObjectOutputStream(bytes);
		out.writeObject(original);
		out.close();

		ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()));
		ParseException restored = (ParseException) in.readObject();
		in.close();

		assertEquals(original.getMessage(), restored.getMessage());
		assertEquals(original.getPosition(), restored.getPosition());
	}

	/*
	 * ---- Binary compatibility with the published 1.1.1. ----
	 *
	 * These three regressions reached master after 1.1.1 shipped and were fixed
	 * in 1.1.2. Source compatibility never broke, so only reflection and
	 * serialization metadata can catch them.
	 */

	/**
	 * Widening these parameters from List to Collection left already-compiled
	 * callers looking up a descriptor that no longer existed, so they failed
	 * with NoSuchMethodError at runtime.
	 */
	public void testListOverloadsStillExist() throws Exception {
		assertNotNull(JSONArray.class.getMethod("toJSONString", java.util.List.class));
		assertNotNull(JSONArray.class.getMethod("writeJSONString",
				java.util.List.class, java.io.Writer.class));
		// and the Collection forms they delegate to
		assertNotNull(JSONArray.class.getMethod("toJSONString", java.util.Collection.class));
		assertNotNull(JSONArray.class.getMethod("writeJSONString",
				java.util.Collection.class, java.io.Writer.class));

		java.util.List list = new java.util.ArrayList();
		list.add("x");
		list.add(Long.valueOf(1));
		assertEquals("[\"x\",1]", JSONArray.toJSONString(list));

		java.io.StringWriter writer = new java.io.StringWriter();
		JSONArray.writeJSONString(list, writer);
		assertEquals("[\"x\",1]", writer.toString());
	}

	/**
	 * On 1.1.1 toString() carried the description and getMessage() returned
	 * null. Both must return it now, so callers written against either release
	 * see the same text.
	 */
	public void testParseExceptionToStringCarriesTheDescription() {
		try {
			JSONValue.parseWithException("[1,2}");
			fail("expected a ParseException");
		} catch (ParseException e) {
			String expected = "Unexpected token RIGHT BRACE(}) at position 4.";
			assertEquals(expected, e.getMessage());
			assertEquals(expected, e.toString());
		}
	}

	/** Changing this value would break deserialization across versions. */
	public void testParseExceptionSerialVersionUidIsUnchanged() {
		assertEquals(-7880698968187728548L,
				java.io.ObjectStreamClass.lookup(ParseException.class).getSerialVersionUID());
	}

	/*
	 * ---- Red lines. These pin behaviour that must not change; see CLAUDE.md. ----
	 */

	/** Forward slashes are escaped, and so are U+2028 and its neighbours. */
	public void testEscapingIsUnchanged() {
		assertEquals("\"a\\/b\"", JSONValue.toJSONString("a/b"));
		assertEquals("\"\\u2028\"", JSONValue.toJSONString("\u2028"));
		assertEquals("\"\\u0085\"", JSONValue.toJSONString("\u0085"));
		assertEquals("\"\\t\"", JSONValue.toJSONString("\t"));
	}

	/** Integers are always Long and fractions always Double, at any magnitude. */
	public void testDecodedNumberTypesAreUnchanged() throws Exception {
		assertEquals(Long.class, JSONValue.parseWithException("1").getClass());
		assertEquals(Long.class, JSONValue.parseWithException("2147483648").getClass());
		assertEquals(Double.class, JSONValue.parseWithException("1.5").getClass());
		assertEquals(Double.class, JSONValue.parseWithException("1e3").getClass());
	}

	/** The deprecated parse() still swallows everything and returns null. */
	public void testDeprecatedParseStillReturnsNullOnGarbage() {
		assertNull(JSONValue.parse("{"));
		assertNull(JSONValue.parse("'nope'"));
	}

	/** Documented parser leniency, relied on downstream for well over a decade. */
	public void testParserLeniencyIsUnchanged() throws Exception {
		assertEquals("[1,2]", JSONValue.toJSONString(JSONValue.parseWithException("[1,2,]")));
		assertEquals("[1,2]", JSONValue.toJSONString(JSONValue.parseWithException("[1 2]")));
		assertEquals("[1,2]", JSONValue.toJSONString(JSONValue.parseWithException("[1,,2]")));
		assertEquals("{\"a\":1}", JSONValue.toJSONString(JSONValue.parseWithException("{\"a\" 1}")));
		assertEquals(Long.valueOf(123), JSONValue.parseWithException("0123"));
	}

	/** Decoding is iterative and must stay safe at depths that break recursion. */
	public void testDeeplyNestedInputStillDecodes() throws Exception {
		int depth = 50000;
		StringBuilder sb = new StringBuilder(depth * 2);
		for (int i = 0; i < depth; i++) {
			sb.append('[');
		}
		for (int i = 0; i < depth; i++) {
			sb.append(']');
		}
		Object decoded = JSONValue.parseWithException(sb.toString());
		assertTrue(decoded instanceof JSONArray);
	}
}