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