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 */ 016package org.forgerock.http.bindings; 017 018import static java.lang.String.format; 019import static org.assertj.core.api.Assertions.assertThat; 020import static org.assertj.core.api.Assertions.fail; 021import static org.forgerock.http.Applications.describedHttpApplication; 022import static org.forgerock.http.Applications.simpleHttpApplication; 023import static org.forgerock.http.filter.TransactionIdInboundFilter.SYSPROP_TRUST_TRANSACTION_HEADER; 024import static org.forgerock.http.handler.Handlers.chainOf; 025import static org.forgerock.http.protocol.Response.newResponsePromise; 026import static org.forgerock.json.JsonValue.json; 027import static org.forgerock.json.test.assertj.AssertJJsonValueAssert.assertThat; 028import static org.mockito.Mockito.mock; 029import static org.mockito.Mockito.verify; 030import static org.mockito.Mockito.verifyNoMoreInteractions; 031import static org.mockito.Mockito.when; 032 033import io.swagger.v3.oas.models.OpenAPI; 034import io.swagger.v3.oas.models.Operation; 035import io.swagger.v3.oas.models.PathItem; 036import io.swagger.v3.oas.models.Paths; 037import io.swagger.v3.oas.models.info.Info; 038import java.util.List; 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 070/** 071 * A test class for CHF bindings. 072 */ 073public abstract class BindingTest { 074 075 private int port; 076 077 /** 078 * Create a server to bind a CHF application to in tests. 079 */ 080 protected abstract void createServer(); 081 082 /** 083 * Stop the server. 084 * @throws Exception In case of error. 085 */ 086 protected abstract void stopServer() throws Exception; 087 088 /** 089 * Start the server. 090 * @throws Exception In case of error. 091 * @return The port number the server is listening on. 092 */ 093 protected abstract int startServer() throws Exception; 094 095 /** 096 * Add an application to the server. The application should be added to the root path. 097 * @param application The application. 098 * @throws Exception In case of failure. 099 */ 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}