This is an automated email from the ASF dual-hosted git repository. robertlazarski pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git
commit 4d4a720e4786e01c284605198c0e78bd20583917 Author: Robert Lazarski <[email protected]> AuthorDate: Tue Apr 14 10:44:16 2026 -1000 AXIS2-6103 Add unit tests for streaming JSON message formatters 18 tests across three test classes: FlushingOutputStreamTest (9 tests): - Data passthrough correctness - Single-byte write flush at interval - Bulk write flush behavior - No-flush below interval threshold - Counter reset after flush - Default 64KB interval - Zero/negative interval rejection - Long counter overflow prevention (bulk + single-byte combined) JSONStreamingMessageFormatterTest (4 tests): - Return-object serialization (GSON, matches standard formatter output) - Fault response serialization - Large object with many fields (validates JSON parseable) - Content type passthrough MoshiStreamingMessageFormatterTest (5 tests): - Return-object serialization (Moshi) - Fault response serialization - Large object validation - Output matches standard Moshi JsonFormatter (bit-identical comparison) - Content type passthrough --- .../json/streaming/FlushingOutputStreamTest.java | 195 ++++++++++++++++++++ .../JSONStreamingMessageFormatterTest.java | 187 +++++++++++++++++++ .../MoshiStreamingMessageFormatterTest.java | 205 +++++++++++++++++++++ 3 files changed, 587 insertions(+) diff --git a/modules/json/test/org/apache/axis2/json/streaming/FlushingOutputStreamTest.java b/modules/json/test/org/apache/axis2/json/streaming/FlushingOutputStreamTest.java new file mode 100644 index 0000000000..7bc57ae85a --- /dev/null +++ b/modules/json/test/org/apache/axis2/json/streaming/FlushingOutputStreamTest.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.axis2.json.streaming; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * Unit tests for {@link FlushingOutputStream}. + */ +public class FlushingOutputStreamTest { + + /** + * Verify that data is written through correctly. + */ + @Test + public void testDataPassthrough() throws IOException { + ByteArrayOutputStream underlying = new ByteArrayOutputStream(); + FlushingOutputStream fos = new FlushingOutputStream(underlying, 100); + + byte[] data = "Hello, streaming world!".getBytes("UTF-8"); + fos.write(data, 0, data.length); + fos.flush(); + + Assert.assertEquals("Hello, streaming world!", underlying.toString("UTF-8")); + } + + /** + * Verify that single-byte writes accumulate and flush at the interval. + */ + @Test + public void testSingleByteWrite() throws IOException { + CountingOutputStream counter = new CountingOutputStream(); + FlushingOutputStream fos = new FlushingOutputStream(counter, 10); + + // Write 25 bytes one at a time — should trigger 2 flushes (at byte 10 and 20) + for (int i = 0; i < 25; i++) { + fos.write('A'); + } + + Assert.assertEquals(2, counter.flushCount); + Assert.assertEquals(25, counter.bytesWritten); + } + + /** + * Verify that bulk writes trigger flush at the correct interval. + */ + @Test + public void testBulkWriteFlush() throws IOException { + CountingOutputStream counter = new CountingOutputStream(); + FlushingOutputStream fos = new FlushingOutputStream(counter, 100); + + // Write 250 bytes in one call — single write exceeds interval, + // triggers 1 flush and resets counter + byte[] data = new byte[250]; + fos.write(data, 0, data.length); + + Assert.assertEquals(1, counter.flushCount); + Assert.assertEquals(250, counter.bytesWritten); + Assert.assertEquals(0, fos.getBytesSinceFlush()); + } + + /** + * Verify that writes smaller than the flush interval do not trigger flush. + */ + @Test + public void testNoFlushBelowInterval() throws IOException { + CountingOutputStream counter = new CountingOutputStream(); + FlushingOutputStream fos = new FlushingOutputStream(counter, 1000); + + byte[] data = new byte[500]; + fos.write(data, 0, data.length); + + Assert.assertEquals(0, counter.flushCount); + Assert.assertEquals(500, counter.bytesWritten); + } + + /** + * Verify that the counter resets after each flush. + */ + @Test + public void testCounterResets() throws IOException { + CountingOutputStream counter = new CountingOutputStream(); + FlushingOutputStream fos = new FlushingOutputStream(counter, 100); + + // First write: 120 bytes → exceeds 100, flush, counter resets to 0 + fos.write(new byte[120], 0, 120); + Assert.assertEquals(1, counter.flushCount); + Assert.assertEquals(0, fos.getBytesSinceFlush()); + + // Second write: 90 bytes → counter at 90, below 100, no flush + fos.write(new byte[90], 0, 90); + Assert.assertEquals(1, counter.flushCount); + Assert.assertEquals(90, fos.getBytesSinceFlush()); + + // Third write: 20 bytes → counter at 110, flush, counter resets + fos.write(new byte[20], 0, 20); + Assert.assertEquals(2, counter.flushCount); + Assert.assertEquals(0, fos.getBytesSinceFlush()); + } + + /** + * Verify that the default flush interval is 64 KB. + */ + @Test + public void testDefaultFlushInterval() throws IOException { + ByteArrayOutputStream underlying = new ByteArrayOutputStream(); + FlushingOutputStream fos = new FlushingOutputStream(underlying); + + Assert.assertEquals(64 * 1024, fos.getFlushIntervalBytes()); + } + + /** + * Verify that zero or negative flush intervals are rejected. + */ + @Test(expected = IllegalArgumentException.class) + public void testZeroIntervalRejected() { + new FlushingOutputStream(new ByteArrayOutputStream(), 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testNegativeIntervalRejected() { + new FlushingOutputStream(new ByteArrayOutputStream(), -1); + } + + /** + * Verify long counter does not overflow on large cumulative writes. + */ + @Test + public void testLongCounterNoOverflow() throws IOException { + CountingOutputStream counter = new CountingOutputStream(); + // Use a very small interval so we can verify the counter resets properly + FlushingOutputStream fos = new FlushingOutputStream(counter, 10); + + // Write 100K bytes in bulk — single write exceeds interval, 1 flush + byte[] data = new byte[100_000]; + fos.write(data, 0, data.length); + + Assert.assertEquals(1, counter.flushCount); + Assert.assertEquals(0, fos.getBytesSinceFlush()); + + // Write 100K more in single-byte mode to exercise per-byte counter + for (int i = 0; i < 100; i++) { + fos.write('X'); + } + // 100 single-byte writes with interval=10 → 10 more flushes + Assert.assertEquals(11, counter.flushCount); + } + + /** + * Helper: OutputStream that counts bytes written and flush() calls. + */ + private static class CountingOutputStream extends ByteArrayOutputStream { + int flushCount = 0; + int bytesWritten = 0; + + @Override + public void write(int b) { + super.write(b); + bytesWritten++; + } + + @Override + public void write(byte[] b, int off, int len) { + super.write(b, off, len); + bytesWritten += len; + } + + @Override + public void flush() throws IOException { + super.flush(); + flushCount++; + } + } +} diff --git a/modules/json/test/org/apache/axis2/json/streaming/JSONStreamingMessageFormatterTest.java b/modules/json/test/org/apache/axis2/json/streaming/JSONStreamingMessageFormatterTest.java new file mode 100644 index 0000000000..113b2ff34d --- /dev/null +++ b/modules/json/test/org/apache/axis2/json/streaming/JSONStreamingMessageFormatterTest.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.axis2.json.streaming; + +import com.google.gson.Gson; +import org.apache.axiom.om.OMAbstractFactory; +import org.apache.axiom.om.OMElement; +import org.apache.axiom.om.OMFactory; +import org.apache.axiom.om.OMNamespace; +import org.apache.axiom.om.OMOutputFormat; +import org.apache.axiom.soap.SOAPEnvelope; +import org.apache.axiom.soap.SOAPFactory; +import org.apache.axis2.Constants; +import org.apache.axis2.context.MessageContext; +import org.apache.axis2.json.factory.JsonConstant; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; + +/** + * Unit tests for {@link JSONStreamingMessageFormatter} (GSON variant). + * Mirrors the test patterns in {@code JsonFormatterTest} to verify + * the streaming formatter produces identical output. + */ +public class JSONStreamingMessageFormatterTest { + + private MessageContext outMsgContext; + private OMOutputFormat outputFormat; + private ByteArrayOutputStream outputStream; + + @Before + public void setUp() throws Exception { + SOAPFactory soapFactory = OMAbstractFactory.getSOAP11Factory(); + SOAPEnvelope soapEnvelope = soapFactory.getDefaultEnvelope(); + outputFormat = new OMOutputFormat(); + outputStream = new ByteArrayOutputStream(); + + outMsgContext = new MessageContext(); + outMsgContext.setProperty(Constants.Configuration.CHARACTER_SET_ENCODING, "UTF-8"); + } + + @After + public void tearDown() throws Exception { + outputStream.close(); + } + + /** + * Test that a return-object response produces valid JSON identical + * to the non-streaming JsonFormatter. + */ + @Test + public void testWriteToReturnObject() throws Exception { + TestPerson person = new TestPerson("Leo", 27, "Male", true); + outMsgContext.setProperty(JsonConstant.RETURN_OBJECT, person); + outMsgContext.setProperty(JsonConstant.RETURN_TYPE, TestPerson.class); + + String expected = "{\"" + JsonConstant.RESPONSE + "\":" + + new Gson().toJson(person, TestPerson.class) + "}"; + + JSONStreamingMessageFormatter formatter = new JSONStreamingMessageFormatter(); + formatter.writeTo(outMsgContext, outputFormat, outputStream, false); + + String result = outputStream.toString("UTF-8"); + Assert.assertEquals(expected, result); + } + + /** + * Test that a fault response produces valid JSON. + */ + @Test + public void testWriteToFaultMessage() throws Exception { + String expected = "{\"Fault\":{\"faultcode\":\"soapenv:Server\"," + + "\"faultstring\":\"javax.xml.stream.XMLStreamException\"," + + "\"detail\":\"testFaultMsg\"}}"; + + outMsgContext.setProcessingFault(true); + SOAPFactory soapFactory = OMAbstractFactory.getSOAP11Factory(); + SOAPEnvelope soapEnvelope = soapFactory.getDefaultEnvelope(); + soapEnvelope.getBody().addChild(createFaultOMElement()); + outMsgContext.setEnvelope(soapEnvelope); + + JSONStreamingMessageFormatter formatter = new JSONStreamingMessageFormatter(); + formatter.writeTo(outMsgContext, outputFormat, outputStream, false); + + String result = outputStream.toString("UTF-8"); + Assert.assertEquals(expected, result); + } + + /** + * Test that the streaming formatter produces identical output to the + * standard formatter for a large object with many fields. + */ + @Test + public void testLargeObjectProducesValidJSON() throws Exception { + // Build a response with many fields to exercise the flushing path + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + sb.append("field_").append(i).append("_value_padding_data "); + } + TestPerson person = new TestPerson(sb.toString(), 99, "Other", false); + outMsgContext.setProperty(JsonConstant.RETURN_OBJECT, person); + outMsgContext.setProperty(JsonConstant.RETURN_TYPE, TestPerson.class); + + JSONStreamingMessageFormatter formatter = new JSONStreamingMessageFormatter(); + formatter.writeTo(outMsgContext, outputFormat, outputStream, false); + + String result = outputStream.toString("UTF-8"); + // Verify it's valid JSON by parsing it + new Gson().fromJson(result, Object.class); + Assert.assertTrue(result.startsWith("{\"" + JsonConstant.RESPONSE + "\":")); + Assert.assertTrue(result.endsWith("}")); + } + + /** + * Test that the content type is returned correctly. + */ + @Test + public void testGetContentType() { + outMsgContext.setProperty(Constants.Configuration.CONTENT_TYPE, "application/json"); + JSONStreamingMessageFormatter formatter = new JSONStreamingMessageFormatter(); + String ct = formatter.getContentType(outMsgContext, outputFormat, null); + Assert.assertEquals("application/json", ct); + } + + // --- Helper methods --- + + private OMElement createFaultOMElement() { + OMFactory factory = OMAbstractFactory.getOMFactory(); + OMNamespace ns = factory.createOMNamespace("http://schemas.xmlsoap.org/soap/envelope/", "soapenv"); + OMElement fault = factory.createOMElement("Fault", ns); + OMElement faultCode = factory.createOMElement("faultcode", ns, fault); + faultCode.setText("soapenv:Server"); + OMElement faultString = factory.createOMElement("faultstring", ns, fault); + faultString.setText("javax.xml.stream.XMLStreamException"); + OMElement detail = factory.createOMElement("detail", ns, fault); + detail.setText("testFaultMsg"); + return fault; + } + + /** + * Simple POJO for test serialization — matches the pattern in JsonFormatterTest. + */ + public static class TestPerson { + private String name; + private int age; + private String gender; + private boolean single; + + public TestPerson() {} + + public TestPerson(String name, int age, String gender, boolean single) { + this.name = name; + this.age = age; + this.gender = gender; + this.single = single; + } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getGender() { return gender; } + public void setGender(String gender) { this.gender = gender; } + public boolean isSingle() { return single; } + public void setSingle(boolean single) { this.single = single; } + } +} diff --git a/modules/json/test/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatterTest.java b/modules/json/test/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatterTest.java new file mode 100644 index 0000000000..ac1c7576f5 --- /dev/null +++ b/modules/json/test/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatterTest.java @@ -0,0 +1,205 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.axis2.json.streaming; + +import com.squareup.moshi.Moshi; +import org.apache.axiom.om.OMAbstractFactory; +import org.apache.axiom.om.OMElement; +import org.apache.axiom.om.OMFactory; +import org.apache.axiom.om.OMNamespace; +import org.apache.axiom.om.OMOutputFormat; +import org.apache.axiom.soap.SOAPEnvelope; +import org.apache.axiom.soap.SOAPFactory; +import org.apache.axis2.Constants; +import org.apache.axis2.context.MessageContext; +import org.apache.axis2.json.factory.JsonConstant; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; + +/** + * Unit tests for {@link MoshiStreamingMessageFormatter}. + * Mirrors the GSON streaming formatter tests to verify both + * variants produce valid, equivalent output. + */ +public class MoshiStreamingMessageFormatterTest { + + private MessageContext outMsgContext; + private OMOutputFormat outputFormat; + private ByteArrayOutputStream outputStream; + + @Before + public void setUp() throws Exception { + SOAPFactory soapFactory = OMAbstractFactory.getSOAP11Factory(); + SOAPEnvelope soapEnvelope = soapFactory.getDefaultEnvelope(); + outputFormat = new OMOutputFormat(); + outputStream = new ByteArrayOutputStream(); + + outMsgContext = new MessageContext(); + outMsgContext.setProperty(Constants.Configuration.CHARACTER_SET_ENCODING, "UTF-8"); + } + + @After + public void tearDown() throws Exception { + outputStream.close(); + } + + /** + * Test that a return-object response produces valid JSON. + */ + @Test + public void testWriteToReturnObject() throws Exception { + TestData data = new TestData("streaming-test", 42, true); + outMsgContext.setProperty(JsonConstant.RETURN_OBJECT, data); + outMsgContext.setProperty(JsonConstant.RETURN_TYPE, TestData.class); + + MoshiStreamingMessageFormatter formatter = new MoshiStreamingMessageFormatter(); + formatter.writeTo(outMsgContext, outputFormat, outputStream, false); + + String result = outputStream.toString("UTF-8"); + Assert.assertTrue("Response should start with {\"response\":", + result.startsWith("{\"" + JsonConstant.RESPONSE + "\":")); + Assert.assertTrue("Response should end with }", + result.endsWith("}")); + // Verify round-trip: parse the inner object back + Assert.assertTrue(result.contains("\"label\":\"streaming-test\"")); + Assert.assertTrue(result.contains("\"count\":42")); + Assert.assertTrue(result.contains("\"active\":true")); + } + + /** + * Test that a fault response produces valid JSON. + */ + @Test + public void testWriteToFaultMessage() throws Exception { + String expected = "{\"Fault\":{\"faultcode\":\"soapenv:Server\"," + + "\"faultstring\":\"test.Exception\"," + + "\"detail\":\"moshi fault test\"}}"; + + outMsgContext.setProcessingFault(true); + SOAPFactory soapFactory = OMAbstractFactory.getSOAP11Factory(); + SOAPEnvelope soapEnvelope = soapFactory.getDefaultEnvelope(); + soapEnvelope.getBody().addChild(createFaultOMElement()); + outMsgContext.setEnvelope(soapEnvelope); + + MoshiStreamingMessageFormatter formatter = new MoshiStreamingMessageFormatter(); + formatter.writeTo(outMsgContext, outputFormat, outputStream, false); + + String result = outputStream.toString("UTF-8"); + Assert.assertEquals(expected, result); + } + + /** + * Test that a large object serializes as valid JSON through the + * flushing stream without corruption. + */ + @Test + public void testLargeObjectProducesValidJSON() throws Exception { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + sb.append("field_").append(i).append("_padding_data "); + } + TestData data = new TestData(sb.toString(), 99999, false); + outMsgContext.setProperty(JsonConstant.RETURN_OBJECT, data); + outMsgContext.setProperty(JsonConstant.RETURN_TYPE, TestData.class); + + MoshiStreamingMessageFormatter formatter = new MoshiStreamingMessageFormatter(); + formatter.writeTo(outMsgContext, outputFormat, outputStream, false); + + String result = outputStream.toString("UTF-8"); + // Verify it's parseable JSON + Moshi moshi = new Moshi.Builder().build(); + moshi.adapter(Object.class).fromJson(result); + Assert.assertTrue(result.startsWith("{\"" + JsonConstant.RESPONSE + "\":")); + } + + /** + * Test that the streaming formatter produces the same output as the + * standard Moshi formatter for the same input. + */ + @Test + public void testOutputMatchesStandardFormatter() throws Exception { + TestData data = new TestData("consistency-check", 7, true); + outMsgContext.setProperty(JsonConstant.RETURN_OBJECT, data); + outMsgContext.setProperty(JsonConstant.RETURN_TYPE, TestData.class); + + // Streaming formatter + MoshiStreamingMessageFormatter streamingFormatter = new MoshiStreamingMessageFormatter(); + streamingFormatter.writeTo(outMsgContext, outputFormat, outputStream, false); + String streamingResult = outputStream.toString("UTF-8"); + + // Standard formatter + ByteArrayOutputStream standardOutput = new ByteArrayOutputStream(); + org.apache.axis2.json.moshi.JsonFormatter standardFormatter = + new org.apache.axis2.json.moshi.JsonFormatter(); + standardFormatter.writeTo(outMsgContext, outputFormat, standardOutput, false); + String standardResult = standardOutput.toString("UTF-8"); + + Assert.assertEquals("Streaming and standard formatters should produce identical output", + standardResult, streamingResult); + } + + /** + * Test content type passthrough. + */ + @Test + public void testGetContentType() { + outMsgContext.setProperty(Constants.Configuration.CONTENT_TYPE, "application/json"); + MoshiStreamingMessageFormatter formatter = new MoshiStreamingMessageFormatter(); + String ct = formatter.getContentType(outMsgContext, outputFormat, null); + Assert.assertEquals("application/json", ct); + } + + // --- Helper methods --- + + private OMElement createFaultOMElement() { + OMFactory factory = OMAbstractFactory.getOMFactory(); + OMNamespace ns = factory.createOMNamespace( + "http://schemas.xmlsoap.org/soap/envelope/", "soapenv"); + OMElement fault = factory.createOMElement("Fault", ns); + OMElement faultCode = factory.createOMElement("faultcode", ns, fault); + faultCode.setText("soapenv:Server"); + OMElement faultString = factory.createOMElement("faultstring", ns, fault); + faultString.setText("test.Exception"); + OMElement detail = factory.createOMElement("detail", ns, fault); + detail.setText("moshi fault test"); + return fault; + } + + /** + * Simple POJO for test serialization. + */ + public static class TestData { + public String label; + public int count; + public boolean active; + + public TestData() {} + + public TestData(String label, int count, boolean active) { + this.label = label; + this.count = count; + this.active = active; + } + } +}
