View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2016 ForgeRock AS.
15   */
16  
17  package org.forgerock.http.bindings;
18  
19  import static io.swagger.models.Scheme.HTTP;
20  import static io.swagger.models.Scheme.HTTPS;
21  import static java.lang.String.format;
22  import static java.util.Arrays.asList;
23  import static org.assertj.core.api.Assertions.assertThat;
24  import static org.assertj.core.api.Assertions.fail;
25  import static org.forgerock.http.Applications.describedHttpApplication;
26  import static org.forgerock.http.Applications.simpleHttpApplication;
27  import static org.forgerock.http.filter.TransactionIdInboundFilter.SYSPROP_TRUST_TRANSACTION_HEADER;
28  import static org.forgerock.http.handler.Handlers.chainOf;
29  import static org.forgerock.http.protocol.Response.newResponsePromise;
30  import static org.forgerock.json.JsonValue.json;
31  import static org.forgerock.json.test.assertj.AssertJJsonValueAssert.assertThat;
32  import static org.mockito.Mockito.mock;
33  import static org.mockito.Mockito.verify;
34  import static org.mockito.Mockito.verifyNoMoreInteractions;
35  import static org.mockito.Mockito.when;
36  
37  import java.util.List;
38  
39  import org.assertj.core.api.SoftAssertionError;
40  import org.assertj.core.api.SoftAssertions;
41  import org.forgerock.http.ApiProducer;
42  import org.forgerock.http.Client;
43  import org.forgerock.http.DescribedHttpApplication;
44  import org.forgerock.http.Handler;
45  import org.forgerock.http.HttpApplication;
46  import org.forgerock.http.HttpApplicationException;
47  import org.forgerock.http.handler.DescribableHandler;
48  import org.forgerock.http.handler.HttpClientHandler;
49  import org.forgerock.http.header.CookieHeader;
50  import org.forgerock.http.header.SetCookieHeader;
51  import org.forgerock.http.protocol.Cookie;
52  import org.forgerock.http.protocol.Request;
53  import org.forgerock.http.protocol.Response;
54  import org.forgerock.http.protocol.Status;
55  import org.forgerock.http.routing.UriRouterContext;
56  import org.forgerock.http.session.Session;
57  import org.forgerock.http.session.SessionContext;
58  import org.forgerock.http.swagger.OpenApiRequestFilter;
59  import org.forgerock.http.swagger.SwaggerApiProducer;
60  import org.forgerock.services.TransactionId;
61  import org.forgerock.services.context.ClientContext;
62  import org.forgerock.services.context.Context;
63  import org.forgerock.services.context.TransactionIdContext;
64  import org.forgerock.util.promise.NeverThrowsException;
65  import org.forgerock.util.promise.Promise;
66  import org.testng.annotations.AfterMethod;
67  import org.testng.annotations.BeforeMethod;
68  import org.testng.annotations.Test;
69  
70  import io.swagger.models.Info;
71  import io.swagger.models.Operation;
72  import io.swagger.models.Path;
73  import io.swagger.models.Swagger;
74  
75  /**
76   * A test class for CHF bindings.
77   */
78  public abstract class BindingTest {
79  
80      private int port;
81  
82      /**
83       * Create a server to bind a CHF application to in tests.
84       */
85      protected abstract void createServer();
86  
87      /**
88       * Stop the server.
89       * @throws Exception In case of error.
90       */
91      protected abstract void stopServer() throws Exception;
92  
93      /**
94       * Start the server.
95       * @throws Exception In case of error.
96       * @return The port number the server is listening on.
97       */
98      protected abstract int startServer() throws Exception;
99  
100     /**
101      * Add an application to the server. The application should be added to the root path.
102      * @param application The application.
103      * @throws Exception In case of failure.
104      */
105     protected abstract void addApplication(HttpApplication application) throws Exception;
106 
107     /**
108      * Set up for tests.
109      * @throws Exception In case of failure.
110      */
111     @BeforeMethod
112     public final void setUp() throws Exception {
113         createServer();
114     }
115 
116     /**
117      * Tear down after tests.
118      * @throws Exception In case of failure.
119      */
120     @AfterMethod
121     public final void tearDown() throws Exception {
122         stopServer();
123         port = 0;
124     }
125 
126     /**
127      * Test the application lifecycle for a described application.
128      * @throws Exception In case of failure.
129      */
130     @Test
131     public void testDescribedHttpApplicationLifecycle() throws Exception {
132         final DescribedHttpApplication application = mock(DescribedHttpApplication.class);
133         when(application.start()).thenReturn(mock(DescribableHandler.class));
134         addApplication(application);
135         port = startServer();
136         verify(application).getBufferFactory();
137         verify(application).start();
138         verify(application).getApiProducer();
139         verifyNoMoreInteractions(application);
140 
141         stopServer();
142         verify(application).stop();
143         verifyNoMoreInteractions(application);
144     }
145 
146     /**
147      * Test the application lifecycle.
148      * @throws Exception In case of failure.
149      */
150     @Test
151     public void testHttpApplicationLifecycle() throws Exception {
152         final HttpApplication application = mock(HttpApplication.class);
153         addApplication(application);
154         port = startServer();
155         verify(application).getBufferFactory();
156         verify(application).start();
157         verifyNoMoreInteractions(application);
158 
159         stopServer();
160         verify(application).stop();
161         verifyNoMoreInteractions(application);
162     }
163 
164     /**
165      * Test 500 errors are returned if the application doesn't start correctly.
166      * @throws Exception In case of failure.
167      */
168     @Test
169     public void testAnswerWith500IfHttpApplicationFailedToStart() throws Exception {
170         final HttpApplication application = mock(HttpApplication.class);
171         addApplication(application);
172 
173         when(application.start()).thenThrow(new HttpApplicationException("Unable to start the HttpApplication"));
174         port = startServer();
175 
176         try (final HttpClientHandler handler = new HttpClientHandler()) {
177             final Client client = new Client(handler);
178             final Request request = new Request()
179                     .setMethod("GET")
180                     .setUri(format("http://localhost:%d/test", port));
181             final Response response = client.send(request).get();
182             assertThat(response.getStatus()).isEqualTo(Status.INTERNAL_SERVER_ERROR);
183         }
184     }
185 
186     /**
187      * Test a request.
188      * @throws Exception In case of failure.
189      */
190     @Test
191     public void testRequest() throws Exception {
192         HttpApplication application = simpleHttpApplication(new TestHandler(), null);
193         addApplication(application);
194         port = startServer();
195 
196         try (final HttpClientHandler handler = new HttpClientHandler()) {
197             final Client client = new Client(handler);
198             final Request request = new Request()
199                     .setMethod("POST")
200                     .setUri(format("http://localhost:%d/test", port));
201             request.getHeaders().add("X-WhateverHeader", "Whatever Value");
202             request.getEntity().setString("Hello");
203 
204             final Response response = client.send(request).get();
205             assertThat(response.getEntity().toString()).isEqualTo("HELLO");
206             assertThat(response.getHeaders().get("X-WhateverHeader").getFirstValue()).isEqualTo("Whatever Value");
207         }
208     }
209 
210     /**
211      * Test an API request.
212      * @throws Exception In case of failure.
213      */
214     @Test
215     public void testRequestApi() throws Exception {
216         DescribableHandler testHandler = chainOf(new TestHandler(), new OpenApiRequestFilter());
217         HttpApplication application = describedHttpApplication(testHandler, null,
218                 new SwaggerApiProducer(new Info(), "", "", asList(HTTP, HTTPS)));
219         addApplication(application);
220 
221         port = startServer();
222 
223         try (final HttpClientHandler handler = new HttpClientHandler()) {
224             final Client client = new Client(handler);
225             final Request request = new Request()
226                     .setMethod("GET")
227                     .setUri(format("http://localhost:%d/test?_api", port));
228             request.getHeaders().add("X-WhateverHeader", "Whatever Value");
229 
230             final Response response = client.send(request).get();
231             assertThat(json(response.getEntity().getJson())).isObject()
232                     .hasObject("paths")
233                     .hasObject("test")
234                     .hasObject("post")
235                     .hasArray("produces")
236                     .containsExactly("text/plain");
237         }
238     }
239 
240     /**
241      * Test the session.
242      * @throws Exception In case of failure.
243      */
244     @Test
245     public void testSession() throws Exception {
246         HttpApplication application = simpleHttpApplication(new TestSessionHandler(), null);
247         addApplication(application);
248         port = startServer();
249 
250         try (final HttpClientHandler handler = new HttpClientHandler()) {
251             final Client client = new Client(handler);
252             final Request populate = new Request()
253                     .setMethod("POST")
254                     .setUri(format("http://localhost:%d/populate", port));
255 
256             Response response = client.send(populate).get();
257             assertThat(response.getStatus()).isEqualTo(Status.OK);
258             final List<Cookie> sessionCookie = response.getHeaders().get(SetCookieHeader.class).getCookies();
259 
260             final Request check = new Request()
261                     .setMethod("POST")
262                     .setUri(format("http://localhost:%d/check", port));
263             check.getHeaders().put(new CookieHeader(sessionCookie));
264 
265             response = client.send(check).get();
266             assertThat(response.getEntity().toString()).isEqualTo("OK");
267         }
268     }
269 
270     /**
271      * Test the presence of the transaction context and the propagation of the transactionId.
272      * @throws Exception In case of failure.
273      */
274     @Test
275     public void testTransactionContext() throws Exception {
276         Handler handler = new Handler() {
277             @Override
278             public Promise<Response, NeverThrowsException> handle(Context context, Request request) {
279                 if (!context.containsContext(TransactionIdContext.class)) {
280                     return newResponsePromise(new Response(Status.EXPECTATION_FAILED));
281                 }
282                 Response response = new Response(Status.OK);
283                 TransactionId transactionId = context.asContext(TransactionIdContext.class).getTransactionId();
284                 response.setEntity(transactionId.getValue());
285                 return newResponsePromise(response);
286             }
287         };
288         HttpApplication application = simpleHttpApplication(handler, null);
289         addApplication(application);
290 
291         String previousPropertyValue = System.setProperty(SYSPROP_TRUST_TRANSACTION_HEADER, "true");
292         port = startServer();
293 
294         try (final HttpClientHandler httpClientHandler = new HttpClientHandler()) {
295             final Client client = new Client(httpClientHandler);
296             final Request request = new Request()
297                     .setMethod("GET")
298                     .setUri(format("http://localhost:%d/", port));
299             request.getHeaders().add("X-ForgeRock-TransactionId", "test-transaction-id");
300 
301             Response response = client.send(request).get();
302             assertThat(response.getStatus()).isEqualTo(Status.OK);
303             assertThat(response.getEntity().toString()).isEqualTo("test-transaction-id");
304         } finally {
305             if (previousPropertyValue == null) {
306                 System.clearProperty(SYSPROP_TRUST_TRANSACTION_HEADER);
307             } else {
308                 System.setProperty(SYSPROP_TRUST_TRANSACTION_HEADER, previousPropertyValue);
309             }
310         }
311     }
312 
313     private final class TestHandler implements DescribableHandler {
314 
315         @Override
316         public Promise<Response, NeverThrowsException> handle(Context context, Request request) {
317             final SoftAssertions softly = new SoftAssertions();
318             try {
319                 softly.assertThat(request.getMethod()).isEqualTo("POST");
320                 softly.assertThat(request.getUri().getPath()).isEqualTo("/test");
321                 softly.assertThat(request.getEntity().toString()).isEqualTo("Hello");
322                 softly.assertThat(request.getHeaders().get("X-WhateverHeader").getFirstValue())
323                         .isEqualTo("Whatever Value");
324                 softly.assertThat(context.asContext(UriRouterContext.class)).isNotNull();
325                 softly.assertThat(context.asContext(UriRouterContext.class).getMatchedUri()).isEmpty();
326                 softly.assertThat(context.asContext(UriRouterContext.class).getOriginalUri().toString())
327                         .isEqualTo(format("http://localhost:%d/test", port));
328                 softly.assertThat(context.asContext(SessionContext.class)).isNotNull();
329                 softly.assertThat(context.asContext(SessionContext.class).getSession()).isNotNull();
330                 softly.assertThat(context.asContext(ClientContext.class)).isNotNull();
331                 softly.assertThat(context.asContext(ClientContext.class).getLocalPort())
332                         .isEqualTo(port);
333                 softly.assertAll();
334 
335                 final Response response = new Response(Status.OK);
336                 response.getHeaders().addAll(request.getHeaders().asMapOfHeaders());
337                 response.setEntity(request.getEntity().toString().toUpperCase());
338                 return newResponsePromise(response);
339             } catch (SoftAssertionError e) {
340                 return newResponsePromise(new Response(Status.INTERNAL_SERVER_ERROR)
341                         .setEntity(e.getMessage()).setCause(new Exception(e)));
342             }
343         }
344 
345         @Override
346         public Swagger api(ApiProducer<Swagger> producer) {
347             return null;
348         }
349 
350         @Override
351         public Swagger handleApiRequest(Context context, Request request) {
352             return new Swagger().path("test", new Path().post(new Operation().produces("text/plain")));
353         }
354 
355         @Override
356         public void addDescriptorListener(Listener listener) {
357 
358         }
359 
360         @Override
361         public void removeDescriptorListener(Listener listener) {
362 
363         }
364     }
365 
366     private final class TestSessionHandler implements Handler {
367         @Override
368         public Promise<Response, NeverThrowsException> handle(Context context, Request request) {
369             final Session session = context.asContext(SessionContext.class).getSession();
370             try {
371                 if (request.getUri().toASCIIString().endsWith("/populate")) {
372                     assertThat(session.isEmpty()).isTrue();
373                     assertThat(session.size()).isEqualTo(0);
374                     assertThat(session.containsKey("sessionKey")).isFalse();
375                     assertThat(session.containsValue("sessionValue")).isFalse();
376                     assertThat(session.put("sessionKey", "sessionValue")).isNull();
377                 } else if (request.getUri().toASCIIString().endsWith("/check")) {
378                     assertThat(session.get("sessionKey")).isEqualTo("sessionValue");
379                     assertThat(session.isEmpty()).isFalse();
380                     assertThat(session.size()).isEqualTo(1);
381                     assertThat(session.containsKey("sessionKey")).isTrue();
382                     assertThat(session.containsValue("sessionValue")).isTrue();
383                 } else {
384                     fail("Unsupported URI: " + request.getUri().toString());
385                 }
386 
387                 final Response response = new Response(Status.OK);
388                 response.setEntity("OK");
389                 return newResponsePromise(response);
390             } catch (AssertionError e) {
391                 return newResponsePromise(new Response(Status.INTERNAL_SERVER_ERROR)
392                         .setEntity(e.getMessage()).setCause(new Exception(e)));
393             }
394         }
395     }
396 }