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}