001/*
002 * Copyright 2022-2025 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 javax.annotation.Nonnull;
020import java.util.function.Consumer;
021
022/**
023 * Simulates server behavior of accepting a request and returning a response without touching the network, useful for writing integration tests.
024 * <p>
025 * <a href="https://www.soklet.com/docs/server-sent-events">Server-Sent Event</a> simulation is also supported.
026 * <p>
027 * Instances of {@link Simulator} are made available via {@link com.soklet.Soklet#runSimulator(SokletConfig, Consumer)}.
028 * <p>
029 * Usage example:
030 * <pre>{@code @Test
031 * public void basicIntegrationTest () {
032 *   // Just use your app's existing configuration
033 *   SokletConfig config = obtainMySokletConfig();
034 *
035 *   // With the Simulator, you can issue requests
036 *   // and receive responses just like you would with real servers.
037 *   Soklet.runSimulator(config, (simulator) -> {
038 *     // Construct a request
039 *     Request request = Request.withPath(HttpMethod.GET, "/hello")
040 *       .queryParameters(Map.of("name", Set.of("Mark")))
041 *       .build();
042 *
043 *     // Perform the request and get a handle to the result
044 *     RequestResult result = simulator.performRequest(request);
045 *
046 *     // Verify status code
047 *     Integer expectedCode = 200;
048 *     Integer actualCode = result.getMarshaledResponse().getStatusCode();
049 *     assertEquals(expectedCode, actualCode, "Bad status code");
050 *
051 *     // Now, create a request for an SSE Event Source...
052 *     Request eventSourceRequest = Request.withPath(HttpMethod.GET, "/sse-test")
053 *         .queryParameters(Map.of("signingToken", Set.of("xxx")))
054 *         .build();
055 *
056 *     // ...and perform it and get a handle to the result.
057 *     ServerSentEventRequestResult eventSourceResult =
058 *       simulator.performServerSentEventRequest(eventSourceRequest);
059 *
060 *     // Single-shot latch; we'll wait until a Server-Sent Event comes through
061 *     CountDownLatch eventReceivedLatch = new CountDownLatch(1);
062 *
063 *     // The Simulator provides 3 logical outcomes for SSE connections:
064 *     // * Accepted Handshake (connection stays open)
065 *     // * Rejected Handshake (explicit rejection, connection closed)
066 *     // * Request Failed (implicit rejection, e.g. uncaught exception, connection closed)
067 *     switch (eventSourceResult) {
068 *       // Explicit handshake acceptance
069 *       case HandshakeAccepted handshakeAccepted -> {
070 *         handshakeAccepted.registerEventConsumer((event) -> {
071 *           // Server-Sent Event received: open the latch to end the test
072 *           eventReceivedLatch.countDown();
073 *         });
074 *
075 *         // On a separate thread, broadcast a Server-Sent Event
076 *         new Thread(() -> {
077 *           // ... not shown
078 *         }).start();
079 *       }
080 *
081 *       // Explicit handshake rejection
082 *       case HandshakeRejected handshakeRejected ->
083 *         Assertions.fail("SSE Handshake Rejected: " + handshakeRejected);
084 *
085 *       // Uncaught exception
086 *       case RequestFailed requestFailed ->
087 *         Assertions.fail("SSE Request Failed: " + requestFailed);
088 *     }
089 *
090 *     // Finally, wait a bit for the latch to open
091 *     try {
092 *       eventReceivedLatch.await(5, SECONDS);
093 *     } catch (InterruptedException e) {
094 *       Assertions.fail("Didn't receive a Server-Sent Event in time");
095 *     }
096 *   });
097 * }}</pre>
098 * <p>
099 * Full documentation is available at <a href="https://www.soklet.com/docs/testing">https://www.soklet.com/docs/testing</a>.
100 *
101 * @author <a href="https://www.revetkn.com">Mark Allen</a>
102 */
103public interface Simulator {
104        /**
105         * 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.
106         * <p>
107         * To make requests that would normally be handled by your {@link ServerSentEventServer}, use {@link #performServerSentEventRequest(Request)}.
108         *
109         * @param request the standard HTTP request to process
110         * @return the result (logical response, marshaled response, etc.) that corresponds to the request
111         */
112        @Nonnull
113        RequestResult performRequest(@Nonnull Request request);
114
115        /**
116         * 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});
117         * <p>
118         * To make requests that would normally be handled by your {@link Server}, use {@link #performRequest(Request)}.
119         *
120         * @param request the Server-Sent Event HTTP request to process
121         * @return the result (handshake outcode, etc.) that corresponds to the request
122         */
123        @Nonnull
124        ServerSentEventRequestResult performServerSentEventRequest(@Nonnull Request request);
125}