001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import java.util.function.Consumer;
023
024/**
025 * Simulates server behavior of accepting a request and returning a response without touching the network, useful for writing integration tests.
026 * <p>
027 * <a href="https://www.soklet.com/docs/server-sent-events">Server-Sent Event</a> simulation is also supported.
028 * <p>
029 * Instances of {@link Simulator} are made available via {@link com.soklet.Soklet#runSimulator(SokletConfig, Consumer)}.
030 * <p>
031 * Usage example:
032 * <pre>{@code @Test
033 * public void basicIntegrationTest () {
034 *   // Just use your app's existing configuration
035 *   SokletConfig config = obtainMySokletConfig();
036 *
037 *   // With the Simulator, you can issue requests
038 *   // and receive responses just like you would with real servers.
039 *   Soklet.runSimulator(config, (simulator) -> {
040 *     // Construct a request
041 *     Request request = Request.withPath(HttpMethod.GET, "/hello")
042 *       .queryParameters(Map.of("name", Set.of("Mark")))
043 *       .build();
044 *
045 *     // Perform the request and get a handle to the result
046 *     RequestResult result = simulator.performRequest(request);
047 *
048 *     // Verify status code
049 *     Integer expectedCode = 200;
050 *     Integer actualCode = result.getMarshaledResponse().getStatusCode();
051 *     assertEquals(expectedCode, actualCode, "Bad status code");
052 *
053 *     // Now, create a request for an SSE Event Source...
054 *     Request eventSourceRequest = Request.withPath(HttpMethod.GET, "/sse-test")
055 *         .queryParameters(Map.of("signingToken", Set.of("xxx")))
056 *         .build();
057 *
058 *     // ...and perform it and get a handle to the result.
059 *     ServerSentEventRequestResult eventSourceResult =
060 *       simulator.performServerSentEventRequest(eventSourceRequest);
061 *
062 *     // Single-shot latch; we'll wait until a Server-Sent Event comes through
063 *     CountDownLatch eventReceivedLatch = new CountDownLatch(1);
064 *
065 *     // The Simulator provides 3 logical outcomes for SSE connections:
066 *     // * Accepted Handshake (connection stays open)
067 *     // * Rejected Handshake (explicit rejection, connection closed)
068 *     // * Request Failed (implicit rejection, e.g. uncaught exception, connection closed)
069 *     switch (eventSourceResult) {
070 *       // Explicit handshake acceptance
071 *       case HandshakeAccepted handshakeAccepted -> {
072 *         handshakeAccepted.registerEventConsumer((event) -> {
073 *           // Server-Sent Event received: open the latch to end the test
074 *           eventReceivedLatch.countDown();
075 *         });
076 *
077 *         // On a separate thread, broadcast a Server-Sent Event
078 *         new Thread(() -> {
079 *           // ... not shown
080 *         }).start();
081 *       }
082 *
083 *       // Explicit handshake rejection
084 *       case HandshakeRejected handshakeRejected ->
085 *         Assertions.fail("SSE Handshake Rejected: " + handshakeRejected);
086 *
087 *       // Uncaught exception
088 *       case RequestFailed requestFailed ->
089 *         Assertions.fail("SSE Request Failed: " + requestFailed);
090 *     }
091 *
092 *     // Finally, wait a bit for the latch to open
093 *     try {
094 *       eventReceivedLatch.await(5, SECONDS);
095 *     } catch (InterruptedException e) {
096 *       Assertions.fail("Didn't receive a Server-Sent Event in time");
097 *     }
098 *   });
099 * }}</pre>
100 * <p>
101 * Full documentation is available at <a href="https://www.soklet.com/docs/testing">https://www.soklet.com/docs/testing</a>.
102 *
103 * @author <a href="https://www.revetkn.com">Mark Allen</a>
104 */
105public interface Simulator {
106        /**
107         * Given a request that would normally be handled by your standard {@link Server}, process it and return response data (both logical {@link Response}, if present, and the {@link MarshaledResponse} bytes to be sent over the wire) as well as the matching <em>Resource Method</em>, if available.
108         * <p>
109         * To make requests that would normally be handled by your {@link ServerSentEventServer}, use {@link #performServerSentEventRequest(Request)}.
110         *
111         * @param request the standard HTTP request to process
112         * @return the result (logical response, marshaled response, etc.) that corresponds to the request
113         */
114        @NonNull
115        RequestResult performRequest(@NonNull Request request);
116
117        /**
118         * Given a request that would normally be handled by your {@link ServerSentEventServer} (that is, for a <em>Resource Method</em> decorated with the {@link com.soklet.annotation.ServerSentEventSource} annotation), process it and return response data ({@link com.soklet.ServerSentEventRequestResult.HandshakeAccepted}, {@link com.soklet.ServerSentEventRequestResult.HandshakeRejected}, or {@link com.soklet.ServerSentEventRequestResult.RequestFailed});
119         * <p>
120         * To make requests that would normally be handled by your {@link Server}, use {@link #performRequest(Request)}.
121         *
122         * @param request the Server-Sent Event HTTP request to process
123         * @return the result (handshake outcode, etc.) that corresponds to the request
124         */
125        @NonNull
126        ServerSentEventRequestResult performServerSentEventRequest(@NonNull Request request);
127
128        /**
129         * Registers a handler for exceptions thrown by simulated Server-Sent Event consumers.
130         * <p>
131         * This only applies to simulator-mode SSE broadcasts.
132         *
133         * @param onBroadcastError handler for broadcast errors, or {@code null} to clear
134         * @return this simulator
135         */
136        @NonNull
137        default Simulator onBroadcastError(@Nullable Consumer<Throwable> onBroadcastError) {
138                return this;
139        }
140
141        /**
142         * Registers a handler for exceptions thrown by simulated Server-Sent Event unicast consumers.
143         * <p>
144         * This only applies to simulator-mode SSE unicast deliveries (including client initializers).
145         *
146         * @param onUnicastError handler for unicast errors, or {@code null} to clear
147         * @return this simulator
148         */
149        @NonNull
150        default Simulator onUnicastError(@Nullable Consumer<Throwable> onUnicastError) {
151                return this;
152        }
153}