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}