diff --git a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/SseEventUpdateResource.java b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/SseEventUpdateResource.java new file mode 100644 index 0000000000..1db6d50e0f --- /dev/null +++ b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/SseEventUpdateResource.java @@ -0,0 +1,67 @@ +package io.quarkus.ts.http.advanced.reactive; + +import java.util.Arrays; +import java.util.concurrent.locks.LockSupport; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.sse.OutboundSseEvent; +import jakarta.ws.rs.sse.Sse; +import jakarta.ws.rs.sse.SseEventSink; +import jakarta.ws.rs.sse.SseEventSource; + +import org.eclipse.microprofile.config.ConfigProvider; + +@Path("sse") +public class SseEventUpdateResource { + public static final String DATA_VALUE = "random data value"; + @Context + Sse sse; + + @GET + @Path("client-update") + @Produces(MediaType.TEXT_PLAIN) + public Response clientUpdate() { + String host = ConfigProvider.getConfig().getValue("quarkus.http.host", String.class); + int port = ConfigProvider.getConfig().getValue("quarkus.http.port", Integer.class); + StringBuilder eventCapture = new StringBuilder(); + + WebTarget target = ClientBuilder.newClient().target("http://" + host + ":" + port + "/api/sse/server-update"); + SseEventSource eventSource = SseEventSource.target(target).build(); + eventSource.register(ev -> { + eventCapture.append("event: name=").append(ev.getName()).append(" data={").append(ev.readData()) + .append("} and is empty: ").append(ev.isEmpty()).append("\n"); + }, thr -> { + eventCapture.append("Error: ").append(thr.getMessage()).append("\n") + .append(Arrays.toString(thr.getStackTrace())); + }); + + eventSource.open(); + LockSupport.parkNanos(5_000_000_000L); + eventSource.close(); + + return Response.ok(eventCapture).build(); + } + + @GET + @Path("server-update") + @Produces(MediaType.SERVER_SENT_EVENTS) + public void updates(@Context SseEventSink eventSink) { + eventSink.send(createEvent("NON EMPTY", DATA_VALUE)); + LockSupport.parkNanos(1_000_000_000L); + eventSink.send(createEvent("EMPTY", "")); + } + + private OutboundSseEvent createEvent(String name, String data) { + return sse.newEventBuilder() + .name(name) + .data(data) + .build(); + } +} diff --git a/http/http-advanced-reactive/src/main/resources/application.properties b/http/http-advanced-reactive/src/main/resources/application.properties index bc40751d27..a7cdf2ac11 100644 --- a/http/http-advanced-reactive/src/main/resources/application.properties +++ b/http/http-advanced-reactive/src/main/resources/application.properties @@ -60,6 +60,8 @@ quarkus.keycloak.policy-enforcer.paths.grpc.path=/api/grpc/* quarkus.keycloak.policy-enforcer.paths.grpc.enforcement-mode=DISABLED quarkus.keycloak.policy-enforcer.paths.client.path=/api/client/* quarkus.keycloak.policy-enforcer.paths.client.enforcement-mode=DISABLED +quarkus.keycloak.policy-enforcer.paths.sse.path=/api/sse/* +quarkus.keycloak.policy-enforcer.paths.sse.enforcement-mode=DISABLED quarkus.oidc.client-id=test-application-client quarkus.oidc.credentials.secret=test-application-client-secret # tolerate 1 minute of clock skew between the Keycloak server and the application diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java index 5b01aa2537..5f7f905a31 100644 --- a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java @@ -12,6 +12,7 @@ import static io.quarkus.ts.http.advanced.reactive.MultipleResponseSerializersResource.MULTIPLE_RESPONSE_SERIALIZERS_PATH; import static io.quarkus.ts.http.advanced.reactive.NinetyNineBottlesOfBeerResource.QUARKUS_PLATFORM_VERSION_LESS_THAN_2_8_3; import static io.quarkus.ts.http.advanced.reactive.NinetyNineBottlesOfBeerResource.QUARKUS_PLATFORM_VERSION_LESS_THAN_2_8_3_VAL; +import static io.quarkus.ts.http.advanced.reactive.SseEventUpdateResource.DATA_VALUE; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static jakarta.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM; import static jakarta.ws.rs.core.MediaType.APPLICATION_XML; @@ -71,6 +72,7 @@ import io.quarkus.example.StreamingGrpc; import io.quarkus.test.bootstrap.Protocol; import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.annotations.DisabledOnNative; import io.quarkus.test.scenarios.annotations.EnabledOnQuarkusVersion; import io.quarkus.ts.http.advanced.reactive.clients.HttpVersionClientService; import io.quarkus.ts.http.advanced.reactive.clients.HttpVersionClientServiceAsync; @@ -427,6 +429,18 @@ public void constraintsExist() throws JsonProcessingException { Assertions.assertEquals("^[A-Za-z]+$", validation.get("pattern").asText()); } + @DisplayName("SSE check for response values and it's availability") + @Test + @Disabled("https://github.com/quarkusio/quarkus/issues/37033") + @DisabledOnNative(reason = "https://github.com/quarkusio/quarkus/issues/36986") + void testSseResponseAndAvailability() { + getApp().given() + .get(ROOT_PATH + "/sse/client-update") + .then().statusCode(SC_OK) + .body(containsString(String.format("event: name=NON EMPTY data={%s} and is empty: false", DATA_VALUE)), + containsString("event: name=EMPTY data={} and is empty: true")); + } + private void assertAcceptedMediaTypeEqualsResponseBody(String acceptedMediaType) { getApp() .given()