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}