001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.mock;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.rest.server.util.RestUtils.*;
021
022import java.io.*;
023import java.util.*;
024import java.util.concurrent.*;
025import java.util.logging.*;
026
027import org.apache.juneau.commons.inject.*;
028import org.apache.juneau.http.header.NoTrace;
029import org.apache.juneau.marshall.json.*;
030import org.apache.juneau.marshall.parser.*;
031import org.apache.juneau.rest.client.*;
032import org.apache.juneau.rest.client.RestRequest;
033import org.apache.juneau.rest.server.*;
034import org.apache.juneau.rest.server.logging.*;
035
036import jakarta.servlet.*;
037
038/**
039 * Next-generation mock REST client that routes requests in-process to a Juneau {@link RestContext}.
040 *
041 * <p>
042 * Use this class to write serverless unit tests for {@link Rest @Rest}-annotated
043 * resource classes.  Requests are dispatched directly to the {@link RestContext} without any network activity,
044 * making tests fast and self-contained.
045 *
046 * <h5 class='section'>Example:</h5>
047 * <p class='bjava'>
048 *    <ja>@Rest</ja>
049 *    <jk>public class</jk> MyResource {
050 *       <ja>@RestGet</ja>(<js>"/hello"</js>)
051 *       <jk>public</jk> String hello() { <jk>return</jk> <js>"Hello!"</js>; }
052 *    }
053 *
054 *    <ja>@Test</ja>
055 *    <jk>void</jk> test() <jk>throws</jk> Exception {
056 *       <jk>try</jk> (<jv>client</jv> = MockRestClient.<jsm>create</jsm>(MyResource.<jk>class</jk>)) {
057 *          var <jv>body</jv> = <jv>client</jv>.get(<js>"/hello"</js>).run().asString();
058 *          assertEquals(<js>"\"Hello!\""</js>, <jv>body</jv>);
059 *       }
060 *    }
061 * </p>
062 *
063 * <h5 class='section'>See Also:</h5><ul>
064 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/NextGenRestClient">Next-Generation REST Client</a>
065 * </ul>
066 *
067 * @since 9.2.1
068 */
069@SuppressWarnings({
070   "resource" // RestClient is closed by MockRestClient.close(); transport is owned by the client
071})
072public final class MockRestClient implements Closeable {
073
074   private static final Map<Class<?>,RestContext> restContextCache = new ConcurrentHashMap<>();
075
076   private final RestClient client;
077
078   private MockRestClient(RestClient client) {
079      this.client = client;
080   }
081
082   /**
083    * Creates an {@link MockRestClient} backed by the given REST bean or bean class.
084    *
085    * <p>
086    * {@link RestContext} instances are cached per class for performance when a {@link Class} is passed;
087    * a supplied bean instance always gets its own context.
088    *
089    * @param impl
090    *    The REST bean or bean class annotated with {@link Rest @Rest}.
091    *    If a {@link Class}, it must have a no-arg constructor.
092    * @return A new client. Never <jk>null</jk>.
093    */
094   public static MockRestClient create(Object impl) {
095      return builder(impl).build();
096   }
097
098   /**
099    * Returns a new {@link Builder} for the given REST bean or bean class.
100    *
101    * @param impl
102    *    The REST bean or bean class annotated with {@link Rest @Rest}.
103    *    If a {@link Class}, it must have a no-arg constructor.
104    * @return A new builder. Never <jk>null</jk>.
105    */
106   public static Builder builder(Object impl) {
107      return new Builder(impl);
108   }
109
110   /**
111    * Creates a GET request to the given path.
112    *
113    * @param path The request path (relative to the resource's servlet path). Must not be <jk>null</jk>.
114    * @return A new {@link RestRequest}. Never <jk>null</jk>.
115    */
116   public RestRequest get(String path) { return client.get(path); }
117
118   /**
119    * Creates a POST request to the given path.
120    *
121    * @param path The request path. Must not be <jk>null</jk>.
122    * @return A new {@link RestRequest}. Never <jk>null</jk>.
123    */
124   public RestRequest post(String path) { return client.post(path); }
125
126   /**
127    * Creates a PUT request to the given path.
128    *
129    * @param path The request path. Must not be <jk>null</jk>.
130    * @return A new {@link RestRequest}. Never <jk>null</jk>.
131    */
132   public RestRequest put(String path) { return client.put(path); }
133
134   /**
135    * Creates a PATCH request to the given path.
136    *
137    * @param path The request path. Must not be <jk>null</jk>.
138    * @return A new {@link RestRequest}. Never <jk>null</jk>.
139    */
140   public RestRequest patch(String path) { return client.patch(path); }
141
142   /**
143    * Creates a DELETE request to the given path.
144    *
145    * @param path The request path. Must not be <jk>null</jk>.
146    * @return A new {@link RestRequest}. Never <jk>null</jk>.
147    */
148   public RestRequest delete(String path) { return client.delete(path); }
149
150   /**
151    * Creates a HEAD request to the given path.
152    *
153    * @param path The request path. Must not be <jk>null</jk>.
154    * @return A new {@link RestRequest}. Never <jk>null</jk>.
155    */
156   public RestRequest head(String path) { return client.head(path); }
157
158   /**
159    * Creates a request with the given HTTP method and path.
160    *
161    * @param method The HTTP method. Must not be <jk>null</jk>.
162    * @param path The request path. Must not be <jk>null</jk>.
163    * @return A new {@link RestRequest}. Never <jk>null</jk>.
164    */
165   public RestRequest request(String method, String path) { return client.request(method, path); }
166
167   /**
168    * Returns the underlying {@link RestClient}.
169    *
170    * @return The client. Never <jk>null</jk>.
171    */
172   public RestClient getClient() { return client; }
173
174   @Override /* Closeable */
175   public void close() throws IOException {
176      client.close();
177   }
178
179   // -----------------------------------------------------------------------------------------------------------------
180   // Inner types
181   // -----------------------------------------------------------------------------------------------------------------
182
183   /**
184    * Fluent builder for {@link MockRestClient}.
185    *
186    * @since 9.2.1
187    */
188   public static final class Builder implements BeanStoreOverridable<Builder> {
189
190      private final Object impl;
191      private String contextPath;
192      private BeanStore overridingBeanStore;
193      private Parser defaultParser;
194      private boolean noTrace;
195
196      private Builder(Object impl) {
197         this.impl = impl;
198      }
199
200      /**
201       * Sets the servlet context path (e.g. {@code "/myapp"}).
202       *
203       * @param value The context path. May be <jk>null</jk>.
204       * @return This object.
205       */
206      public Builder contextPath(String value) {
207         contextPath = toValidContextPath(value);
208         return this;
209      }
210
211      /**
212       * Installs a {@link BeanStore} as the {@code overridingParent} of the {@link RestContext}'s
213       * bean store, so test-time overrides (e.g. {@code TestBeanStore}) resolve at tier 1 of the
214       * lookup chain &mdash; above the resource's local {@code @Bean} factory entries.
215       *
216       * <p>
217       * Setting this disables the per-resource {@link RestContext} cache for the duration of this
218       * build, so the overlay is not silently shared with a previously-cached instance.
219       *
220       * <h5 class='section'>Example:</h5>
221       * <p class='bjava'>
222       *    <jk>var</jk> <jv>overlay</jv> = <jk>new</jk> TestBeanStore().override(MyService.<jk>class</jk>, mockSvc);
223       *    <jk>try</jk> (<jv>client</jv> = MockRestClient.<jsm>builder</jsm>(MyResource.<jk>class</jk>)
224       *          .overridingBeanStore(<jv>overlay</jv>)
225       *          .build()) {
226       *       <jv>client</jv>.get(<js>"/widgets/1"</js>).run().assertStatus().is(200);
227       *    }
228       * </p>
229       *
230       * @param value The override layer. Can be <jk>null</jk> to clear a previously-set value.
231       * @return This object.
232       *
233       * @since 10.0.0
234       */
235      @Override
236      public Builder overridingBeanStore(BeanStore value) {
237         overridingBeanStore = value;
238         return this;
239      }
240
241      /**
242       * Sets an explicit default parser used when the response {@code Content-Type} matches no registered parser.
243       *
244       * <p>
245       * Setting this also causes the client to advertise the parser's media types in the {@code Accept} header,
246       * so the server must be able to produce that content type.  Omit this (the default) when the target
247       * resource does not have a JSON serializer configured.
248       *
249       * <h5 class='section'>Example:</h5>
250       * <p class='bjava'>
251       *    <jk>try</jk> (<jv>client</jv> = MockRestClient.<jsm>builder</jsm>(MyJsonResource.<jk>class</jk>)
252       *          .defaultParser(JsonParser.<jsf>DEFAULT</jsf>)
253       *          .build()) {
254       *       <jk>var</jk> <jv>api</jv> = <jv>client</jv>.getClient().remote(MyApi.<jk>class</jk>);
255       *       <jv>api</jv>.someMethod();
256       *    }
257       * </p>
258       *
259       * @param value The parser. May be <jk>null</jk>.
260       * @return This object.
261       */
262      public Builder defaultParser(Parser value) {
263         defaultParser = value;
264         return this;
265      }
266
267      /**
268       * Sends a <c>No-Trace: true</c> header with every request, so the server leaves the stack trace of a failed
269       * call out of its log record.
270       *
271       * <p>
272       * Opt-in only; no other builder option turns it on. The one-line access record is still logged.  To do the
273       * same for a single call, use {@link RestRequest#noTrace()}.
274       *
275       * <h5 class='section'>Example:</h5>
276       * <p class='bjava'>
277       *    <jk>try</jk> (<jv>client</jv> = MockRestClient.<jsm>builder</jsm>(MyResource.<jk>class</jk>).noTrace().build()) {
278       *       <jv>client</jv>.get(<js>"/missing"</js>).run().assertStatus().is(404);
279       *    }
280       * </p>
281       *
282       * @return This object.
283       */
284      public Builder noTrace() {
285         noTrace = true;
286         return this;
287      }
288
289      /**
290       * Builds and returns the {@link MockRestClient}.
291       *
292       * @return A new instance. Never <jk>null</jk>.
293       */
294      @SuppressWarnings({
295         "java:S112" // Re-wrap as RuntimeException - build() wraps arbitrary checked exceptions from reflection (newInstance) and RestContext init/postInit; no specific type fits and the fluent build() method cannot declare checked exceptions.
296      })
297      public MockRestClient build() {
298         try {
299            var isClass = impl instanceof Class<?>;
300            var overlay = overridingBeanStore;
301            // Only class-based contexts are cached; a caller-supplied instance always gets its own context.
302            var cacheable = isClass && overlay == null;
303            RestContext restContext = cacheable ? restContextCache.get(impl) : null;
304            if (restContext == null) {
305               var o = isClass ? ((Class<?>)impl).getDeclaredConstructor().newInstance() : impl;
306               restContext = new RestContext(new RestContext.Args(o.getClass(), null, null, () -> o, "", bs -> {
307                  bs.addBeanType(RestDebugFormatter.class, BasicTestRestDebugFormatter.class);
308               }, overlay, null, RestContext.ContextKind.ROOT)).postInit().postInitChildFirst();
309               if (cacheable)
310                  restContextCache.put((Class<?>)impl, restContext);
311            }
312
313            var servletPath = toValidContextPath(restContext.getFullPath());
314            var fullContextPath = emptyIfNull(contextPath) + emptyIfNull(servletPath);
315
316         var transport = new JuneauRestTransport(restContext, emptyIfNull(contextPath), servletPath);
317         var clientBuilder = RestClient.builder()
318            .transport(transport)
319            .rootUrl("http://localhost" + fullContextPath)
320            .defaultSerializer(JsonSerializer.DEFAULT);
321         if (defaultParser != null)
322            clientBuilder.defaultParser(defaultParser);
323         if (noTrace)
324            clientBuilder.header(NoTrace.NAME, "true");
325         var ngClient = clientBuilder.build();
326
327            return new MockRestClient(ngClient);
328         } catch (Exception e) {
329            throw rex(e, "Could not initialize MockRestClient");
330         }
331      }
332
333      private static String emptyIfNull(String s) { return s == null ? "" : s; }
334   }
335
336   // -----------------------------------------------------------------------------------------------------------------
337   // Transport implementation
338   // -----------------------------------------------------------------------------------------------------------------
339
340   /**
341    * An {@link HttpTransport} that dispatches requests in-process through a Juneau {@link RestContext}.
342    */
343   private static final class JuneauRestTransport implements HttpTransport {
344
345      private final RestContext restContext;
346      private final Object restObject;
347      private final String contextPath;
348      private final String servletPath;
349
350      JuneauRestTransport(RestContext restContext, String contextPath, String servletPath) {
351         this.restContext = restContext;
352         this.restObject = restContext.getResource();
353         this.contextPath = contextPath;
354         this.servletPath = servletPath;
355      }
356
357      @Override /* HttpTransport */
358      public boolean supportsUrlPolicy() {
359         // Requests are dispatched in-process, directly to the RestContext -- there is no real DNS resolution or
360         // socket connect to pin-on-connect, so the "cannot honor the connect-time contract" fail-closed
361         // rationale does not apply. The deny-private lexical pre-check (RemoteUrlPolicy.requireAllowedUrl)
362         // still runs upstream in the @Remote proxy engine before any request reaches this transport, and this
363         // class's own "http://localhost" root (see MockRestClient.Builder.build()) is never re-checked by that
364         // pre-check -- only an explicit absolute @Url/baseUrl() override on the proxy interface is.
365         return true;
366      }
367
368      @Override /* HttpTransport */
369      public TransportResponse execute(TransportRequest request) throws TransportException {
370         if (request.isSsrfGuardActive())
371            return PolicyEnforcedRedirects.execute(request, this::dispatch);
372         return dispatch(request);
373      }
374
375      private TransportResponse dispatch(TransportRequest request) throws TransportException {
376         try {
377            var method = request.getMethod();
378            var uri = request.getUri().toString();
379
380            var servletReq = MockServletRequest.create(method, uri)
381               .servletPath(servletPath)
382               .contextPath(contextPath);
383
384            for (var h : request.getHeaders())
385               servletReq.header(h.name(), h.value());
386
387            var body = request.getBody();
388            if (body != null) {
389               var baos = new ByteArrayOutputStream();
390               body.writeTo(baos);
391               servletReq.content(baos.toByteArray());
392            }
393
394            var servletRes = MockServletResponse.create();
395
396            var logLevel = servletReq.getLogLevel();
397            if (logLevel != null) {
398               var target = Logger.getLogger(restObject.getClass().getName());
399               var prevLevel = target.getLevel();
400               target.setLevel(logLevel);
401               try {
402                  restContext.execute(restObject, servletReq, servletRes);
403               } finally {
404                  target.setLevel(prevLevel);
405               }
406            } else {
407               restContext.execute(restObject, servletReq, servletRes);
408            }
409
410            var responseBuilder = TransportResponse.builder()
411               .statusCode(servletRes.getStatus())
412               .reasonPhrase(servletRes.getMessage());
413
414            for (var name : servletRes.getHeaderNames())
415               for (var value : servletRes.getHeaders(name))
416                  responseBuilder.header(name, value);
417
418            var content = servletRes.getContent();
419            if (content.length > 0)
420               responseBuilder.body(new ByteArrayInputStream(content));
421
422            return responseBuilder.build();
423
424         } catch (TransportException e) {
425            throw e;
426         } catch (ServletException | IOException e) {
427            throw new TransportException("In-process dispatch failed: " + e.getMessage(), e);
428         }
429      }
430   }
431}