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}