001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2016 ForgeRock AS. 015 */ 016 017package org.forgerock.http.bindings; 018 019import static io.swagger.models.Scheme.HTTP; 020import static io.swagger.models.Scheme.HTTPS; 021import static java.lang.String.format; 022import static java.util.Arrays.asList; 023import static org.assertj.core.api.Assertions.assertThat; 024import static org.assertj.core.api.Assertions.fail; 025import static org.forgerock.http.Applications.describedHttpApplication; 026import static org.forgerock.http.Applications.simpleHttpApplication; 027import static org.forgerock.http.filter.TransactionIdInboundFilter.SYSPROP_TRUST_TRANSACTION_HEADER; 028import static org.forgerock.http.handler.Handlers.chainOf; 029import static org.forgerock.http.protocol.Response.newResponsePromise; 030import static org.forgerock.json.JsonValue.json; 031import static org.forgerock.json.test.assertj.AssertJJsonValueAssert.assertThat; 032import static org.mockito.Mockito.mock; 033import static org.mockito.Mockito.verify; 034import static org.mockito.Mockito.verifyNoMoreInteractions; 035import static org.mockito.Mockito.when; 036 037import java.util.List; 038 039import org.assertj.core.api.SoftAssertionError; 040import org.assertj.core.api.SoftAssertions; 041import org.forgerock.http.ApiProducer; 042import org.forgerock.http.Client; 043import org.forgerock.http.DescribedHttpApplication; 044import org.forgerock.http.Handler; 045import org.forgerock.http.HttpApplication; 046import org.forgerock.http.HttpApplicationException; 047import org.forgerock.http.handler.DescribableHandler; 048import org.forgerock.http.handler.HttpClientHandler; 049import org.forgerock.http.header.CookieHeader; 050import org.forgerock.http.header.SetCookieHeader; 051import org.forgerock.http.protocol.Cookie; 052import org.forgerock.http.protocol.Request; 053import org.forgerock.http.protocol.Response; 054import org.forgerock.http.protocol.Status; 055import org.forgerock.http.routing.UriRouterContext; 056import org.forgerock.http.session.Session; 057import org.forgerock.http.session.SessionContext; 058import org.forgerock.http.swagger.OpenApiRequestFilter; 059import org.forgerock.http.swagger.SwaggerApiProducer; 060import org.forgerock.services.TransactionId; 061import org.forgerock.services.context.ClientContext; 062import org.forgerock.services.context.Context; 063import org.forgerock.services.context.TransactionIdContext; 064import org.forgerock.util.promise.NeverThrowsException; 065import org.forgerock.util.promise.Promise; 066import org.testng.annotations.AfterMethod; 067import org.testng.annotations.BeforeMethod; 068import org.testng.annotations.Test; 069 070import io.swagger.models.Info; 071import io.swagger.models.Operation; 072import io.swagger.models.Path; 073import io.swagger.models.Swagger; 074 075/** 076 * A test class for CHF bindings. 077 */ 078public abstract class BindingTest { 079 080 private int port; 081 082 /** 083 * Create a server to bind a CHF application to in tests. 084 */ 085 protected abstract void createServer(); 086 087 /** 088 * Stop the server. 089 * @throws Exception In case of error. 090 */ 091 protected abstract void stopServer() throws Exception; 092 093 /** 094 * Start the server. 095 * @throws Exception In case of error. 096 * @return The port number the server is listening on. 097 */ 098 protected abstract int startServer() throws Exception; 099 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}