diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java index 0880440d02..37256c15ef 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java @@ -44,6 +44,8 @@ import java.util.TimeZone; import java.util.stream.Collectors; import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; import jakarta.servlet.DispatcherType; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletConnection; @@ -920,7 +922,19 @@ public class MockHttpServletRequest implements HttpServletRequest { public AsyncContext startAsync(ServletRequest request, @Nullable ServletResponse response) { Assert.state(this.asyncSupported, "Async not supported"); this.asyncStarted = true; - this.asyncContext = new MockAsyncContext(request, response); + MockAsyncContext newAsyncContext = new MockAsyncContext(request, response); + if (this.asyncContext != null) { + try { + AsyncEvent startEvent = new AsyncEvent(newAsyncContext); + for (AsyncListener asyncListener : this.asyncContext.getListeners()) { + asyncListener.onStartAsync(startEvent); + } + } + catch (IOException ex) { + // ignore failures + } + } + this.asyncContext = newAsyncContext; return this.asyncContext; } diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java index 963846482b..2dd438f31b 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java @@ -30,6 +30,9 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.Test; @@ -663,6 +666,44 @@ class MockHttpServletRequestTests { request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE)); } + @Test + void shouldRejectAsyncStartsIfUnsupported() { + assertThat(request.isAsyncStarted()).isFalse(); + assertThatIllegalStateException().isThrownBy(request::startAsync); + } + + @Test + void startAsyncShouldUpdateRequestState() { + assertThat(request.isAsyncStarted()).isFalse(); + request.setAsyncSupported(true); + AsyncContext asyncContext = request.startAsync(); + assertThat(request.isAsyncStarted()).isTrue(); + } + + @Test + void shouldNotifyAsyncListeners() { + request.setAsyncSupported(true); + AsyncContext asyncContext = request.startAsync(); + TestAsyncListener testAsyncListener = new TestAsyncListener(); + asyncContext.addListener(testAsyncListener); + asyncContext.complete(); + assertThat(testAsyncListener.events).hasSize(1); + assertThat(testAsyncListener.events.get(0)).extracting("name").isEqualTo("onComplete"); + } + + @Test + void shouldNotifyAsyncListenersWhenNewAsyncStarted() { + request.setAsyncSupported(true); + AsyncContext asyncContext = request.startAsync(); + TestAsyncListener testAsyncListener = new TestAsyncListener(); + asyncContext.addListener(testAsyncListener); + AsyncContext newAsyncContext = request.startAsync(); + assertThat(testAsyncListener.events).hasSize(1); + ListenerEvent listenerEvent = testAsyncListener.events.get(0); + assertThat(listenerEvent).extracting("name").isEqualTo("onStartAsync"); + assertThat(listenerEvent.event.getAsyncContext()).isEqualTo(newAsyncContext); + } + private void assertEqualEnumerations(Enumeration enum1, Enumeration enum2) { int count = 0; while (enum1.hasMoreElements()) { @@ -672,4 +713,31 @@ class MockHttpServletRequestTests { } } + static class TestAsyncListener implements AsyncListener { + + List events = new ArrayList<>(); + + @Override + public void onComplete(AsyncEvent asyncEvent) throws IOException { + this.events.add(new ListenerEvent("onComplete", asyncEvent)); + } + + @Override + public void onTimeout(AsyncEvent asyncEvent) throws IOException { + this.events.add(new ListenerEvent("onTimeout", asyncEvent)); + } + + @Override + public void onError(AsyncEvent asyncEvent) throws IOException { + this.events.add(new ListenerEvent("onError", asyncEvent)); + } + + @Override + public void onStartAsync(AsyncEvent asyncEvent) throws IOException { + this.events.add(new ListenerEvent("onStartAsync", asyncEvent)); + } + } + + record ListenerEvent(String name, AsyncEvent event) {} + } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletRequest.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletRequest.java index 59eb0b977c..31ac9d1482 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletRequest.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletRequest.java @@ -44,6 +44,8 @@ import java.util.TimeZone; import java.util.stream.Collectors; import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; import jakarta.servlet.DispatcherType; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletConnection; @@ -921,7 +923,19 @@ public class MockHttpServletRequest implements HttpServletRequest { public AsyncContext startAsync(ServletRequest request, @Nullable ServletResponse response) { Assert.state(this.asyncSupported, "Async not supported"); this.asyncStarted = true; - this.asyncContext = new MockAsyncContext(request, response); + MockAsyncContext newAsyncContext = new MockAsyncContext(request, response); + if (this.asyncContext != null) { + try { + AsyncEvent startEvent = new AsyncEvent(newAsyncContext); + for (AsyncListener asyncListener : this.asyncContext.getListeners()) { + asyncListener.onStartAsync(startEvent); + } + } + catch (IOException ex) { + // ignore failures + } + } + this.asyncContext = newAsyncContext; return this.asyncContext; }