/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.jooby.test;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import org.jooby.Deferred;
import org.jooby.Err;
import org.jooby.Jooby;
import org.jooby.MediaType;
import org.jooby.Request;
import org.jooby.Response;
import org.jooby.Result;
import org.jooby.Results;
import org.jooby.Route;
import org.jooby.Route.After;
import org.jooby.Route.Definition;
import org.jooby.Route.Filter;
import org.jooby.Status;
import com.google.common.collect.Lists;
import com.google.common.reflect.Reflection;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.name.Names;
import javaslang.control.Try;
/**
* <h1>tests</h1>
* <p>
* In this section we are going to see how to run unit and integration tests in Jooby.
* </p>
*
* <h2>unit tests</h2>
* <p>
* We do offer two programming models:
* </p>
* <ul>
* <li>script programming model; and</li>
* <li>mvc programming model</li>
* </ul>
*
* <p>
* You don't need much for <code>MVC</code> routes, because a route got binded to a method of some
* class. So it is usually very easy and simple to mock and run unit tests against a
* <code>MVC</code> route.
* </p>
*
* <p>
* We can't say the same for <code>script</code> routes, because a route is represented by a
* <code>lambda</code> and there is no easy or simple way to get access to the lambda object.
* </p>
*
* <p>
* We do provide a {@link MockRouter} which simplify unit tests for <code>script routes</code>:
* </p>
*
* <h3>usage</h3>
*
* <pre>{@code
* public class MyApp extends Jooby {
* {
* get("/test", () -> "Hello unit tests!");
* }
* }
* }</pre>
*
* <p>
* A unit test for this route, looks like:
* </p>
*
* <pre>
* @Test
* public void simpleTest() {
* String result = new MockRouter(new MyApp())
* .get("/test");
* assertEquals("Hello unit tests!", result);
* }</pre>
*
* <p>
* Just create a new instance of {@link MockRouter} with your application and call one of the HTTP
* method, like <code>get</code>, <code>post</code>, etc...
* </p>
*
* <h3>mocks</h3>
* <p>
* You're free to choose the mock library of your choice. Here is an example using
* <a href="http://easymock.org">EasyMock</a>:
* </p>
*
* <pre>{@code
* {
* get("/mock", req -> {
* return req.path();
* });
* }
* }</pre>
*
* <p>
* A test with <a href="http://easymock.org">EasyMock</a> looks like:
* </p>
* <pre>
*
* @Test
* public void shouldGetRequestPath() {
* Request req = EasyMock.createMock(Request.class);
* expect(req.path()).andReturn("/mypath");
*
* EasyMock.replay(req);
*
* String result = new MockRouter(new MyApp(), req)
* .get("/mock");
*
* assertEquals("/mypath", result);
*
* EasyMock.verify(req);
* }
* </pre>
*
* <p>
* You can mock a {@link Response} object in the same way:
* </p>
*
* <pre>{@code
* {
* get("/mock", (req, rsp) -> {
* rsp.send("OK");
* });
* }
* }</pre>
*
* <p>
* A test with <a href="http://easymock.org">EasyMock</a> looks like:
* </p>
* <pre>
*
* @Test
* public void shouldUseResponseSend() {
* Request req = EasyMock.createMock(Request.class);
* Response rsp = EasyMock.createMock(Response.class);
* rsp.send("OK");
*
* EasyMock.replay(req, rsp);
*
* String result = new MockRouter(new MyApp(), req, rsp)
* .get("/mock");
*
* assertEquals("OK", result);
*
* EasyMock.verify(req, rsp);
* }
* </pre>
*
* <p>
* What about external dependencies? It works in a similar way:
* </p>
*
* <pre>{@code
* {
* get("/", () -> {
* HelloService service = require(HelloService.class);
* return service.salute();
* });
* }
* }</pre>
*
* <pre>
* @Test
* public void shouldMockExternalDependencies() {
* HelloService service = EasyMock.createMock(HelloService.class);
* expect(service.salute()).andReturn("Hola!");
*
* EasyMock.replay(service);
*
* String result = new MockRouter(new MyApp())
* .set(service)
* .get("/");
*
* assertEquals("Hola!", result);
*
* EasyMock.verify(service);
* }
* </pre>
*
* <p>
* The {@link #set(Object)} call push and register an external dependency (usually a mock). This
* make it possible to resolve services from <code>require</code> calls.
* </p>
*
* <h3>deferred</h3>
* <p>
* Mock of promises are possible too:
* </p>
*
* <pre>{@code
* {
* get("/", promise(deferred -> {
* deferred.resolve("OK");
* }));
* }
* }</pre>
*
* <pre>
* @Test
* public void shouldMockPromises() {
*
* String result = new MockRouter(new MyApp())
* .get("/");
*
* assertEquals("OK", result);
* }
* </pre>
*
* Previous test works for deferred routes:
*
* <pre>{@code
* {
* get("/", deferred(() -> {
* return "OK";
* }));
* }
* }</pre>
*
* @author edgar
*/
public class MockRouter {
private static final Route.Chain NOOP_CHAIN = (req, rsp, next) -> {
};
private static class MockResponse extends Response.Forwarding {
List<Route.After> afterList = new ArrayList<>();
private AtomicReference<Object> ref;
public MockResponse(final Response response, final AtomicReference<Object> ref) {
super(response);
this.ref = ref;
}
@Override
public void after(final After handler) {
afterList.add(handler);
}
@Override
public void send(final Object result) throws Throwable {
rsp.send(result);
ref.set(result);
}
@Override
public void send(final Result result) throws Throwable {
rsp.send(result);
ref.set(result);
}
}
private static final int CLEAN_STACK = 4;
@SuppressWarnings("rawtypes")
private Map<Key, Object> registry = new HashMap<>();
private List<Definition> routes;
private Request req;
private Response rsp;
public MockRouter(final Jooby app) {
this(app, empty(Request.class), empty(Response.class));
}
public MockRouter(final Jooby app, final Request req) {
this(app, req, empty(Response.class));
}
public MockRouter(final Jooby app, final Request req, final Response rsp) {
this.routes = Jooby.exportRoutes(hackInjector(app));
this.req = req;
this.rsp = rsp;
}
public MockRouter set(final Object dependency) {
return set(null, dependency);
}
@SuppressWarnings({"unchecked", "rawtypes" })
public MockRouter set(final String name, final Object object) {
traverse(object.getClass(), type -> {
Object key = Optional.ofNullable(name)
.map(it -> Key.get(type, Names.named(name)))
.orElseGet(() -> Key.get(type));
registry.putIfAbsent((Key) key, object);
});
return this;
}
public <T> T get(final String path) throws Throwable {
return execute(Route.GET, path);
}
public <T> T post(final String path) throws Throwable {
return execute(Route.POST, path);
}
public <T> T put(final String path) throws Throwable {
return execute(Route.PUT, path);
}
public <T> T patch(final String path) throws Throwable {
return execute(Route.PATCH, path);
}
public <T> T delete(final String path) throws Throwable {
return execute(Route.DELETE, path);
}
public <T> T execute(final String method, final String path) throws Throwable {
return execute(method, path, MediaType.all, MediaType.all);
}
@SuppressWarnings("unchecked")
private <T> T execute(final String method, final String path, final MediaType contentType,
final MediaType... accept) throws Throwable {
List<Filter> filters = pipeline(method, path, contentType, Arrays.asList(accept));
if (filters.isEmpty()) {
throw new Err(Status.NOT_FOUND, path);
}
Iterator<Filter> pipeline = filters.iterator();
AtomicReference<Object> ref = new AtomicReference<>();
MockResponse rsp = new MockResponse(this.rsp, ref);
while (ref.get() == null && pipeline.hasNext()) {
Filter next = pipeline.next();
if (next instanceof Route.ZeroArgHandler) {
ref.set(((Route.ZeroArgHandler) next).handle());
} else if (next instanceof Route.OneArgHandler) {
ref.set(((Route.OneArgHandler) next).handle(req));
} else if (next instanceof Route.Handler) {
((Route.Handler) next).handle(req, rsp);
} else {
next.handle(req, rsp, NOOP_CHAIN);
}
}
Object lastResult = ref.get();
// after callbacks:
if (rsp.afterList.size() > 0) {
Result result = wrap(lastResult);
for (int i = rsp.afterList.size() - 1; i >= 0; i--) {
result = rsp.afterList.get(i).handle(req, rsp, result);
}
if (Result.class.isInstance(lastResult)) {
return (T) result;
}
return result.get();
}
// deferred results:
if (lastResult instanceof Deferred) {
Deferred deferred = ((Deferred) lastResult);
// execute deferred code:
deferred.handler(req, (v, x) -> {
});
// get result
lastResult = deferred.get();
if (Throwable.class.isInstance(lastResult)) {
throw (Throwable) lastResult;
}
}
return (T) lastResult;
}
private Result wrap(final Object value) {
if (value instanceof Result) {
return (Result) value;
}
return Results.with(value);
}
private List<Route.Filter> pipeline(final String method, final String path,
final MediaType contentType,
final List<MediaType> accept) {
List<Route.Filter> routes = new ArrayList<>();
for (Route.Definition routeDef : this.routes) {
Optional<Route> route = routeDef.matches(method, path, contentType, accept);
if (route.isPresent()) {
routes.add(routeDef.filter());
}
}
return routes;
}
private Jooby hackInjector(final Jooby app) {
Try.run(() -> {
Field field = Jooby.class.getDeclaredField("injector");
field.setAccessible(true);
Injector injector = proxyInjector(getClass().getClassLoader(), registry);
field.set(app, injector);
registry.put(Key.get(Injector.class), injector);
}).get();
return app;
}
@SuppressWarnings("rawtypes")
private static Injector proxyInjector(final ClassLoader loader, final Map<Key, Object> registry) {
return Reflection.newProxy(Injector.class, (proxy, method, args) -> {
if (method.getName().equals("getInstance")) {
Key key = (Key) args[0];
Object value = registry.get(key);
if (value == null) {
Object type = key.getAnnotation() != null ? key : key.getTypeLiteral();
IllegalStateException iex = new IllegalStateException("Not found: " + type);
// Skip proxy and some useless lines:
Try.of(() -> {
StackTraceElement[] stacktrace = iex.getStackTrace();
return Lists.newArrayList(stacktrace).subList(CLEAN_STACK, stacktrace.length);
}).onSuccess(stacktrace -> iex
.setStackTrace(stacktrace.toArray(new StackTraceElement[stacktrace.size()])));
throw iex;
}
return value;
}
throw new UnsupportedOperationException(method.toString());
});
}
@SuppressWarnings("rawtypes")
private void traverse(final Class type, final Consumer<Class> set) {
if (type != Object.class) {
set.accept(type);
Optional.ofNullable(type.getSuperclass()).ifPresent(it -> traverse(it, set));
Arrays.asList(type.getInterfaces()).forEach(it -> traverse(it, set));
}
}
private static <T> T empty(final Class<T> type) {
return Reflection.newProxy(type, (proxy, method, args) -> {
throw new UnsupportedOperationException(method.toString());
});
}
}