package org.folio.okapi;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;

import guru.nidi.ramltester.RamlDefinition;
import guru.nidi.ramltester.RamlLoaders;
import guru.nidi.ramltester.restassured3.RestAssuredClient;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.json.JsonArray;
import io.vertx.ext.unit.TestContext;
import io.vertx.ext.unit.junit.VertxUnitRunner;
import org.folio.okapi.common.OkapiLogger;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.unit.Async;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import java.util.Arrays;
import java.util.Base64;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.Logger;
import org.folio.okapi.common.HttpClientLegacy;
import org.folio.okapi.common.HttpResponse;
import org.folio.okapi.common.XOkapiHeaders;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

@RunWith(VertxUnitRunner.class)
public class ProxyTest {

  private final Logger logger = OkapiLogger.get();
  private Vertx vertx;
  private HttpClient httpClient;
  private static final String LS = System.lineSeparator();
  private static final String CORS_TEST_HEADER = "CORS_TEST_HEADER";
  private final int portTimer = 9235;
  private final int portPre = 9236;
  private final int portPost = 9237;
  private final int portEdge = 9238;
  private final int port = 9230;
  private Buffer preBuffer;
  private Buffer postBuffer;
  private MultiMap postHandlerHeaders;
  private static RamlDefinition api;
  private int timerDelaySum = 0;
  private int timerTenantInitStatus = 200;
  private int timerTenantPermissionsStatus = 200;
  private JsonObject timerPermissions = new JsonObject();
  private JsonArray edgePermissionsAtInit = null;
  private JsonObject timerTenantData;

  @BeforeClass
  public static void setUpBeforeClass() throws Exception {
    api = RamlLoaders.fromFile("src/main/raml").load("okapi.raml");
  }

  private void myPreHandle(RoutingContext ctx) {
    logger.info("myPreHandle!");
    preBuffer = Buffer.buffer();
    if (HttpMethod.DELETE.equals(ctx.request().method())) {
      ctx.request().endHandler(x -> HttpResponse.responseText(ctx, 204).end());
    } else {
      ctx.response().setStatusCode(200);
      ctx.request().handler(preBuffer::appendBuffer);
      ctx.request().endHandler(res -> {
        logger.info("myPreHandle end=" + preBuffer.toString());
        ctx.response().end();
      });
    }
  }

  private void myPostHandle(RoutingContext ctx) {
    logger.info("myPostHandle!");
    postHandlerHeaders = ctx.request().headers();
    postBuffer = Buffer.buffer();
    if (HttpMethod.DELETE.equals(ctx.request().method())) {
      ctx.request().endHandler(x -> HttpResponse.responseText(ctx, 204).end());
    } else {
      ctx.response().setStatusCode(200);
      ctx.request().handler(postBuffer::appendBuffer);
      ctx.request().endHandler(res -> {
        logger.info("myPostHandle end=" + postBuffer.toString());
        ctx.response().end();
      });
    }
  }

  private void myEdgeCallTest(RoutingContext ctx, String token) {
    HttpClientRequest get = HttpClientLegacy.get(httpClient, port, "localhost", "/testb/1", res1 -> {
      Buffer resBuf = Buffer.buffer();
      res1.handler(resBuf::appendBuffer);
      res1.endHandler(res2 -> {
        ctx.response().setStatusCode(res1.statusCode());
        ctx.response().end(resBuf);
      });
    });
    get.putHeader("X-Okapi-Token", token);
    get.end();
  }

  private void myEdgeHandle(RoutingContext ctx) {
    logger.info("myEdgeHandle");
    String p = ctx.request().path();
    if (HttpMethod.DELETE.equals(ctx.request().method())) {
      ctx.request().endHandler(x -> HttpResponse.responseText(ctx, 204).end());
    } else if (HttpMethod.POST.equals(ctx.request().method()) && p.equals("/_/tenant")) {
      edgePermissionsAtInit = timerPermissions.getJsonArray("edge-module-1.0.0");
      ctx.request().endHandler(x -> HttpResponse.responseText(ctx, 200).end());
    } else if (HttpMethod.GET.equals(ctx.request().method())) {
      Buffer buf = Buffer.buffer();
      ctx.request().handler(buf::appendBuffer);
      ctx.request().endHandler(res -> {
        if (!p.startsWith("/edge/")) {
          ctx.response().setStatusCode(404);
          ctx.response().end("Edge module reports not found");
          return;
        }
        String tenant = p.substring(6);
        final String docLogin = "{" + LS
            + "  \"tenant\" : \"" + tenant + "\"," + LS
            + "  \"username\" : \"peter\"," + LS
            + "  \"password\" : \"peter-password\"" + LS
            + "}";
        HttpClientRequest post = HttpClientLegacy.post(httpClient, port, "localhost", "/authn/login", res1 -> {
          Buffer loginBuf = Buffer.buffer();
          res1.handler(loginBuf::appendBuffer);
          res1.endHandler(res2 -> {
            if (res1.statusCode() != 200) {
              ctx.response().setStatusCode(res1.statusCode());
              ctx.response().end(loginBuf);
            } else {
              myEdgeCallTest(ctx, res1.getHeader("X-Okapi-Token"));
            }
          });
        });
        post.putHeader("Content-Type", "application/json");
        post.putHeader("Accept", "application/json");
        post.putHeader("X-Okapi-Tenant", tenant);
        post.end(docLogin);
      });
    } else {
      ctx.response().setStatusCode(404);
      ctx.response().end("Unsupported method");
    }
  }

  private void myTimerHandle(RoutingContext ctx) {
    final String p = ctx.request().path();
    logger.info("myTimerHandle p=" + p);
    for (Entry<String, String> ent : ctx.request().headers().entries()) {
      logger.info(ent.getKey() + ":" + ent.getValue());
    }
    if (HttpMethod.DELETE.equals(ctx.request().method())) {
      ctx.request().endHandler(x -> HttpResponse.responseText(ctx, 204).end());
    } else if (HttpMethod.POST.equals(ctx.request().method())) {
      Buffer buf = Buffer.buffer();
      ctx.request().handler(buf::appendBuffer);
      ctx.request().endHandler(res -> {
        try {
          if (p.startsWith("/_/tenant")) {
            timerTenantData = new JsonObject(buf);
            ctx.response().setStatusCode(timerTenantInitStatus);
            ctx.response().end("timer response");
          } else if (p.startsWith("/permissionscall")) {
            JsonObject permObject = new JsonObject(buf);
            if (timerTenantPermissionsStatus == 200) {
              timerPermissions.put(permObject.getString("moduleId"), permObject.getJsonArray("perms"));
            }
            ctx.response().setStatusCode(timerTenantPermissionsStatus);
            ctx.response().end("timer permissions response");
          } else if (p.startsWith("/timercall/")) {
            long delay = Long.parseLong(p.substring(11)); // assume /timercall/[0-9]+
            timerDelaySum += delay;
            vertx.setTimer(delay, x -> {
              ctx.response().setStatusCode(200);
              ctx.response().end();
            });
          } else if (p.startsWith("/regularcall")) {
            ctx.response().end(extractSubFromToken(ctx));
          } else if (p.startsWith("/corscall")) {
            ctx.response().putHeader(CORS_TEST_HEADER, ctx.request().query());
            ctx.response().end("Response from CORS test");
          } else {
            ctx.response().setStatusCode(404);
            ctx.response().end(p);
          }
        } catch (Exception ex) {
          ctx.response().setStatusCode(400);
          ctx.response().end(ex.getMessage());
        }
      });
    } else {
      ctx.response().setStatusCode(404);
      ctx.response().end("Unsupported method");
    }
  }

  Future<Void> startPreServer() {
    Router router = Router.router(vertx);

    router.routeWithRegex("/.*").handler(this::myPreHandle);

    Promise<Void> promise = Promise.promise();
    HttpServerOptions so = new HttpServerOptions().setHandle100ContinueAutomatically(true);
    vertx.createHttpServer(so)
        .requestHandler(router)
        .listen(portPre, x -> promise.handle(x.mapEmpty()));
    return promise.future();
  }

  Future<Void> startPostServer() {
    Router router = Router.router(vertx);

    router.routeWithRegex("/.*").handler(this::myPostHandle);

    Promise<Void> promise = Promise.promise();
    HttpServerOptions so = new HttpServerOptions().setHandle100ContinueAutomatically(true);
    vertx.createHttpServer(so)
        .requestHandler(router)
        .listen(portPost, x -> promise.handle(x.mapEmpty()));
    return promise.future();
  }

  Future<Void> startTimerServer() {
    Router router = Router.router(vertx);

    router.routeWithRegex("/.*").handler(this::myTimerHandle);

    Promise<Void> promise = Promise.promise();
    HttpServerOptions so = new HttpServerOptions().setHandle100ContinueAutomatically(true);
    vertx.createHttpServer(so)
        .requestHandler(router)
        .listen(portTimer, x -> promise.handle(x.mapEmpty()));
    return promise.future();
  }

  Future<Void> startEdgeServer() {
    Router router = Router.router(vertx);

    router.routeWithRegex("/.*").handler(this::myEdgeHandle);

    HttpServerOptions so = new HttpServerOptions().setHandle100ContinueAutomatically(true);
    Promise<Void> promise = Promise.promise();
    vertx.createHttpServer(so)
        .requestHandler(router)
        .listen(portEdge,  x -> promise.handle(x.mapEmpty()));
    return promise.future();
  }

  Future<Void> startOkapi() {
    DeploymentOptions opt = new DeploymentOptions()
        .setConfig(new JsonObject()
            .put("loglevel", "info")
            .put("port", Integer.toString(port))
            .put("httpCache", true));
    Promise<Void> promise = Promise.promise();
    vertx.deployVerticle(MainVerticle.class.getName(), opt, x -> promise.handle(x.mapEmpty()));
    return promise.future();
  }

  @Before
  public void setUp(TestContext context) {
    vertx = Vertx.vertx();
    httpClient = vertx.createHttpClient();

    timerTenantInitStatus = 200;
    RestAssured.port = port;

    Future<Void> future = Future.succeededFuture()
        .compose(x -> startOkapi())
        .compose(x -> startEdgeServer())
        .compose(x -> startTimerServer())
        .compose(x -> startPreServer())
        .compose(x -> startPostServer());
    future.onComplete(context.asyncAssertSuccess());
  }

  private void td(TestContext context, Async async) {
    vertx.close(x -> {
      async.complete();
    });
  }

  @After
  public void tearDown(TestContext context) {
    Async async = context.async();

    HttpClientLegacy.delete(httpClient, port, "localhost", "/_/discovery/modules", response -> {
      context.assertTrue(response.statusCode() == 404 || response.statusCode() == 204);
      response.endHandler(x -> {
        httpClient.close();
        td(context, async);
      });
    }).end();
  }

  @Test
  public void testBadToken(TestContext context) {

    given()
      .header("X-Okapi-Token", "a")
      .header("Content-Type", "application/json")
      .get("/_/proxy/modules")
      .then().statusCode(400)
      .body(containsString("Invalid Token: "));

    given()
      .header("X-Okapi-Token", "a.b.c")
      .header("Content-Type", "application/json")
      .get("/_/proxy/modules")
      .then().statusCode(400)
      .body(equalTo("Invalid Token: Input byte[] should at least have 2 bytes for base64 bytes"));

    given()
      .header("X-Okapi-Token", "a.ewo=.d")
      .header("Content-Type", "application/json")
      .get("/_/proxy/modules")
      .then().statusCode(400)
      .body(containsString("Unexpected end-of-input"));
  }

  @Test
  public void test1(TestContext context) {
    RestAssuredClient c;
    Response r;
    final String okapiTenant = "roskilde";

    // add tenant
    final String docTenantRoskilde = "{" + LS
      + "  \"id\" : \"" + okapiTenant + "\"," + LS
      + "  \"name\" : \"" + okapiTenant + "\"," + LS
      + "  \"description\" : \"Roskilde bibliotek\"" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docTenantRoskilde).post("/_/proxy/tenants")
      .then().statusCode(201)
      .body(equalTo(docTenantRoskilde))
      .extract().response();
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    final String locationTenantRoskilde = r.getHeader("Location");

    final String docBasic_1_0_0 = "{" + LS
      + "  \"id\" : \"basic-module-1.0.0\"," + LS
      + "  \"name\" : \"this module\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"_tenant\"," + LS
      + "    \"version\" : \"1.1\"," + LS
      + "    \"interfaceType\" : \"system\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant/disable\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"POST\", \"DELETE\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  }, {" + LS
      + "    \"id\" : \"myxfirst\"," + LS
      + "    \"version\" : \"1.0\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"GET\"]," + LS
      + "      \"pathPattern\" : \"/testb/client_id\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  }, {" + LS
      + "    \"id\" : \"mysecond\"," + LS
      + "    \"version\" : \"1.0\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"GET\"]," + LS
      + "      \"pathPattern\" : \"/testb/{id}\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]," + LS
      + "  \"optional\" : [ { \"id\" : \"optional-foo\", \"version\": \"1.0\"} ]," + LS
      + "  \"launchDescriptor\" : {" + LS
      + "    \"exec\" : "
      + "\"java -Dport=%p -jar ../okapi-test-module/target/okapi-test-module-fat.jar\"" + LS
      + "  }" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docBasic_1_0_0).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    final String locationBasic_1_0_0 = r.getHeader("Location");

    /* missing action so this will fail */
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("[ {\"id\" : \"basic-module-1.0.0\"} ]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install?deploy=true")
      .then().statusCode(400).log().ifValidationFails()
      .body(equalTo("Missing action for id basic-module-1.0.0"));
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("[ {\"id\" : \"basic-module-1.0.0\", \"action\" : \"enable\"} ]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install?deploy=true")
      .then().statusCode(200).log().ifValidationFails()
      .body(equalTo("[ {" + LS
        + "  \"id\" : \"basic-module-1.0.0\"," + LS
        + "  \"action\" : \"enable\"" + LS
        + "} ]"));
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    r = c.given()
      .header("X-Okapi-Tenant", okapiTenant)
      .header("X-all-headers", "B")
      .get("/testb/hugo")
      .then().statusCode(200).log().ifValidationFails()
      .extract().response();
    Assert.assertTrue(r.body().asString().contains("X-Okapi-Match-Path-Pattern:/testb/{id}"));

    c = api.createRestAssured3();
    r = c.given()
      .header("X-Okapi-Tenant", okapiTenant)
      .header("X-all-headers", "B")
      .get("/testb/client_id")
      .then().statusCode(200).log().ifValidationFails()
      .extract().response();
    Assert.assertTrue(r.body().asString().contains("X-Okapi-Match-Path-Pattern:/testb/client_id"));

    c = api.createRestAssured3();
    r = c.given()
      .header("X-Okapi-Tenant", okapiTenant)
      .header("X-all-headers", "B")
      .get("/testb/client_id%2Fx")
      .then().statusCode(200).log().ifValidationFails()
      .extract().response();
    Assert.assertTrue(r.body().asString().contains("X-Okapi-Match-Path-Pattern:/testb/{id}"));

    c = api.createRestAssured3();
    c.given()
      .header("X-Okapi-Tenant", okapiTenant)
      .get("/testb/client_id/x")
      .then().statusCode(404).log().ifValidationFails();

  }

  @Test
  public void testAdditionalToken(TestContext context) {
    RestAssuredClient c;
    Response r;
    final String okapiTenant = "roskilde";

    // add tenant
    final String docTenantRoskilde = "{" + LS
      + "  \"id\" : \"" + okapiTenant + "\"," + LS
      + "  \"name\" : \"" + okapiTenant + "\"," + LS
      + "  \"description\" : \"Roskilde bibliotek\"" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docTenantRoskilde).post("/_/proxy/tenants")
      .then().statusCode(201)
      .body(equalTo(docTenantRoskilde))
      .extract().response();
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    final String locationTenantRoskilde = r.getHeader("Location");

    final String testAuthJar = "../okapi-test-auth-module/target/okapi-test-auth-module-fat.jar";
    final String docAuthModule = "{" + LS
      + "  \"id\" : \"auth-module-1.0.0\"," + LS
      + "  \"name\" : \"auth\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"auth\"," + LS
      + "    \"version\" : \"1.2\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"path\" : \"/authn/login\"," + LS
      + "      \"level\" : \"20\"," + LS
      + "      \"type\" : \"request-response\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"filters\" : [ {" + LS
      + "    \"methods\" : [ \"*\" ]," + LS
      + "    \"path\" : \"/\"," + LS
      + "    \"phase\" : \"auth\"," + LS
      + "    \"type\" : \"headers\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]," + LS
      + "  \"launchDescriptor\" : {" + LS
      + "    \"exec\" : \"java -Dport=%p -jar " + testAuthJar + "\"" + LS
      + "  }" + LS
      + "}";

    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docAuthModule).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String docBasic_1_0_0 = "{" + LS
      + "  \"id\" : \"basic-module-1.0.0\"," + LS
      + "  \"name\" : \"this module\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"_tenant\"," + LS
      + "    \"version\" : \"1.1\"," + LS
      + "    \"interfaceType\" : \"system\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant/disable\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"POST\", \"DELETE\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  }, {" + LS
      + "    \"id\" : \"myint\"," + LS
      + "    \"version\" : \"1.0\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"GET\"]," + LS
      + "      \"pathPattern\" : \"/testb/{id}\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]," + LS
      + "  \"launchDescriptor\" : {" + LS
      + "    \"exec\" : "
      + "\"java -Dport=%p -jar ../okapi-test-module/target/okapi-test-module-fat.jar\"" + LS
      + "  }" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docBasic_1_0_0).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"basic-module-1.0.0\", \"action\" : \"enable\"},"
        + " {\"id\" : \"auth-module-1.0.0\", \"action\" : \"enable\"}"
        + "]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install?deploy=true")
      .then().statusCode(200).log().ifValidationFails()
      .body(equalTo("[ {" + LS
        + "  \"id\" : \"basic-module-1.0.0\"," + LS
        + "  \"action\" : \"enable\"" + LS
        + "}, {" + LS
        + "  \"id\" : \"auth-module-1.0.0\"," + LS
        + "  \"action\" : \"enable\"" + LS
        + "} ]"));
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String docLogin = "{" + LS
      + "  \"tenant\" : \"" + okapiTenant + "\"," + LS
      + "  \"username\" : \"peter\"," + LS
      + "  \"password\" : \"peter-password\"" + LS
      + "}";
    final String okapiToken = c.given().header("Content-Type", "application/json").body(docLogin)
      .header("X-Okapi-Tenant", okapiTenant).post("/authn/login")
      .then().statusCode(200).extract().header("X-Okapi-Token");

    // tenant but no token
    c = api.createRestAssured3();
    r = c.given()
      .header("X-Okapi-Tenant", okapiTenant)
      .get("/testb/hugo")
      .then().statusCode(200).log().ifValidationFails()
      .extract().response();
    Assert.assertEquals("It works", r.getBody().asString());

    // token with implied tenant
    c = api.createRestAssured3();
    r = c.given()
      .header("X-Okapi-Token", okapiToken)
      .get("/testb/hugo")
      .then().statusCode(200).log().ifValidationFails()
      .extract().response();
    Assert.assertEquals("It works", r.getBody().asString());

    c = api.createRestAssured3();
    r = c.given()
      .header("X-all-headers", "B")
      .header("X-Okapi-Token", okapiToken)
      .header("X-Okapi-Additional-Token", "dummyJwt")
      .get("/testb/hugo")
      .then().statusCode(200).log().ifValidationFails()
      .extract().response();
    String b = r.getBody().asString();
    Assert.assertTrue(b.contains("It works"));
    // test module must NOT receive the X-Okapi-Additional-Token
    Assert.assertTrue(!b.contains("X-Okapi-Additional-Token"));

    c = api.createRestAssured3();
    c.given()
      .header("X-all-headers", "B")
      .header("X-Okapi-Token", okapiToken)
      .header("X-Okapi-Additional-Token", "nomatch")
      .get("/testb/hugo")
      .then().statusCode(400).log().ifValidationFails();
  }

  @Test
  public void testProxy(TestContext context) {
    final String okapiTenant = "roskilde";

    RestAssuredClient c;
    Response r;

    String nodeListDoc = "[ {" + LS
      + "  \"nodeId\" : \"localhost\"," + LS
      + "  \"url\" : \"http://localhost:9230\"" + LS
      + "} ]";

    c = api.createRestAssured3();
    c.given().get("/_/discovery/nodes").then().statusCode(200)
      .body(equalTo(nodeListDoc));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given().get("/_/discovery/nodes/gyf").then().statusCode(404);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given().get("/_/discovery/nodes/localhost").then().statusCode(200);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("{ }").post("/_/xyz").then().statusCode(404);
    Assert.assertEquals("RamlReport{requestViolations=[Resource '/_/xyz' is not defined], "
      + "responseViolations=[], validationViolations=[]}",
      c.getLastReport().toString());

    final String badDoc = "{" + LS
      + "  \"instId\" : \"BAD\"," + LS // the comma here makes it bad json!
      + "}";
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(badDoc).post("/_/deployment/modules")
      .then().statusCode(400);

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("{}").post("/_/deployment/modules")
      .then().statusCode(400);

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("{\"srvcId\" : \"foo\"}").post("/_/deployment/modules")
      .then().statusCode(400);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String docUnknownJar = "{" + LS
      + "  \"srvcId\" : \"auth-1\"," + LS
      + "  \"descriptor\" : {" + LS
      + "    \"exec\" : "
      + "\"java -Dport=%p -jar ../okapi-test-auth-module/target/okapi-unknown.jar\"" + LS
      + "  }" + LS
      + "}";

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(docUnknownJar).post("/_/deployment/modules")
      .then()
      .statusCode(400);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String docAuthDeployment = "{" + LS
      + "  \"srvcId\" : \"auth-1\"," + LS
      + "  \"descriptor\" : {" + LS
      + "    \"exec\" : "
      + "\"java -Dport=%p -jar ../okapi-test-auth-module/target/okapi-test-auth-module-fat.jar\"" + LS
      + "  }" + LS
      + "}";

    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docAuthDeployment).post("/_/deployment/modules")
      .then()
      .statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    String locationAuthDeployment = r.getHeader("Location");

    c = api.createRestAssured3();
    String docAuthDiscovery = c.given().get(locationAuthDeployment)
      .then().statusCode(200).extract().body().asString();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String docAuthModule = "{" + LS
      + "  \"id\" : \"auth-1\"," + LS
      + "  \"name\" : \"auth\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"auth\"," + LS
      + "    \"version\" : \"1.2\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"path\" : \"/authn/login\"," + LS
      + "      \"level\" : \"20\"," + LS
      + "      \"type\" : \"request-response\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"filters\" : [ {" + LS
      + "    \"methods\" : [ \"*\" ]," + LS
      + "    \"path\" : \"/\"," + LS
      + "    \"phase\" : \"auth\"," + LS
      + "    \"type\" : \"request-response\"," + LS
      + "    \"permissionsRequired\" : [ ]," + LS
      + "    \"permissionsDesired\" : [ \"auth.extra\" ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]" + LS
      + "}";

    // Check that we fail on unknown route types
    final String docBadTypeModule
      = docAuthModule.replaceAll("request-response", "UNKNOWN-ROUTE-TYPE");
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(docBadTypeModule).post("/_/proxy/modules")
      .then().statusCode(400);

    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docAuthModule).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    final String locationAuthModule = r.getHeader("Location");

    // PUT not defined for RAML
    given()
      .header("Content-Type", "application/json")
      .body("{ \"bad Json\" ").put(locationAuthModule).then().statusCode(404);

    final String docAuthModule2 = "{" + LS
      + "  \"id\" : \"auth2-1\"," + LS
      + "  \"name\" : \"auth2\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"auth2\"," + LS
      + "    \"version\" : \"1.2\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"path\" : \"/authn/login\"," + LS
      + "      \"level\" : \"20\"," + LS
      + "      \"type\" : \"request-response\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"filters\" : [ {" + LS
      + "    \"methods\" : [ \"*\" ]," + LS
      + "    \"path\" : \"/\"," + LS
      + "    \"level\" : \"10\"," + LS
      + "    \"type\" : \"request-response\"," + LS
      + "    \"permissionsRequired\" : [ ]," + LS
      + "    \"permissionsDesired\" : [ \"auth.extra\" ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]" + LS
      + "}";

    c = api.createRestAssured3();
    final String locationAuthModule2 = c.given()
      .header("Content-Type", "application/json")
      .body(docAuthModule2).post("/_/proxy/modules")
      .then().statusCode(201)
      .extract().header("Location");
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given().delete(locationAuthModule2).then().statusCode(204);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String docSampleDeployment = "{" + LS
      + "  \"srvcId\" : \"sample-module-1\"," + LS
      + "  \"descriptor\" : {" + LS
      + "    \"exec\" : "
      + "\"java -Dport=%p -jar ../okapi-test-module/target/okapi-test-module-fat.jar\"," + LS
      + "    \"env\" : [ {" + LS
      + "      \"name\" : \"helloGreeting\"," + LS
      + "      \"value\" : \"hej\"" + LS
      + "    } ]" + LS
      + "  }" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docSampleDeployment).post("/_/deployment/modules")
      .then()
      .statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    String locationSampleDeployment = r.getHeader("Location");

    c = api.createRestAssured3();
    String docSampleDiscovery = c.given().get(locationSampleDeployment)
      .then().statusCode(200).extract().body().asString();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String docSampleModuleBadRequire = "{" + LS
      + "  \"id\" : \"sample-module-1\"," + LS
      + "  \"name\" : \"sample module\"," + LS
      + "  \"requires\" : [ {" + LS
      + "    \"id\" : \"SOMETHINGWEDONOTHAVE\"," + LS
      + "    \"version\" : \"1.2\"" + LS
      + "  } ]," + LS
      + "  \"routingEntries\" : [ ] " + LS
      + "}";

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(docSampleModuleBadRequire).post("/_/proxy/modules").then().statusCode(400)
      .extract().response();

    final String docSampleModuleBadVersion = "{" + LS
      + "  \"id\" : \"sample-module-1\"," + LS
      + "  \"name\" : \"sample module\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"sample\"," + LS
      + "    \"version\" : \"1.0\"" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ {" + LS
      + "    \"id\" : \"auth\"," + LS
      + "    \"version\" : \"9.9\"" + LS // We only have 1.2
      + "  } ]," + LS
      + "}";

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(docSampleModuleBadVersion).post("/_/proxy/modules").then().statusCode(400)
      .extract().response();

    final String docSampleModule = "{" + LS
      + "  \"id\" : \"sample-module-1\"," + LS
      + "  \"name\" : \"sample module\"," + LS
      + "  \"requires\" : [ {" + LS
      + "    \"id\" : \"auth\"," + LS
      + "    \"version\" : \"1.2\"" + LS
      + "  } ]," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"sample\"," + LS
      + "    \"version\" : \"1.0\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"GET\", \"POST\" ]," + LS
      + "      \"path\" : \"/testb\"," + LS
      + "      \"level\" : \"30\"," + LS
      + "      \"type\" : \"request-response\"," + LS
      + "      \"modulePermissions\" : [ \"sample.modperm\" ]," + LS
      + "      \"permissionsRequired\" : [ \"sample.needed\" ]," + LS
      + "      \"permissionsDesired\" : [ \"sample.extra\" ]" + LS
      + "      } ]" + LS
      + "  }, {" + LS
      + "    \"id\" : \"_tenant\"," + LS
      + "    \"version\" : \"1.0\"" + LS // TODO - Define paths - add test
      + "  }]," + LS
      + "  \"launchDescriptor\" : {" + LS
      + "    \"exec\" : \"/usr/bin/false\"" + LS
      + "  }" + LS
      + "}";
    logger.debug(docSampleModule);
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docSampleModule).post("/_/proxy/modules")
      .then()
      .statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    final String locationSampleModule = r.getHeader("Location");

    // Try to delete the auth module that our sample depends on
    c.given().delete(locationAuthModule).then().statusCode(400);

    // Create our tenant
    final String docTenantRoskilde = "{" + LS
      + "  \"id\" : \"" + okapiTenant + "\"," + LS
      + "  \"name\" : \"" + okapiTenant + "\"," + LS
      + "  \"description\" : \"Roskilde bibliotek\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(docTenantRoskilde).post("/_/proxy/tenants/") // trailing slash fails
      .then().statusCode(404);
    Assert.assertEquals("RamlReport{requestViolations=[Resource '/_/proxy/tenants/' is not defined], "
      + "responseViolations=[], validationViolations=[]}",
      c.getLastReport().toString());

    // add tenant by using PUT (which will insert)
    final String locationTenantRoskilde = "/_/proxy/tenants/" + okapiTenant;
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(docTenantRoskilde)
      .put(locationTenantRoskilde)
      .then().statusCode(200)
      .body(equalTo(docTenantRoskilde))
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // Try to enable sample without the auth that it requires
    final String docEnableWithoutDep = "{" + LS
      + "  \"id\" : \"sample-module-1\"" + LS
      + "}";
    given()
      .header("Content-Type", "application/json")
      .body(docEnableWithoutDep).post("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(400);

    // try to enable a module we don't know
    final String docEnableAuthBad = "{" + LS
      + "  \"id\" : \"UnknonwModule-1\"" + LS
      + "}";
    given()
      .header("Content-Type", "application/json")
      .body(docEnableAuthBad).post("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(404);

    final String docEnableAuth = "{" + LS
      + "  \"id\" : \"auth-1\"" + LS
      + "}";
    given()
      .header("Content-Type", "application/json")
      .body(docEnableAuth).post("/_/proxy/tenants/" + okapiTenant + "/modules/")
      .then().statusCode(404);  // trailing slash is no good

    // Actually enable the auith
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(docEnableAuth).post("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(201).body(equalTo(docEnableAuth));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    given().get("/_/proxy/tenants/" + okapiTenant + "/modules/")
      .then().statusCode(404);  // trailing slash again

    // Get the list of one enabled module
    c = api.createRestAssured3();
    final String exp1 = "[ {" + LS
      + "  \"id\" : \"auth-1\"" + LS
      + "} ]";
    c.given().get("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(200).body(equalTo(exp1));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // get the auth enabled record
    final String expAuthEnabled = "{" + LS
      + "  \"id\" : \"auth-1\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given().get("/_/proxy/tenants/" + okapiTenant + "/modules/auth-1")
      .then().statusCode(200).body(equalTo(expAuthEnabled));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // Enable with bad JSON
    given()
      .header("Content-Type", "application/json")
      .body("{").post("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(400);

    // Enable the sample
    // Note that we can do this without the auth token. The test-auth module
    // will create a non-login token certifying that we do not have a login,
    // but will allow requests to any /_/ path,
    final String docEnableSample = "{" + LS
      + "  \"id\" : \"sample-module-1\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(docEnableSample).post("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().log().ifValidationFails()
      .statusCode(201)
      .body(equalTo(docEnableSample));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // Try to enable it again, should fail
    given()
      .header("Content-Type", "application/json")
      .body(docEnableSample).post("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(400)
      .body(containsString("already provided"));

    given().get("/_/proxy/tenants/" + okapiTenant + "/modules/")
      .then().statusCode(404); // trailing slash

    c = api.createRestAssured3();
    final String expEnabledBoth = "[ {" + LS
      + "  \"id\" : \"auth-1\"" + LS
      + "}, {" + LS
      + "  \"id\" : \"sample-module-1\"" + LS
      + "} ]";
    c.given().get("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(200).body(equalTo(expEnabledBoth));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // Try to disable the auth module for the tenant.
    // Ought to fail, because it is needed by sample module
    given().delete("/_/proxy/tenants/" + okapiTenant + "/modules/auth-1")
      .then().statusCode(400);

    // Update the tenant
    String docTenant = "{" + LS
      + "  \"id\" : \"" + okapiTenant + "\"," + LS
      + "  \"name\" : \"Roskilde-library\"," + LS
      + "  \"description\" : \"Roskilde bibliotek\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(docTenant).put("/_/proxy/tenants/" + okapiTenant)
      .then().statusCode(200)
      .body(equalTo(docTenant));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // Check that both modules are still enabled
    c = api.createRestAssured3();
    c.given().get("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(200).body(equalTo(expEnabledBoth));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // Request without any X-Okapi headers
    given()
      .get("/testb")
      .then().statusCode(404).body(equalTo("No suitable module found for path /testb for tenant supertenant"));

    // Request with a header, to unknown path
    // (note, should fail without invoking the auth module)
    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .get("/something.we.do.not.have")
      .then().statusCode(404)
      .body(equalTo("No suitable module found for path /something.we.do.not.have for tenant roskilde"));

    // Request without an auth token
    // In theory, this is acceptable, we should get back a token that certifies
    // that we have no logged-in username. We can use this for modulePermissions
    // still. A real auth module would be likely to refuse the request because
    // we do not have the necessary ModulePermissions.
    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .header("X-all-headers", "B") // ask sample to report all headers
      .get("/testb")
      .then().log().ifValidationFails()
      .statusCode(401);

    // Failed login
    final String docWrongLogin = "{" + LS
      + "  \"tenant\" : \"t1\"," + LS
      + "  \"username\" : \"peter\"," + LS
      + "  \"password\" : \"peter-wrong-password\"" + LS
      + "}";
    given().header("Content-Type", "application/json").body(docWrongLogin)
      .header("X-Okapi-Tenant", okapiTenant).post("/authn/login")
      .then().statusCode(401);

    // Ok login, get token
    final String docLogin = "{" + LS
      + "  \"tenant\" : \"" + okapiTenant + "\"," + LS
      + "  \"username\" : \"peter\"," + LS
      + "  \"password\" : \"peter-password\"" + LS
      + "}";
    String okapiToken = given().header("Content-Type", "application/json").body(docLogin)
      .header("X-Okapi-Tenant", okapiTenant).post("/authn/login")
      .then().statusCode(200).extract().header("X-Okapi-Token");

    // Actual requests to the module
    // Check the X-Okapi-Url header in, as well as URL parameters.
    // X-Okapi-Filter can not be checked here, but the log shows that it gets
    // passed to the auth filter, and not to the handler.
    // Check that the auth module has seen the right X-Okapi-Permissions-Required
    // and -Desired, it returns them in X-Auth-Permissions-Required and -Desired.
    // The X-Okapi-Permissions-Required and -Desired can not be checked here
    // directly, since Okapi sanitizes them away after invoking the auth module.
    // The auth module should return X-Okapi-Permissions to the sample module
    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .header("X-all-headers", "HBL") // ask sample to report all headers
      .get("/testb?query=foo&limit=10")
      .then().statusCode(200)
      .log().ifValidationFails()
      .header("X-Okapi-Url", "http://localhost:9230") // no trailing slash!
      .header("X-Okapi-User-Id", "peter")
      .header("X-Url-Params", "query=foo&limit=10")
      .header("X-Okapi-Permissions", containsString("sample.extra"))
      .header("X-Okapi-Permissions", containsString("auth.extra"))
      .header("X-Auth-Permissions-Desired", containsString("auth.extra"))
      .header("X-Auth-Permissions-Desired", containsString("sample.extra"))
      .header("X-Auth-Permissions-Required", "sample.needed")
      .body(containsString("It works"));
    // Check the CORS headers.
    // The presence of the Origin header should provoke the two extra headers.
    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .header("Origin", "http://foobar.com")
      .get("/testb")
      .then().statusCode(200)
      .header("Access-Control-Allow-Origin", "*")
      .header("Access-Control-Expose-Headers", containsString(
        "ocation,X-Okapi-Trace,X-Okapi-Token,Authorization,X-Okapi-Request-Id"))
      .body(equalTo("It works"));

    // Post request.
    // Test also URL parameters.
    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .header("Content-Type", "text/plain")
      .header("Accept", "text/xml")
      .header("X-all-headers", "H") // ask sample to report all headers
      .body("Okapi").post("/testb?query=foo")
      .then().statusCode(200)
      .header("X-Url-Params", "query=foo")
      .header("Content-Type", "text/xml")
      .body(equalTo("<test>hej Okapi</test>"));

    // Verify that the path matching is case sensitive
    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .get("/TESTB")
      .then().statusCode(404);

    // See that a delete fails - we only match auth, which is a filter
    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .delete("/testb")
      .then().statusCode(404);

    // Check that we don't do prefix matching
    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .get("/testbZZZ")
      .then().statusCode(404);

    // Check that parameters don't mess with the routing
    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .get("/testb?p=parameters&q=query")
      .then().statusCode(200);

    // Check that we called the tenant init
    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .header("X-tenant-reqs", "yes")
      .get("/testb")
      .then()
      .statusCode(200) // No longer expects a DELETE. See Okapi-252
      .body(equalTo("It works Tenant requests: POST-roskilde-auth "))
      .log().ifValidationFails();

    // Check that we refuse unknown paths, even with auth module
    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .get("/something.we.do.not.have")
      .then().statusCode(404);

    // Check that we accept Authorization: Bearer <token> instead of X-Okapi-Token,
    // and that we can extract the tenant from it.
    given()
      .header("X-all-headers", "H") // ask sample to report all headers
      .header("Authorization", "Bearer " + okapiToken)
      .get("/testb")
      .then().log().ifValidationFails()
      .header("X-Okapi-Tenant", okapiTenant)
      .statusCode(200);
    // Note that we can not check the token, the module sees a different token,
    // created by the auth module, when it saw a ModulePermission for the sample
    // module. This is all right, since we explicitly ask sample to pass its
    // request headers into its response. See Okapi-266.

    // Check that we accept Authorization without lead  "Bearer" <token>
    // instead of X-Okapi-Token, and that we can extract the tenant from it.
    given()
      .header("X-all-headers", "H") // ask sample to report all headers
      .header("Authorization", okapiToken)
      .get("/testb")
      .then().log().ifValidationFails()
      .header("X-Okapi-Tenant", okapiTenant)
      .statusCode(200);

    // Check that we fail on conflicting X-Okapi-Token and Auth tokens
    given().header("X-all-headers", "H") // ask sample to report all headers
      .header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .header("Authorization", "Bearer " + okapiToken + "WRONG")
      .get("/testb")
      .then().log().ifValidationFails()
      .statusCode(400);

    // Check that we fail on invalid Token/Authorization
    given().header("X-all-headers", "H") // ask sample to report all headers
      .header("X-Okapi-Token", "xx")
      .header("Authorization", "Bearer xx")
      .get("/testb")
      .then().log().ifValidationFails()
      .statusCode(400);

    // Declare sample2
    final String docSample2Module = "{" + LS
      + "  \"id\" : \"sample-module2-1\"," + LS
      + "  \"name\" : \"another-sample-module2\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"_tenant\"," + LS
      + "    \"version\" : \"1.0\"" + LS
      + "  } ]," + LS
      + "  \"filters\" : [ {" + LS
      + "    \"methods\" : [ \"GET\", \"POST\" ]," + LS
      + "    \"path\" : \"/testb\"," + LS
      + "    \"level\" : \"31\"," + LS
      + "    \"type\" : \"request-response\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  } ]" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docSample2Module).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    final String locationSample2Module = r.getHeader("Location");

    // 2nd sample module. We only create it in discovery and give it same URL as
    // for sample-module (first one). Then we delete it again.
    c = api.createRestAssured3();
    final String docSample2Deployment = "{" + LS
      + "  \"instId\" : \"sample2-inst\"," + LS
      + "  \"srvcId\" : \"sample-module2-1\"," + LS
      + "  \"url\" : \"http://localhost:9232\"" + LS
      + "}";
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docSample2Deployment).post("/_/discovery/modules")
      .then()
      .statusCode(201).extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    final String locationSample2Discovery = r.header("Location");

    // Get the sample-2
    c = api.createRestAssured3();
    c.given().get("/_/discovery/modules/sample-module2-1")
      .then().statusCode(200)
      .log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // and its instance
    c = api.createRestAssured3();
    c.given().get("/_/discovery/modules/sample-module2-1/sample2-inst")
      .then().statusCode(200)
      .log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // Get with unknown instanceId AND serviceId
    c = api.createRestAssured3();
    c.given().get("/_/discovery/modules/foo/xyz")
      .then().statusCode(404)
      .log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // Get with unknown instanceId
    c = api.createRestAssured3();
    c.given().get("/_/discovery/modules/sample-module2-1/xyz")
      .then().statusCode(404)
      .log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // Get with unknown serviceId
    c = api.createRestAssured3();
    c.given().get("/_/discovery/modules/foo/sample2-inst")
      .then().statusCode(404)
      .log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // health check
    c = api.createRestAssured3();
    c.given().get("/_/discovery/health")
      .then().statusCode(200);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // health for sample2
    c = api.createRestAssured3();
    c.given().get("/_/discovery/health/sample-module2-1")
      .then().statusCode(200);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // health for an instance
    c = api.createRestAssured3();
    c.given().get("/_/discovery/health/sample-module2-1/sample2-inst")
      .then().statusCode(200);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // Health with unknown instanceId
    c = api.createRestAssured3();
    c.given().get("/_/discovery/health/sample-module2-1/xyz")
      .then().statusCode(404)
      .log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // health with unknown serviceId
    c = api.createRestAssured3();
    c.given().get("/_/discovery/health/foo/sample2-inst")
      .then().statusCode(404)
      .log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // enable sample2
    final String docEnableSample2 = "{" + LS
      + "  \"id\" : \"sample-module2-1\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(docEnableSample2).post("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(201)
      .body(equalTo(docEnableSample2));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // disable it, and re-enable.
    // Later we will check that we got the right calls in its
    // tenant interface.
    given()
      .delete("/_/proxy/tenants/" + okapiTenant + "/modules/sample-module2-1")
      .then().statusCode(204);
    given()
      .header("Content-Type", "application/json")
      .body(docEnableSample2).post("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(201)
      .body(equalTo(docEnableSample2));

    final String docSample3Module = "{" + LS
      + "  \"id\" : \"sample-module3-1\"," + LS
      + "  \"name\" : \"sample-module3\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"_tenant\"," + LS
      + "    \"version\" : \"1.0\"" + LS
      + "  } ]," + LS
      + "  \"filters\" : [ {" + LS
      + "    \"methods\" : [ \"GET\", \"POST\" ]," + LS
      + "    \"path\" : \"/testb\"," + LS
      + "    \"level\" : \"05\"," + LS
      + "    \"type\" : \"headers\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  }, {" + LS
      + "    \"methods\" : [ \"GET\", \"POST\" ]," + LS
      + "    \"path\" : \"/testb\"," + LS
      + "    \"level\" : \"45\"," + LS
      + "    \"type\" : \"headers\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  }, {" + LS
      + "    \"methods\" : [ \"GET\", \"POST\" ]," + LS
      + "    \"path\" : \"/testb\"," + LS
      + "    \"level\" : \"33\"," + LS
      + "    \"type\" : \"request-only\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  } ]" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docSample3Module).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    final String locationSample3Module = r.getHeader("Location");

    // 3rd sample module. We only create it in discovery and give it same URL as
    // for sample-module (first one), just like sample2 above.
    final String docSample3Deployment = "{" + LS
      + "  \"instId\" : \"sample3-instance\"," + LS
      + "  \"srvcId\" : \"sample-module3-1\"," + LS
      + "  \"url\" : \"http://localhost:9232\"" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docSample3Deployment).post("/_/discovery/modules")
      .then()
      .statusCode(201).extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    final String locationSample3Inst = r.getHeader("Location");
    logger.debug("Deployed: locationSample3Inst " + locationSample3Inst);

    // same instId but different module .. must result in error
    final String docSample3DeploymentError = "{" + LS
      + "  \"instId\" : \"sample3-instance\"," + LS
      + "  \"srvcId\" : \"sample-module2-1\"," + LS
      + "  \"url\" : \"http://localhost:9232\"" + LS
      + "}";
    c.given()
      .header("Content-Type", "application/json")
      .body(docSample3DeploymentError).post("/_/discovery/modules")
      .then()
      .statusCode(400).body(equalTo("Duplicate instId sample3-instance"));

    final String docEnableSample3 = "{" + LS
      + "  \"id\" : \"sample-module3-1\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(docEnableSample3).post("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(201)
      .header("Location", equalTo("/_/proxy/tenants/" + okapiTenant + "/modules/sample-module3-1"))
      .log().ifValidationFails()
      .body(equalTo(docEnableSample3));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .get("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(200);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .get("/_/proxy/tenants/" + "unknown" + "/modules")
      .then().statusCode(404);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .get("/_/proxy/tenants/" + "unknown" + "/modules/unknown")
      .then().statusCode(404);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .get("/testb")
      .then().statusCode(200).body(equalTo("It works"));

    // Verify that both modules get executed
    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .body("OkapiX").post("/testb")
      .then()
      .log().ifValidationFails()
      .statusCode(200)
      .body(equalTo("hej hej OkapiX"));

    // Verify that we have seen tenant requests to POST but not DELETE
    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .header("X-tenant-reqs", "yes")
      .get("/testb")
      .then()
      .statusCode(200) // No longer expects a DELETE. See Okapi-252
      .body(containsString("POST-roskilde-auth POST-roskilde-auth"))
      .log().ifValidationFails();

    // Check that the X-Okapi-Stop trick works. Sample will set it if it sees
    // a X-Stop-Here header.
    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .header("X-Stop-Here", "Enough!")
      .body("OkapiX").post("/testb")
      .then().statusCode(200)
      .header("X-Okapi-Stop", "Enough!")
      .body(equalTo("hej OkapiX")); // only one "Hello"

    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .header("Content-Type", "text/xml")
      .get("/testb")
      .then().statusCode(200).body(equalTo("It works"));

    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .header("Content-Type", "text/plain")
      .header("Accept", "text/xml")
      .body("OkapiX").post("/testb")
      .then().statusCode(200).body(equalTo("hej <test>hej OkapiX</test>"));

    c = api.createRestAssured3();
    final String exp4Modules = "[ {" + LS
      + "  \"id\" : \"auth-1\"" + LS
      + "}, {" + LS
      + "  \"id\" : \"sample-module-1\"" + LS
      + "}, {" + LS
      + "  \"id\" : \"sample-module2-1\"" + LS
      + "}, {" + LS
      + "  \"id\" : \"sample-module3-1\"" + LS
      + "} ]";
    c.given().get(locationTenantRoskilde + "/modules")
      .then().statusCode(200)
      .body(equalTo(exp4Modules));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given().delete(locationTenantRoskilde + "/modules/sample-module3-1")
      .then().statusCode(204);
    Assert.assertTrue(c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    final String exp3Modules = "[ {" + LS
      + "  \"id\" : \"auth-1\"" + LS
      + "}, {" + LS
      + "  \"id\" : \"sample-module-1\"" + LS
      + "}, {" + LS
      + "  \"id\" : \"sample-module2-1\"" + LS
      + "} ]";
    c.given().get(locationTenantRoskilde + "/modules")
      .then().statusCode(200)
      .body(equalTo(exp3Modules));
    Assert.assertTrue(c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given().get("/_/discovery/modules")
      .then().statusCode(200)
      .log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // make sample 2 disappear from discovery!
    c = api.createRestAssured3();
    c.given().delete(locationSample2Discovery)
      .then().statusCode(204);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given().get("/_/discovery/modules")
      .then().statusCode(200)
      .log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .header("Content-Type", "text/xml")
      .get("/testb")
      .then().statusCode(404); // because sample2 was removed

    // Disable the sample module. No tenant-destroy for sample
    c = api.createRestAssured3();
    c.given()
      .delete("/_/proxy/tenants/" + okapiTenant + "/modules/sample-module-1?purge=true")
      .then().statusCode(204);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
        c.getLastReport().isEmpty());

    // Disable the sample2 module + auth-1. It has a tenant request handler which is
    // no longer invoked, so it does not matter we don't have a running instance

    c = api.createRestAssured3();
    c.given()
      .delete("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(204);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    given()
      .get("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(200).body(equalTo("[ ]"));

    c = api.createRestAssured3();
    c.given()
      .delete("/_/proxy/tenants/unknown-tenant/modules")
      .then().statusCode(400);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given().delete(locationTenantRoskilde)
      .then().statusCode(204);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // Clean up, so the next test starts with a clean slate
    given().delete(locationSample3Inst).then().log().ifValidationFails().statusCode(204);
    given().delete(locationSample3Module).then().log().ifValidationFails().statusCode(204);
    given().delete("/_/proxy/modules/sample-module-1").then().log().ifValidationFails().statusCode(204);
    given().delete("/_/proxy/modules/sample-module2-1").then().log().ifValidationFails().statusCode(204);
    given().delete("/_/proxy/modules/auth-1").then().log().ifValidationFails().statusCode(204);
    given().delete(locationAuthDeployment).then().log().ifValidationFails().statusCode(204);
    locationAuthDeployment = null;
    given().delete(locationSampleDeployment).then().log().ifValidationFails().statusCode(204);
    locationSampleDeployment = null;
  }


  /*
   * Test redirect types. Sets up two modules, our sample, and the header test
   * module.
   *
   * Both modules support the /testb path.
   * Test also supports /testr path.
   * Header will redirect /red path to /testr, which will end up in the test module.
   * Header will also attempt to support /loop, /loop1, and /loop2 for testing
   * looping redirects. These are expected to fail.
   *
   */
  @Test
  public void testRedirect(TestContext context) {
    Async async = context.async();
    RestAssuredClient c;
    Response r;
    final String okapiTenant = "roskilde";

    // Set up a tenant to test with
    final String docTenantRoskilde = "{" + LS
      + "  \"id\" : \"" + okapiTenant + "\"," + LS
      + "  \"name\" : \"" + okapiTenant + "\"," + LS
      + "  \"description\" : \"Roskilde bibliotek\"" + LS
      + "}";

    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docTenantRoskilde).post("/_/proxy/tenants")
      .then().statusCode(201)
      .log().ifValidationFails()
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    final String locationTenantRoskilde = r.getHeader("Location");

    // Set up, deploy, and enable a sample module
    final String docSampleModule = "{" + LS
      + "  \"id\" : \"sample-module-1\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"sample\"," + LS
      + "    \"interfaceType\" : \"proxy\"," + LS
      + "    \"version\" : \"1.0\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"GET\", \"POST\" ]," + LS
      + "      \"path\" : \"/testb\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"GET\", \"POST\" ]," + LS
      + "      \"path\" : \"/testr\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"filters\" : [ {" + LS
      + "    \"methods\" : [ \"GET\" ]," + LS
      + "    \"path\" : \"/loop2\"," + LS
      + "    \"level\" : \"22\"," + LS
      + "    \"type\" : \"redirect\"," + LS
      + "    \"redirectPath\" : \"/loop1\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  }, {" + LS
      + "    \"modulePermissions\" : [ \"sample.modperm\" ]," + LS
      + "    \"methods\" : [ \"GET\" ]," + LS
      + "    \"path\" : \"/chain3\"," + LS
      + "    \"level\" : \"23\"," + LS
      + "    \"type\" : \"redirect\"," + LS
      + "    \"redirectPath\" : \"/testr\"," + LS
      + "    \"permissionsRequired\" : [ ]," + LS
      + "    \"permissionsDesired\" : [ \"sample.chain3\" ]" + LS
      + "  } ]," + LS
      + "  \"launchDescriptor\" : {" + LS
      + "    \"exec\" : \"java -Dport=%p -jar ../okapi-test-module/target/okapi-test-module-fat.jar\"" + LS
      + "  }" + LS
      + "}";

    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docSampleModule).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    final String locationSampleModule = r.getHeader("Location");

    final String docSampleDeploy = "{" + LS
      + "  \"srvcId\" : \"sample-module-1\"," + LS
      + "  \"nodeId\" : \"localhost\"" + LS
      + "}";

    c = api.createRestAssured3();
    r = c.given().header("Content-Type", "application/json")
      .body(docSampleDeploy).post("/_/discovery/modules")
      .then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    String locationSampleDeployment = r.getHeader("Location");

    final String docEnableSample = "{" + LS
      + "  \"id\" : \"sample-module-1\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(docEnableSample).post("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(201).body(equalTo(docEnableSample));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .get("/testr")
      .then().statusCode(200)
      .body(containsString("It works"))
      .log().ifValidationFails();

    // Set up, deploy, and enable the header module
    final String docHeaderModule = "{" + LS
      + "  \"id\" : \"header-module-1\"," + LS
      + "  \"filters\" : [ {" + LS
      + "    \"methods\" : [ \"GET\", \"POST\" ]," + LS
      + "    \"path\" : \"/red\"," + LS
      + "    \"level\" : \"21\"," + LS
      + "    \"type\" : \"redirect\"," + LS
      + "    \"redirectPath\" : \"/testr\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  }, {" + LS
      + "    \"methods\" : [ \"GET\" ]," + LS
      + "    \"path\" : \"/badredirect\"," + LS
      + "    \"level\" : \"22\"," + LS
      + "    \"type\" : \"redirect\"," + LS
      + "    \"redirectPath\" : \"/nonexisting\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  }, {" + LS
      + "    \"methods\" : [ \"GET\" ]," + LS
      + "    \"path\" : \"/simpleloop\"," + LS
      + "    \"level\" : \"23\"," + LS
      + "    \"type\" : \"redirect\"," + LS
      + "    \"redirectPath\" : \"/simpleloop\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  }, {" + LS
      + "    \"methods\" : [ \"GET\" ]," + LS
      + "    \"path\" : \"/loop1\"," + LS
      + "    \"level\" : \"24\"," + LS
      + "    \"type\" : \"redirect\"," + LS
      + "    \"redirectPath\" : \"/loop2\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  }, {" + LS
      + "    \"modulePermissions\" : [ \"hdr.modperm\" ]," + LS
      + "    \"methods\" : [ \"GET\" ]," + LS
      + "    \"path\" : \"/chain1\"," + LS
      + "    \"level\" : \"25\"," + LS
      + "    \"type\" : \"redirect\"," + LS
      + "    \"redirectPath\" : \"/chain2\"," + LS
      + "    \"permissionsRequired\" : [ ]," + LS
      + "    \"permissionsDesired\" : [ \"hdr.chain1\" ]" + LS
      + "  }, {" + LS
      + "    \"methods\" : [ \"GET\" ]," + LS
      + "    \"path\" : \"/chain2\"," + LS
      + "    \"level\" : \"26\"," + LS
      + "    \"type\" : \"redirect\"," + LS
      + "    \"redirectPath\" : \"/chain3\"," + LS
      + "    \"permissionsRequired\" : [ ]," + LS
      + "    \"permissionsDesired\" : [ \"hdr.chain2\" ]" + LS
      + "  }, {" + LS
      + "    \"methods\" : [ \"POST\" ]," + LS
      + "    \"path\" : \"/multiple\"," + LS
      + "    \"level\" : \"27\"," + LS
      + "    \"type\" : \"redirect\"," + LS
      + "    \"redirectPath\" : \"/testr\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  }, {" + LS
      + "    \"methods\" : [ \"POST\" ]," + LS
      + "    \"path\" : \"/multiple\"," + LS
      + "    \"level\" : \"28\"," + LS
      + "    \"type\" : \"redirect\"," + LS
      + "    \"redirectPath\" : \"/testr\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  } ]," + LS
      + "  \"launchDescriptor\" : {" + LS
      + "    \"exec\" : \"java -Dport=%p -jar ../okapi-test-header-module/target/okapi-test-header-module-fat.jar\"" + LS
      + "  }" + LS
      + "}";

    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docHeaderModule).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    final String locationHeaderModule = r.getHeader("Location");

    final String docHeaderDeploy = "{" + LS
      + "  \"srvcId\" : \"header-module-1\"," + LS
      + "  \"nodeId\" : \"localhost\"" + LS
      + "}";

    c = api.createRestAssured3();
    r = c.given().header("Content-Type", "application/json")
      .body(docHeaderDeploy).post("/_/discovery/modules")
      .then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    String locationHeaderDeployment = r.getHeader("Location");

    final String docEnableHeader = "{" + LS
      + "  \"id\" : \"header-module-1\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(docEnableHeader).post("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(201).body(equalTo(docEnableHeader));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .get("/testb")
      .then().statusCode(200)
      .body(containsString("It works"))
      .log().ifValidationFails();

    // Actual redirecting request
    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .get("/red")
      .then().statusCode(200)
      .body(containsString("It works"))
      .header("X-Okapi-Trace", containsString("GET sample-module-1 http://localhost:9231/testr"))
      .log().ifValidationFails();

    // Bad redirect
    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .get("/badredirect")
      .then().statusCode(500)
      .body(equalTo("Redirecting /badredirect to /nonexisting FAILED. No suitable module found"))
      .log().ifValidationFails();

    // catch redirect loops
    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .get("/simpleloop")
      .then().statusCode(500)
      .body(containsString("loop:"))
      .log().ifValidationFails();

    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .get("/loop1")
      .then().statusCode(500)
      .body(containsString("loop:"))
      .log().ifValidationFails();

    // redirect to multiple modules
    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .header("Content-Type", "application/json")
      .body("{}")
      .post("/multiple")
      .then().statusCode(200)
      .body(containsString("Hello Hello")) // test-module run twice
      .log().ifValidationFails();

    // Redirect with parameters
    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .get("/red?foo=bar")
      .then().statusCode(200)
      .body(containsString("It works"))
      .log().ifValidationFails();

    // A longer chain of redirects
    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .header("X-all-headers", "B")
      .get("/chain1")
      .then().statusCode(200)
      .body(containsString("It works"))
      // No auth header should be included any more, since we don't have an auth filter
      .log().ifValidationFails();

    // What happens on prefix match
    // /red matches, replaces with /testr, getting /testrlight which is not found
    // This is odd, and subotimal, but not a serious failure. okapi-253
    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .get("/redlight")
      .then().statusCode(404)
      .header("X-Okapi-Trace", containsString("sample-module-1 http://localhost:9231/testrlight : 404"))
      .log().ifValidationFails();

    // Verify that we replace only the beginning of the path
    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .get("/red/blue/red?color=/red")
      .then().statusCode(404)
      .log().ifValidationFails();

    // Clean up
    given().delete(locationTenantRoskilde)
      .then().statusCode(204);
    given().delete(locationSampleModule)
      .then().statusCode(204);
    given().delete(locationSampleDeployment)
      .then().statusCode(204);
    locationSampleDeployment = null;
    given().delete(locationHeaderModule)
      .then().statusCode(204);
    given().delete(locationHeaderDeployment)
      .then().statusCode(204);
    locationHeaderDeployment = null;

    async.complete();
  }

  @Test
  public void testRequestOnly(TestContext context) {
    final String okapiTenant = "roskilde";
    RestAssuredClient c;
    Response r;

    // add tenant
    final String docTenantRoskilde = "{" + LS
      + "  \"id\" : \"" + okapiTenant + "\"," + LS
      + "  \"name\" : \"" + okapiTenant + "\"," + LS
      + "  \"description\" : \"Roskilde bibliotek\"" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docTenantRoskilde).post("/_/proxy/tenants")
      .then().statusCode(201)
      .body(equalTo(docTenantRoskilde))
      .extract().response();
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());
    final String locationTenantRoskilde = r.getHeader("Location");

    final String docRequestPre = "{" + LS
      + "  \"id\" : \"request-pre-1.0.0\"," + LS
      + "  \"name\" : \"request-pre\"," + LS
      + "  \"filters\" : [ {" + LS
      + "    \"methods\" : [ \"GET\", \"POST\" ]," + LS
      + "    \"path\" : \"/testb\"," + LS
      + "    \"phase\" : \"pre\"," + LS
      + "    \"type\" : \"request-log\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  } ]" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docRequestPre).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String docRequestPost = "{" + LS
      + "  \"id\" : \"request-post-1.0.0\"," + LS
      + "  \"name\" : \"request-post\"," + LS
      + "  \"filters\" : [ {" + LS
      + "    \"methods\" : [ \"GET\", \"POST\" ]," + LS
      + "    \"path\" : \"/testb\"," + LS
      + "    \"phase\" : \"post\"," + LS
      + "    \"type\" : \"request-log\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  } ]" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docRequestPost).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String docRequestOnly = "{" + LS
      + "  \"id\" : \"request-only-1.0.0\"," + LS
      + "  \"name\" : \"request-only\"," + LS
      + "  \"filters\" : [ {" + LS
      + "    \"methods\" : [ \"GET\", \"POST\" ]," + LS
      + "    \"path\" : \"/testb\"," + LS
      + "    \"level\" : \"33\"," + LS
      + "    \"type\" : \"request-only\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  } ]," + LS
      + "  \"launchDescriptor\" : {" + LS
      + "    \"exec\" : "
      + "\"java -Dport=%p -jar ../okapi-test-module/target/okapi-test-module-fat.jar\"" + LS
      + "  }" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docRequestOnly).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String testAuthJar = "../okapi-test-auth-module/target/okapi-test-auth-module-fat.jar";
    final String docAuthModule = "{" + LS
      + "  \"id\" : \"auth-f-module-1\"," + LS
      + "  \"name\" : \"auth\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"auth\"," + LS
      + "    \"version\" : \"1.2\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"path\" : \"/authn/login\"," + LS
      + "      \"level\" : \"20\"," + LS
      + "      \"type\" : \"request-response\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"filters\" : [ {" + LS
      + "    \"methods\" : [ \"*\" ]," + LS
      + "    \"path\" : \"/\"," + LS
      + "    \"phase\" : \"auth\"," + LS
      + "    \"type\" : \"headers\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]," + LS
      + "  \"launchDescriptor\" : {" + LS
      + "    \"exec\" : \"java -Dport=%p -jar " + testAuthJar + "\"" + LS
      + "  }" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docAuthModule).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String docSample = "{" + LS
      + "  \"id\" : \"sample-module-1.0.0\"," + LS
      + "  \"name\" : \"sample-module\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"_tenant\"," + LS
      + "    \"version\" : \"1.0\"" + LS
      + "  }, {" + LS
      + "    \"id\" : \"myfirst\"," + LS
      + "    \"version\" : \"1.0\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"GET\", \"POST\", \"DELETE\"]," + LS
      + "      \"path\" : \"/testb\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"launchDescriptor\" : {" + LS
      + "    \"exec\" : "
      + "\"java -Dport=%p -jar ../okapi-test-module/target/okapi-test-module-fat.jar\"" + LS
      + "  }" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docSample).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("[ {\"id\" : \"sample-module-1.0.0\", \"action\" : \"enable\"},"
        + " {\"id\" : \"request-only-1.0.0\", \"action\" : \"enable\"} ]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install?deploy=true")
      .then().statusCode(200).log().ifValidationFails()
      .body(equalTo("[ {" + LS
        + "  \"id\" : \"sample-module-1.0.0\"," + LS
        + "  \"action\" : \"enable\"" + LS
        + "}, {" + LS
        + "  \"id\" : \"request-only-1.0.0\"," + LS
        + "  \"action\" : \"enable\"" + LS
        + "} ]"));
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    given().header("X-Okapi-Tenant", okapiTenant)
      .header("Content-Type", "text/plain")
      .header("Accept", "text/xml")
      .body("Okapi").post("/testb")
      .then().statusCode(200)
      .header("Content-Type", "text/xml")
      .body(equalTo("<test>Hello Okapi</test>"));

    given().header("X-Okapi-Tenant", okapiTenant)
      .header("Content-Type", "text/plain")
      .header("X-Handler-error", "true")
      .header("Accept", "text/xml")
      .body("Okapi").post("/testb")
      .then().statusCode(500)
      .body(equalTo("Okapi"));

    final String nodeDoc1 = "{" + LS
      + "  \"instId\" : \"localhost-" + Integer.toString(portPre) + "\"," + LS
      + "  \"srvcId\" : \"request-pre-1.0.0\"," + LS
      + "  \"url\" : \"http://localhost:" + Integer.toString(portPre) + "\"" + LS
      + "}";

    c = api.createRestAssured3();
    c.given().header("Content-Type", "application/json")
      .body(nodeDoc1).post("/_/discovery/modules")
      .then().statusCode(201);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String nodeDoc2 = "{" + LS
      + "  \"instId\" : \"localhost-" + Integer.toString(portPost) + "\"," + LS
      + "  \"srvcId\" : \"request-post-1.0.0\"," + LS
      + "  \"url\" : \"http://localhost:" + Integer.toString(portPost) + "\"" + LS
      + "}";

    c = api.createRestAssured3();
    c.given().header("Content-Type", "application/json")
      .body(nodeDoc2).post("/_/discovery/modules")
      .then().statusCode(201);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("[ {\"id\" : \"request-pre-1.0.0\", \"action\" : \"enable\"},"
        + " {\"id\" : \"auth-f-module-1\", \"action\" : \"enable\"},"
        + " {\"id\" : \"request-only-1.0.0\", \"action\" : \"disable\"} ]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install?deploy=true")
      .then().statusCode(200).log().ifValidationFails()
      .body(equalTo("[ {" + LS
        + "  \"id\" : \"request-pre-1.0.0\"," + LS
        + "  \"action\" : \"enable\"" + LS
        + "}, {" + LS
        + "  \"id\" : \"auth-f-module-1\"," + LS
        + "  \"action\" : \"enable\"" + LS
        + "}, {" + LS
        + "  \"id\" : \"request-only-1.0.0\"," + LS
        + "  \"action\" : \"disable\"" + LS
        + "} ]"));
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // login and get token
    final String docLogin = "{" + LS
      + "  \"tenant\" : \"" + okapiTenant + "\"," + LS
      + "  \"username\" : \"peter\"," + LS
      + "  \"password\" : \"peter-password\"" + LS
      + "}";
    final String okapiToken = given().header("Content-Type", "application/json").body(docLogin)
      .header("X-Okapi-Tenant", okapiTenant).post("/authn/login")
      .then().statusCode(200).extract().header("X-Okapi-Token");

    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .header("Content-Type", "text/plain")
      .header("Accept", "text/xml")
      .body("Okapi").post("/testb")
      .then().statusCode(200).log().ifValidationFails()
      .header("Content-Type", "text/xml")
      .body(equalTo("<test>Hello Okapi</test>"));
    Assert.assertEquals("Okapi", preBuffer.toString());

    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .header("Content-Type", "text/plain")
      .header("Accept", "text/xml")
      .delete("/testb")
      .then().statusCode(204).log().ifValidationFails();

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("[ {\"id\" : \"request-post-1.0.0\", \"action\" : \"enable\"} ]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install?deploy=true")
      .then().statusCode(200).log().ifValidationFails()
      .body(equalTo("[ {" + LS
        + "  \"id\" : \"request-post-1.0.0\"," + LS
        + "  \"action\" : \"enable\"" + LS
        + "} ]"));

    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .header("Content-Type", "text/plain")
      .header("Accept", "text/xml")
      .delete("/testb")
      .then().statusCode(204).log().ifValidationFails();

    given().header("X-Okapi-Tenant", okapiTenant)
      .header("X-Okapi-Token", okapiToken)
      .header("Content-Type", "text/plain")
      .header("Accept", "text/xml")
      .body("Okapi").post("/testb")
      .then().statusCode(200).log().ifValidationFails()
      .header("Content-Type", "text/xml")
      .body(equalTo("<test>Hello Okapi</test>"));

    Async async = context.async();
    vertx.setTimer(300, res -> {
      context.assertEquals("Okapi", preBuffer.toString());
      context.assertEquals("<test>Hello Okapi</test>", postBuffer.toString());
      context.assertNotNull(postHandlerHeaders);
      context.assertEquals("200", postHandlerHeaders.get(XOkapiHeaders.HANDLER_RESULT));
      async.complete();
    });
  }

  @Test
  public void testEdgeCase(TestContext context) {
    RestAssuredClient c;
    Response r;
    final String superTenant = "supertenant";

    final String testAuthJar = "../okapi-test-auth-module/target/okapi-test-auth-module-fat.jar";
    final String docAuthModule = "{" + LS
      + "  \"id\" : \"auth-module-1.0.0\"," + LS
      + "  \"name\" : \"auth\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"auth\"," + LS
      + "    \"version\" : \"1.2\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"path\" : \"/authn/login\"," + LS
      + "      \"level\" : \"20\"," + LS
      + "      \"type\" : \"request-response\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"filters\" : [ {" + LS
      + "    \"methods\" : [ \"*\" ]," + LS
      + "    \"path\" : \"/\"," + LS
      + "    \"phase\" : \"auth\"," + LS
      + "    \"type\" : \"headers\"," + LS
      + "    \"permissionsRequired\" : [ ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]," + LS
      + "  \"launchDescriptor\" : {" + LS
      + "    \"exec\" : \"java -Dport=%p -jar " + testAuthJar + "\"" + LS
      + "  }" + LS
      + "}";

    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docAuthModule).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String docBasic_1_0_0 = "{" + LS
      + "  \"id\" : \"basic-module-1.0.0\"," + LS
      + "  \"name\" : \"this module\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"_tenant\"," + LS
      + "    \"version\" : \"1.1\"," + LS
      + "    \"interfaceType\" : \"system\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant/disable\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"POST\", \"DELETE\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  }, {" + LS
      + "    \"id\" : \"myint\"," + LS
      + "    \"version\" : \"1.0\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"GET\", \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/testb/{id}\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]," + LS
      + "  \"launchDescriptor\" : {" + LS
      + "    \"exec\" : "
      + "\"java -Dport=%p -jar ../okapi-test-module/target/okapi-test-module-fat.jar\"" + LS
      + "  }" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docBasic_1_0_0).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String docEdge_1_0_0 = "{" + LS
      + "  \"id\" : \"edge-module-1.0.0\"," + LS
      + "  \"name\" : \"edge module\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"edge\"," + LS
      + "    \"version\" : \"1.0\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"GET\", \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/edge/{id}\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docEdge_1_0_0).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String nodeDiscoverEdge = "{" + LS
      + "  \"instId\" : \"localhost-" + Integer.toString(portEdge) + "\"," + LS
      + "  \"srvcId\" : \"edge-module-1.0.0\"," + LS
      + "  \"url\" : \"http://localhost:" + Integer.toString(portEdge) + "\"" + LS
      + "}";

    c = api.createRestAssured3();
    c.given().header("Content-Type", "application/json")
      .body(nodeDiscoverEdge).post("/_/discovery/modules")
      .then().statusCode(201);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"edge-module-1.0.0\", \"action\" : \"enable\"},"
        + " {\"id\" : \"auth-module-1.0.0\", \"action\" : \"enable\"}"
        + "]")
      .post("/_/proxy/tenants/" + superTenant + "/install?deploy=true")
      .then().statusCode(200).log().ifValidationFails();
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String docLogin = "{" + LS
      + "  \"tenant\" : \"" + "supertenant" + "\"," + LS
      + "  \"username\" : \"peter\"," + LS
      + "  \"password\" : \"peter-password\"" + LS
      + "}";
    final String okapiToken = c.given().header("Content-Type", "application/json").body(docLogin)
      .header("X-Okapi-Tenant", "supertenant").post("/authn/login")
      .then().statusCode(200).extract().header("X-Okapi-Token");

    c = api.createRestAssured3();
    given()
      .header("Content-Type", "application/json")
      .get("/_/proxy/tenants")
      .then().statusCode(200);

    final String okapiTenant = "roskilde";
    // add tenant
    final String docTenantRoskilde = "{" + LS
      + "  \"id\" : \"" + okapiTenant + "\"," + LS
      + "  \"name\" : \"" + okapiTenant + "\"," + LS
      + "  \"description\" : \"Roskilde bibliotek\"" + LS
      + "}";
    c = api.createRestAssured3();
    given()
      .header("X-Okapi-Token", okapiToken)
      .header("Content-Type", "application/json")
      .body(docTenantRoskilde).post("/_/proxy/tenants")
      .then().statusCode(201)
      .body(equalTo(docTenantRoskilde));

    given()
      .header("Content-Type", "text/plain")
      .header("Accept", "text/xml")
      .body("Okapi").get("/edge/roskilde")
      .then().statusCode(404).log().ifValidationFails();

    c = api.createRestAssured3();
    c.given()
      .header("X-Okapi-Token", okapiToken)
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"basic-module-1.0.0\", \"action\" : \"enable\"},"
        + " {\"id\" : \"auth-module-1.0.0\", \"action\" : \"enable\"}"
        + "]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install?deploy=true")
      .then().statusCode(200).log().ifValidationFails();

    given()
      .header("Content-Type", "text/plain")
      .header("Accept", "text/xml")
      .body("Okapi").get("/edge/roskilde")
      .then().statusCode(200).log().ifValidationFails()
      .body(equalTo("It works"));

    given()
      .header("Content-Type", "text/plain")
      .header("Accept", "text/xml")
      .body("Okapi").get("/edge/unknown")
      .then().statusCode(400).log().ifValidationFails()
      .body(equalTo("No such Tenant unknown"));

    given()
      .header("X-Okapi-Token", okapiToken)
      .delete("/_/discovery/modules")
      .then().statusCode(204).log().ifValidationFails();
  }

  @Test
  public void testTimer(TestContext context) {
    RestAssuredClient c;
    Response r;

    final String docTimer_1_0_0 = "{" + LS
      + "  \"id\" : \"timer-module-1.0.0\"," + LS
      + "  \"name\" : \"timer module\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"_tenant\"," + LS
      + "    \"version\" : \"1.1\"," + LS
      + "    \"interfaceType\" : \"system\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant/disable\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"POST\", \"DELETE\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  }, {" + LS
      + "    \"id\" : \"_timer\"," + LS
      + "    \"version\" : \"1.0\"," + LS
      + "    \"interfaceType\" : \"system\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/timercall/1\"," + LS
      + "      \"unit\" : \"millisecond\"," + LS
      + "      \"delay\" : \"10\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "   }, {" + LS
      + "      \"methods\" : [ \"GET\" ]," + LS
      + "      \"path\" : \"/timercall/3\"," + LS
      + "      \"unit\" : \"millisecond\"," + LS
      + "      \"delay\" : \"30\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  }, {" + LS
      + "    \"id\" : \"myint\"," + LS
      + "    \"version\" : \"1.0\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/timercall/{id}\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]" + LS
      + "}";

    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docTimer_1_0_0).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String nodeDoc1 = "{" + LS
      + "  \"instId\" : \"localhost-" + Integer.toString(portTimer) + "\"," + LS
      + "  \"srvcId\" : \"timer-module-1.0.0\"," + LS
      + "  \"url\" : \"http://localhost:" + Integer.toString(portTimer) + "\"" + LS
      + "}";

    c = api.createRestAssured3();
    c.given().header("Content-Type", "application/json")
      .body(nodeDoc1).post("/_/discovery/modules")
      .then().statusCode(201);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String okapiTenant = "roskilde";
    // add tenant
    final String docTenantRoskilde = "{" + LS
      + "  \"id\" : \"" + okapiTenant + "\"," + LS
      + "  \"name\" : \"" + okapiTenant + "\"," + LS
      + "  \"description\" : \"Roskilde bibliotek\"" + LS
      + "}";
    c = api.createRestAssured3();
    given()
      .header("Content-Type", "application/json")
      .body(docTenantRoskilde).post("/_/proxy/tenants")
      .then().statusCode(201)
      .body(equalTo(docTenantRoskilde));

    timerDelaySum = 0;
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"timer-module-1.0.0\", \"action\" : \"enable\"}"
        + "]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install?deploy=true")
      .then().statusCode(200).log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .header("Content-Type", "text/plain")
      .header("Accept", "text/plain")
      .body("Okapi").post("/timercall/100")
      .then().statusCode(200).log().ifValidationFails();

    // 10 msecond period and approx 100 total wait time.. 1 tick per call..
    context.assertTrue(timerDelaySum >= 103 && timerDelaySum <= 112, "Got " + timerDelaySum);
    logger.info("timerDelaySum=" + timerDelaySum);

    // disable and enable (quickly)
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"timer-module-1.0.0\", \"action\" : \"disable\"}"
        + "]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install?deploy=true")
      .then().statusCode(200).log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"timer-module-1.0.0\", \"action\" : \"enable\"}"
        + "]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install?deploy=true")
      .then().statusCode(200).log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // disable for some time...
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"timer-module-1.0.0\", \"action\" : \"disable\"}"
        + "]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install?deploy=true")
      .then().statusCode(200).log().ifValidationFails();

    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .header("Content-Type", "text/plain")
      .header("Accept", "text/plain")
      .body("Okapi").post("/timercall/100")
      .then().statusCode(404).log().ifValidationFails();

    try {
      TimeUnit.MILLISECONDS.sleep(100);
    } catch (InterruptedException ex) {
    }

    // enable again
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"timer-module-1.0.0\", \"action\" : \"enable\"}"
        + "]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install?deploy=true")
      .then().statusCode(200).log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .header("Content-Type", "text/plain")
      .header("Accept", "text/plain")
      .body("Okapi").post("/timercall/100")
      .then().statusCode(200).log().ifValidationFails();

    // disable and remove tenant as well
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"timer-module-1.0.0\", \"action\" : \"disable\"}"
        + "]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install?deploy=true")
      .then().statusCode(200).log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    given()
      .header("Content-Type", "application/json")
      .delete("/_/proxy/tenants/roskilde")
      .then().statusCode(204);

    try {
      TimeUnit.MILLISECONDS.sleep(100);
    } catch (InterruptedException ex) {
    }

  }

  @Test
  public void testTenantInit(TestContext context) {
    RestAssuredClient c;
    Response r;

    timerPermissions.clear();

    final String docTimer_1_0_0 = "{" + LS
      + "  \"id\" : \"timer-module-1.0.0\"," + LS
      + "  \"name\" : \"timer module\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"_tenant\"," + LS
      + "    \"version\" : \"1.1\"," + LS
      + "    \"interfaceType\" : \"system\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant/disable\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"POST\", \"DELETE\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  }, {" + LS
      + "    \"id\" : \"myint\"," + LS
      + "    \"version\" : \"1.0\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/timercall/{id}\"," + LS
      + "      \"permissionsRequired\" : [ \"timercall.post.id\" ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"DELETE\" ]," + LS
      + "      \"pathPattern\" : \"/timercall/{id}\"," + LS
      + "      \"permissionsRequired\" : [ \"timercall.delete.id\" ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]," + LS
      + "  \"permissionSets\": [ {" + LS
      + "    \"permissionName\": \"timercall.post.id\"" + LS
      + "  } ]" + LS
      + "}";

    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docTimer_1_0_0).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String nodeDoc1 = "{" + LS
      + "  \"instId\" : \"localhost-" + Integer.toString(portTimer) + "\"," + LS
      + "  \"srvcId\" : \"timer-module-1.0.0\"," + LS
      + "  \"url\" : \"http://localhost:" + Integer.toString(portTimer) + "\"" + LS
      + "}";

    c = api.createRestAssured3();
    c.given().header("Content-Type", "application/json")
      .body(nodeDoc1).post("/_/discovery/modules")
      .then().statusCode(201);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String okapiTenant = "roskilde";
    // add tenant
    final String docTenantRoskilde = "{" + LS
      + "  \"id\" : \"" + okapiTenant + "\"," + LS
      + "  \"name\" : \"" + okapiTenant + "\"," + LS
      + "  \"description\" : \"Roskilde bibliotek\"" + LS
      + "}";
    c = api.createRestAssured3();
    given()
      .header("Content-Type", "application/json")
      .body(docTenantRoskilde).post("/_/proxy/tenants")
      .then().statusCode(201)
      .body(equalTo(docTenantRoskilde));

    c = api.createRestAssured3();
    String body = c.given().header("Content-Type", "application/json")
      .get("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(200).extract().body().asString();
    JsonArray ar = new JsonArray(body);
    Assert.assertEquals(0, ar.size());

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"timer-module-1.0.0\", \"action\" : \"enable\"}"
        + "]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install")
      .then().statusCode(200).log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    Assert.assertEquals(0, timerPermissions.size());
    c = api.createRestAssured3();
    body = c.given().header("Content-Type", "application/json")
      .get("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(200).extract().body().asString();
    ar = new JsonArray(body);
    Assert.assertEquals(1, ar.size());
    Assert.assertEquals("timer-module-1.0.0", ar.getJsonObject(0).getString("id"));

    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .header("Content-Type", "text/plain")
      .header("Accept", "text/plain")
      .body("Okapi").post("/timercall/1")
      .then().statusCode(200).log().ifValidationFails();

    final String docTimer_1_0_1 = "{" + LS
      + "  \"id\" : \"timer-module-1.0.1\"," + LS
      + "  \"name\" : \"timer module\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"_tenant\"," + LS
      + "    \"version\" : \"1.1\"," + LS
      + "    \"interfaceType\" : \"system\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant/disable\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"POST\", \"DELETE\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  }, {" + LS
      + "    \"id\" : \"_tenantPermissions\"," + LS
      + "    \"version\" : \"1.1\"," + LS
      + "    \"interfaceType\" : \"system\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/permissionscall\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  }, {" + LS
      + "    \"id\" : \"myint\"," + LS
      + "    \"version\" : \"1.0\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/timercall/{id}\"," + LS
      + "      \"permissionsRequired\" : [ \"timercall.post.id\" ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"DELETE\" ]," + LS
      + "      \"pathPattern\" : \"/timercall/{id}\"," + LS
      + "      \"permissionsRequired\" : [ \"timercall.delete.id\" ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]," + LS
      + "  \"permissionSets\": [ {" + LS
      + "    \"permissionName\": \"timercall.post.id\"," + LS
      + "    \"displayName\": \"d\"" + LS
      + "  } ]" + LS
      + "}";

    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docTimer_1_0_1).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    Assert.assertEquals(0, timerPermissions.size());

    final String docEdge_1_0_0 = "{" + LS
        + "  \"id\" : \"edge-module-1.0.0\"," + LS
        + "  \"name\" : \"edge module\"," + LS
        + "  \"provides\" : [ {" + LS
        + "    \"id\" : \"_tenant\"," + LS
        + "    \"version\" : \"1.1\"," + LS
        + "    \"interfaceType\" : \"system\"," + LS
        + "    \"handlers\" : [ {" + LS
        + "      \"methods\" : [ \"POST\" ]," + LS
        + "      \"pathPattern\" : \"/_/tenant\"," + LS
        + "      \"permissionsRequired\" : [ ]" + LS
        + "    } ]" + LS
        + "  }, {" + LS
        + "    \"id\" : \"edge\"," + LS
        + "    \"version\" : \"1.0\"," + LS
        + "    \"handlers\" : [ {" + LS
        + "      \"methods\" : [ \"GET\", \"POST\" ]," + LS
        + "      \"pathPattern\" : \"/edge/{id}\"," + LS
        + "      \"permissionsRequired\" : [ ]" + LS
        + "    } ]" + LS
        + "  } ]," + LS
        + "  \"requires\" : [ ]," + LS
        + "  \"permissionSets\": [ {" + LS
        + "    \"permissionName\": \"edge.post.id\"," + LS
        + "    \"displayName\": \"e\"" + LS
        + "  } ]" + LS
        + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docEdge_1_0_0).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());


    final String nodeDiscoverEdge = "{" + LS
      + "  \"instId\" : \"localhost-" + Integer.toString(portEdge) + "\"," + LS
      + "  \"srvcId\" : \"edge-module-1.0.0\"," + LS
      + "  \"url\" : \"http://localhost:" + Integer.toString(portEdge) + "\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given().header("Content-Type", "application/json")
      .body(nodeDiscoverEdge).post("/_/discovery/modules")
      .then().statusCode(201);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    Assert.assertEquals(0, timerPermissions.size());

    // deploy, but with no running instance of timer-module-1.0.1
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"edge-module-1.0.0\", \"action\" : \"enable\"},"
        + " {\"id\" : \"timer-module-1.0.1\", \"action\" : \"enable\"}"
        + "]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install")
      .then().statusCode(400).log().ifValidationFails()
      .body(containsString("No running instances for module timer-module-1.0.1. Can not invoke /_/tenant"));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    Assert.assertEquals(0, timerPermissions.size());

    final String nodeDoc2 = "{" + LS
      + "  \"instId\" : \"localhost1-" + Integer.toString(portTimer) + "\"," + LS
      + "  \"srvcId\" : \"timer-module-1.0.1\"," + LS
      + "  \"url\" : \"http://localhost:" + Integer.toString(portTimer) + "\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given().header("Content-Type", "application/json")
      .body(nodeDoc2).post("/_/discovery/modules")
      .then().statusCode(201);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given().header("Content-Type", "application/json")
      .get("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(200).body(containsString("timer-module-1.0.0"));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    timerTenantInitStatus = 400;
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"edge-module-1.0.0\", \"action\" : \"enable\"},"
        + " {\"id\" : \"timer-module-1.0.1\", \"action\" : \"enable\"}"
        + "]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install")
      .then().statusCode(400).log().ifValidationFails()
      .body(containsString("POST request for timer-module-1.0.1 /_/tenant failed with 400: timer response"));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    Assert.assertEquals(0, timerPermissions.size());

    c = api.createRestAssured3();
    body = c.given().header("Content-Type", "application/json")
      .get("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(200).extract().body().asString();
    ar = new JsonArray(body);
    Assert.assertEquals(2, ar.size());
    Assert.assertEquals("edge-module-1.0.0", ar.getJsonObject(0).getString("id"));
    Assert.assertEquals("timer-module-1.0.0", ar.getJsonObject(1).getString("id"));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    timerTenantInitStatus = 200;
    timerTenantPermissionsStatus = 400;
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"edge-module-1.0.0\", \"action\" : \"enable\"},"
        + " {\"id\" : \"timer-module-1.0.1\", \"action\" : \"enable\"}"
        + "]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install")
      .then().statusCode(400).log().ifValidationFails()
      .body(containsString("POST request for timer-module-1.0.1 "
        +"/permissionscall failed with 400: timer permissions response"));
     Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    Assert.assertEquals(0, timerPermissions.size());

    c = api.createRestAssured3();
    body = c.given().header("Content-Type", "application/json")
      .get("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(200).extract().body().asString();
    ar = new JsonArray(body);
    Assert.assertEquals(2, ar.size());
    Assert.assertEquals("edge-module-1.0.0", ar.getJsonObject(0).getString("id"));
    Assert.assertEquals("timer-module-1.0.0", ar.getJsonObject(1).getString("id"));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    timerTenantPermissionsStatus = 200;
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"edge-module-1.0.0\", \"action\" : \"enable\"},"
        + " {\"id\" : \"timer-module-1.0.1\", \"action\" : \"enable\"}"
        + "]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install")
      .then().statusCode(200).log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    Assert.assertEquals(3, timerPermissions.size());
    Assert.assertTrue(timerPermissions.containsKey("edge-module-1.0.0"));
    Assert.assertTrue(timerPermissions.containsKey("timer-module-1.0.0"));
    Assert.assertTrue(timerPermissions.containsKey("timer-module-1.0.1"));

    // re-enable edge-module and check that permissions for it are available at tenant-init
    timerPermissions.clear();
    c = api.createRestAssured3();
    c.given()
        .header("Content-Type", "application/json")
        .body("["
            + " {\"id\" : \"edge-module-1.0.0\", \"action\" : \"disable\"}"
            + "]")
        .post("/_/proxy/tenants/" + okapiTenant + "/install")
        .then().statusCode(200).log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
        c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
        .header("Content-Type", "application/json")
        .body("["
            + " {\"id\" : \"edge-module-1.0.0\", \"action\" : \"enable\"}"
            + "]")
        .post("/_/proxy/tenants/" + okapiTenant + "/install")
        .then().statusCode(200).log().ifValidationFails();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
        c.getLastReport().isEmpty());
    Assert.assertNotNull(edgePermissionsAtInit);

    given()
      .header("X-Okapi-Tenant", okapiTenant)
      .header("Content-Type", "text/plain")
      .header("Accept", "text/plain")
      .body("Okapi").post("/timercall/1")
      .then().statusCode(200).log().ifValidationFails();


    final String docTimer_1_0_2 = "{" + LS
      + "  \"id\" : \"timer-module-1.0.2\"," + LS
      + "  \"name\" : \"timer module\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"_tenant\"," + LS
      + "    \"version\" : \"1.1\"," + LS
      + "    \"interfaceType\" : \"system\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant/disable\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"POST\", \"DELETE\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  }, {" + LS
      + "    \"id\" : \"_tenantPermissions\"," + LS
      + "    \"version\" : \"1.1\"," + LS
      + "    \"interfaceType\" : \"system\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/permissionscall\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  }, {" + LS
      + "    \"id\" : \"myint\"," + LS
      + "    \"version\" : \"1.0\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/timercall/{id}\"," + LS
      + "      \"permissionsRequired\" : [ \"timercall.post.id\" ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"DELETE\" ]," + LS
      + "      \"pathPattern\" : \"/timercall/{id}\"," + LS
      + "      \"permissionsRequired\" : [ \"timercall.delete.id\" ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]," + LS
      + "  \"permissionSets\": [ {" + LS
      + "    \"permissionName\": \"timercall.post.id\"," + LS
      + "    \"displayName\": \"d\"" + LS
      + "  } ]" + LS
      + "}";
    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docTimer_1_0_2).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String nodeDoc3 = "{" + LS
      + "  \"instId\" : \"localhost2-" + Integer.toString(portTimer) + "\"," + LS
      + "  \"srvcId\" : \"timer-module-1.0.2\"," + LS
      + "  \"url\" : \"http://localhost:" + Integer.toString(portTimer) + "\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given().header("Content-Type", "application/json")
      .body(nodeDoc3).post("/_/discovery/modules")
      .then().statusCode(201);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    timerTenantPermissionsStatus = 400;

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("["
        + " {\"id\" : \"timer-module\", \"action\" : \"enable\"}"
        + "]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install")
      .then().statusCode(400).log().ifValidationFails()
      .body(containsString("POST request for timer-module-1.0.2 "
        +"/permissionscall failed with 400: timer permissions response"));
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

  }

  @Test
  public void testTenantFailedUpgrade(TestContext context) {
    RestAssuredClient c;
    Response r;

    final String okapiTenant = "roskilde";
    // add tenant
    final String docTenantRoskilde = "{" + LS
      + "  \"id\" : \"" + okapiTenant + "\"," + LS
      + "  \"name\" : \"" + okapiTenant + "\"," + LS
      + "  \"description\" : \"Roskilde bibliotek\"" + LS
      + "}";
    c = api.createRestAssured3();
    given()
      .header("Content-Type", "application/json")
      .body(docTenantRoskilde).post("/_/proxy/tenants")
      .then().statusCode(201)
      .body(equalTo(docTenantRoskilde));

    final String docTimer_1_0_0 = "{" + LS
      + "  \"id\" : \"timer-module-1.0.0\"," + LS
      + "  \"name\" : \"timer module\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"myint\"," + LS
      + "    \"version\" : \"1.0\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/timercall/{id}\"," + LS
      + "      \"permissionsRequired\" : [ \"timercall.post.id\" ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"DELETE\" ]," + LS
      + "      \"pathPattern\" : \"/timercall/{id}\"," + LS
      + "      \"permissionsRequired\" : [ \"timercall.delete.id\" ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]," + LS
      + "  \"permissionSets\": [ {" + LS
      + "    \"permissionName\": \"timercall.post.id\"" + LS
      + "  } ]" + LS
      + "}";

    final String docBusiness_1_0_0 = "{" + LS
      + "  \"id\" : \"business-module-1.0.0\"," + LS
      + "  \"name\" : \"business module\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"_tenant\"," + LS
      + "    \"version\" : \"1.1\"," + LS
      + "    \"interfaceType\" : \"system\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant/disable\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"POST\", \"DELETE\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ { \"id\" : \"myint\", \"version\" : \"1.0\" } ]," + LS
      + "  \"permissionSets\": [ {" + LS
      + "    \"permissionName\": \"timercall.post.id\"" + LS
      + "  } ]" + LS
      + "}";

    final String docTimer_2_0_0 = "{" + LS
      + "  \"id\" : \"timer-module-2.0.0\"," + LS
      + "  \"name\" : \"timer module\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"myint\"," + LS
      + "    \"version\" : \"2.0\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/timercall/{id}\"," + LS
      + "      \"permissionsRequired\" : [ \"timercall.post.id\" ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"DELETE\" ]," + LS
      + "      \"pathPattern\" : \"/timercall/{id}\"," + LS
      + "      \"permissionsRequired\" : [ \"timercall.delete.id\" ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ ]," + LS
      + "  \"permissionSets\": [ {" + LS
      + "    \"permissionName\": \"timercall.post.id\"" + LS
      + "  } ]" + LS
      + "}";

    final String docBusiness_2_0_0 = "{" + LS
      + "  \"id\" : \"business-module-2.0.0\"," + LS
      + "  \"name\" : \"business module\"," + LS
      + "  \"provides\" : [ {" + LS
      + "    \"id\" : \"_tenant\"," + LS
      + "    \"version\" : \"1.1\"," + LS
      + "    \"interfaceType\" : \"system\"," + LS
      + "    \"handlers\" : [ {" + LS
      + "      \"methods\" : [ \"POST\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant/disable\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    }, {" + LS
      + "      \"methods\" : [ \"POST\", \"DELETE\" ]," + LS
      + "      \"pathPattern\" : \"/_/tenant\"," + LS
      + "      \"permissionsRequired\" : [ ]" + LS
      + "    } ]" + LS
      + "  } ]," + LS
      + "  \"requires\" : [ { \"id\" : \"myint\", \"version\" : \"2.0\" } ]," + LS
      + "  \"permissionSets\": [ {" + LS
      + "    \"permissionName\": \"timercall.post.id\"" + LS
      + "  } ]" + LS
      + "}";

    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docTimer_1_0_0).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String deployDocTimer_1_0_0 = "{" + LS
      + "  \"instId\" : \"localhost-1-" + Integer.toString(portTimer) + "\"," + LS
      + "  \"srvcId\" : \"timer-module-1.0.0\"," + LS
      + "  \"url\" : \"http://localhost:" + Integer.toString(portTimer) + "\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given().header("Content-Type", "application/json")
      .body(deployDocTimer_1_0_0).post("/_/discovery/modules")
      .then().statusCode(201);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docBusiness_1_0_0).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String deployDocBusiness_1_0_0 = "{" + LS
      + "  \"instId\" : \"localhost-2-" + Integer.toString(portTimer) + "\"," + LS
      + "  \"srvcId\" : \"business-module-1.0.0\"," + LS
      + "  \"url\" : \"http://localhost:" + Integer.toString(portTimer) + "\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given().header("Content-Type", "application/json")
      .body(deployDocBusiness_1_0_0).post("/_/discovery/modules")
      .then().statusCode(201);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docTimer_2_0_0).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String deployDocTimer_2_0_0 = "{" + LS
      + "  \"instId\" : \"localhost-3-" + Integer.toString(portTimer) + "\"," + LS
      + "  \"srvcId\" : \"timer-module-2.0.0\"," + LS
      + "  \"url\" : \"http://localhost:" + Integer.toString(portTimer) + "\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given().header("Content-Type", "application/json")
      .body(deployDocTimer_2_0_0).post("/_/discovery/modules")
      .then().statusCode(201);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    r = c.given()
      .header("Content-Type", "application/json")
      .body(docBusiness_2_0_0).post("/_/proxy/modules").then().statusCode(201)
      .extract().response();
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    final String deployDocBusiness_2_0_0 = "{" + LS
      + "  \"instId\" : \"localhost-4-" + Integer.toString(portTimer) + "\"," + LS
      + "  \"srvcId\" : \"business-module-2.0.0\"," + LS
      + "  \"url\" : \"http://localhost:" + Integer.toString(portTimer) + "\"" + LS
      + "}";
    c = api.createRestAssured3();
    c.given().header("Content-Type", "application/json")
      .body(deployDocBusiness_2_0_0).post("/_/discovery/modules")
      .then().statusCode(201);
    Assert.assertTrue("raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("[ {\"id\" : \"business-module-1.0.0\", \"action\" : \"enable\"}, {\"id\" : \"timer-module-1.0.0\", \"action\" : \"enable\"} ]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install")
      .then().statusCode(200).log().ifValidationFails()
      .body(equalTo("[ {" + LS
        + "  \"id\" : \"timer-module-1.0.0\"," + LS
        + "  \"action\" : \"enable\"" + LS
        + "}, {" + LS
        + "  \"id\" : \"business-module-1.0.0\"," + LS
        + "  \"action\" : \"enable\"" + LS
        + "} ]"));
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .post("/_/proxy/tenants/" + okapiTenant + "/upgrade?simulate=true")
      .then().statusCode(200).log().ifValidationFails()
      .body(equalTo("[ {" + LS
        + "  \"id\" : \"timer-module-2.0.0\"," + LS
        + "  \"from\" : \"timer-module-1.0.0\"," + LS
        + "  \"action\" : \"enable\"" + LS
        + "}, {" + LS
        + "  \"id\" : \"business-module-2.0.0\"," + LS
        + "  \"from\" : \"business-module-1.0.0\"," + LS
        + "  \"action\" : \"enable\"" + LS
        + "} ]"));
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    /* this will try to upgrade both, but business-module-2.0.0 tenant init fails,
     so business-module-1.0.0 stays but timer-module is upgrade (no _tenant interface) */
    timerTenantInitStatus = 400; // make business-module tenant init fail
    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .post("/_/proxy/tenants/" + okapiTenant + "/upgrade")
      .then().statusCode(400).log().ifValidationFails()
      .body(equalTo("POST request for business-module-2.0.0 /_/tenant failed with 400: timer response"));
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .get("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(200).log().ifValidationFails()
      .body(equalTo("[ {" + LS
        + "  \"id\" : \"business-module-1.0.0\"" + LS
        + "}, {" + LS
        + "  \"id\" : \"timer-module-2.0.0\"" + LS
        + "} ]"));
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body("[ {\"id\" : \"timer-module-2.0.0\", \"action\" : \"disable\"} ]")
      .post("/_/proxy/tenants/" + okapiTenant + "/install?simulate=true")
      .then().statusCode(200).log().ifValidationFails()
      .body(equalTo("[ {" + LS
        + "  \"id\" : \"business-module-1.0.0\"," + LS
        + "  \"action\" : \"disable\"" + LS
        + "}, {" + LS
        + "  \"id\" : \"timer-module-2.0.0\"," + LS
        + "  \"action\" : \"disable\"" + LS
        + "} ]"));
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // works, because timer-module-2.0.0 does not have tenant interface
    c = api.createRestAssured3();
    c.given()
      .delete("/_/proxy/tenants/" + okapiTenant + "/modules/timer-module-2.0.0")
      .then().statusCode(204).log().ifValidationFails();
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
      .get("/_/proxy/tenants/" + okapiTenant + "/modules")
      .then().statusCode(200).log().ifValidationFails()
      .body(equalTo("[ {" + LS
        + "  \"id\" : \"business-module-1.0.0\"" + LS
        + "} ]"));
    Assert.assertTrue(
      "raml: " + c.getLastReport().toString(),
      c.getLastReport().isEmpty());

    // disable also fails with 400
    Assert.assertEquals(400, timerTenantInitStatus);
    c = api.createRestAssured3();
    c.given()
        .delete("/_/proxy/tenants/" + okapiTenant + "/modules")
        .then().statusCode(400).log().ifValidationFails()
        .body(equalTo("POST request for business-module-1.0.0 /_/tenant/disable failed with 400: timer response"));
    Assert.assertTrue(
        "raml: " + c.getLastReport().toString(),
        c.getLastReport().isEmpty());

    // however, purge does not
    c = api.createRestAssured3();
    c.given()
        .delete("/_/proxy/tenants/" + okapiTenant + "/modules?purge=true")
        .then().statusCode(204).log().ifValidationFails();
    Assert.assertTrue(
        "raml: " + c.getLastReport().toString(),
        c.getLastReport().isEmpty());

    // tenant init fails, but is not called because in next tests invoke=false
    Assert.assertEquals(400, timerTenantInitStatus);
    c = api.createRestAssured3();
    c.given()
        .header("Content-Type", "application/json")
        .body("[ {\"id\" : \"business-module-1.0.0\", \"action\" : \"enable\"}, {\"id\" : \"timer-module-1.0.0\", \"action\" : \"enable\"} ]")
        .post("/_/proxy/tenants/" + okapiTenant + "/install?invoke=false")
        .then().statusCode(200).log().ifValidationFails()
        .body(equalTo("[ {" + LS
            + "  \"id\" : \"timer-module-1.0.0\"," + LS
            + "  \"action\" : \"enable\"" + LS
            + "}, {" + LS
            + "  \"id\" : \"business-module-1.0.0\"," + LS
            + "  \"action\" : \"enable\"" + LS
            + "} ]"));
    Assert.assertTrue(
        "raml: " + c.getLastReport().toString(),
        c.getLastReport().isEmpty());

    c = api.createRestAssured3();
    c.given()
        .delete("/_/proxy/tenants/" + okapiTenant + "/modules?invoke=false")
        .then().statusCode(204).log().ifValidationFails();
    Assert.assertTrue(
        "raml: " + c.getLastReport().toString(),
        c.getLastReport().isEmpty());
  }

  @Test
  public void testTenantPermissionsVersion() {
    String tenant = "test-tenant-permissions-tenant";
    String moduleId = "test-tenant-permissions-basic-module-1.0.0";
    String authModuleId = "test-tenant-permissions-auth-module-1.0.0";
    String body = new JsonObject().put("id", "test").encode();

    setupBasicTenant(tenant);

    // test _tenantpermissions 1.0 vs 1.1
    for (String tenantPermissionsVersion : Arrays.asList("1.0", "1.1")) {
      timerPermissions.clear();
      setupBasicModule(tenant, moduleId, tenantPermissionsVersion);
      setupBasicAuth(tenant, authModuleId);
      // system generates permission sets for 1.1 version
      if (tenantPermissionsVersion.equals("1.0")) {
        Assert.assertEquals(1, timerPermissions.getJsonArray(moduleId).size());
      } else {
        Assert.assertEquals(4, timerPermissions.getJsonArray(moduleId).size());
      }
      // proxy calls
      RestAssuredClient c = api.createRestAssured3();
      Response r = c.given()
        .header("Content-Type", "application/json")
        .header("X-Okapi-Token", getOkapiToken(tenant))
        .body(body).post("/regularcall")
        .then().statusCode(200).log().ifValidationFails()
        .extract().response();
      if (tenantPermissionsVersion.equals("1.0")) {
        Assert.assertFalse(r.getBody().asString().contains("SYS#"));
      } else {
        Assert.assertTrue(r.getBody().asString().contains("SYS#"));
      }
      // system calls
      given()
        .header("X-Okapi-Tenant", tenant)
        .header("Content-Type", "text/plain")
        .header("Accept", "text/plain")
        .body("Okapi").post("/timercall/10")
        .then().statusCode(200).log().ifValidationFails();
      // clean up
      given().delete("/_/proxy/tenants/" + tenant + "/modules").then().statusCode(204);
      given().delete("/_/discovery/modules").then().statusCode(204);
      given().delete("/_/proxy/modules/" + moduleId).then().statusCode(204);
    }

    // remove testing tenant
    given().delete("/_/proxy/tenants/" + tenant).then().statusCode(204);
  }

  @Test
  public void testDelegateCORS() {
    String tenant = "test-tenant-delegate-cors";
    String moduleId = "test-tenant-delegate-cors-module-1.0.0";
    String authModuleId = "test-tenant-delegate-cors-auth-module-1.0.0";
    String body = new JsonObject().put("id", "test").encode();

    setupBasicTenant(tenant);
    setupBasicModule(tenant, moduleId, "1.1");
    setupBasicAuth(tenant, authModuleId);

    RestAssuredClient c = api.createRestAssured3();

    // no CORS delegate
    c.given()
      .header("Content-Type", "application/json")
      .header("X-Okapi-Token", getOkapiToken(tenant))
      .header("origin", "localhost")
      .body(body)
      .post("/_/invoke/tenant/" + tenant + "/regularcall")
      .then()
      .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS.toString(), notNullValue())
      .statusCode(200)
      .log().ifValidationFails();

    // with CORS delegate
    c.given()
      .header("Content-Type", "application/json")
      .header("X-Okapi-Token", getOkapiToken(tenant))
      .header("origin", "localhost")
      .body(body)
      .post("/_/invoke/tenant/" + tenant + "/corscall")
      .then()
      .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS.toString(), nullValue())
      .statusCode(200)
      .log().ifValidationFails();

    // with CORS delegate and query parameter
    c.given()
      .header("Content-Type", "application/json")
      .header("X-Okapi-Token", getOkapiToken(tenant))
      .header("origin", "localhost")
      .body(body)
      .post("/_/invoke/tenant/" + tenant + "/corscall?x=y")
      .then()
      .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS.toString(), nullValue())
      .header(CORS_TEST_HEADER, "x=y")
      .statusCode(200)
      .log().ifValidationFails();

    given().delete("/_/proxy/tenants/" + tenant + "/modules").then().statusCode(204);
    given().delete("/_/discovery/modules").then().statusCode(204);
    given().delete("/_/proxy/modules/" + moduleId).then().statusCode(204);
    given().delete("/_/proxy/tenants/" + tenant).then().statusCode(204);
  }

  // add basic tenant
  private void setupBasicTenant(String tenant) {
    String tenantJson = new JsonObject().put("id", tenant).encode();
    RestAssuredClient c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(tenantJson).post("/_/proxy/tenants")
      .then().statusCode(201);
  }

  // get X-Okapi-Token
  private String getOkapiToken(String tenant) {
    String loginJson = new JsonObject()
      .put("tenant",  tenant)
      .put("username", "peter")
      .put("password", "peter-password")
      .encode();
    RestAssuredClient c = api.createRestAssured3();
    return c.given()
      .header("Content-Type", "application/json")
      .header("X-Okapi-Tenant", tenant)
      .body(loginJson).post("/authn/login")
      .then().statusCode(200).extract().header("X-Okapi-Token");
  }

  // decode X-Okapi-Token
  private JsonObject decodeOkapiToken(String token) {
    String encodedJson = token.substring(token.indexOf(".") + 1, token.lastIndexOf("."));
    return new JsonObject(new String(Base64.getDecoder().decode(encodedJson)));
  }

  // extract sub from token
  private String extractSubFromToken(RoutingContext ctx) {
    String token = ctx.request().getHeader("X-Okapi-Token");
    if (token != null) {
      return decodeOkapiToken(token).getString("sub");
    }
    return "";
  }

  // add auth module
  private void setupBasicAuth(String tenant, String authModuleId) {
    String testAuthJar = "../okapi-test-auth-module/target/okapi-test-auth-module-fat.jar";
    String mdJson = new JsonObject()
      .put("id", authModuleId)
      .put("name", "auth")
      .put("provides", new JsonArray()
        .add(new JsonObject()
          .put("id", "auth")
          .put("version", "1.2")
          .put("handlers", new JsonArray()
            .add(new JsonObject()
              .put("methods", new JsonArray().add("POST"))
              .put("path", "/authn/login")
              .put("level", "20")
              .put("type", "request-response")
              .put("permissionsRequired", new JsonArray())))))
      .put("filters", new JsonArray()
        .add(new JsonObject()
          .put("methods", new JsonArray().add("*"))
          .put("path", "/")
          .put("phase", "auth")
          .put("type", "headers")
          .put("phase", "auth")
          .put("permissionsRequired", new JsonArray())))
      .put("requires", new JsonArray())
      .put("launchDescriptor", new JsonObject()
        .put("exec", "java -Dport=%p -jar " + testAuthJar))
      .encodePrettily();

    // registration
    RestAssuredClient c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(mdJson).post("/_/proxy/modules")
      .then().statusCode(201).log().ifValidationFails();

    // deploy and enable
    String installJson = new JsonArray().add(new JsonObject()
      .put("id", authModuleId)
      .put("action", "enable"))
      .encode();
    c.given()
      .header("Content-Type", "application/json")
      .body(installJson)
      .post("/_/proxy/tenants/" + tenant + "/install?deploy=true")
      .then().statusCode(200).log().ifValidationFails();
  }

  // add basic module
  private void setupBasicModule(String tenant, String moduleId, String tenantPermissionsVersion) {
    String mdJson = new JsonObject()
      .put("id", moduleId)
      .put("provides", new JsonArray()
        .add(new JsonObject()
          .put("id", "_tenant")
          .put("version", "1.1")
          .put("interfaceType", "system")
          .put("handlers", new JsonArray()
            .add(new JsonObject()
              .put("methods", new JsonArray().add("POST"))
              .put("pathPattern", "/_/tenant/disable")
              .put("permissionsRequired", new JsonArray()))
            .add(new JsonObject()
              .put("methods", new JsonArray().add("POST").add("DELETE"))
              .put("pathPattern", "/_/tenant")
              .put("permissionsRequired", new JsonArray()))))
        .add(new JsonObject()
          .put("id", "_tenantPermissions")
          .put("version", tenantPermissionsVersion)
          .put("interfaceType", "system")
          .put("handlers", new JsonArray()
            .add(new JsonObject()
              .put("methods", new JsonArray().add("POST"))
              .put("pathPattern", "/permissionscall")
              .put("permissionsRequired", new JsonArray()))))
        .add(new JsonObject()
          .put("id", "_timer")
          .put("version", "1.0")
          .put("interfaceType", "system")
          .put("handlers", new JsonArray()
            .add(new JsonObject()
              .put("methods", new JsonArray().add("POST"))
              .put("pathPattern", "/timercall/1")
              .put("unit", "millisecond")
              .put("delay", "10")
              .put("permissionsRequired", new JsonArray().add("timercall.post.id"))
              .put("modulePermissions", new JsonArray().add("timercall.test.post")))
            .add(new JsonObject()
              .put("methods", new JsonArray().add("DELETE"))
              .put("pathPattern", "/timercall/{id}")
              .put("permissionsRequired", new JsonArray().add("timercall.delete.id")))))
        .add(new JsonObject()
          .put("id", "myint")
          .put("version", "1.0")
          .put("handlers", new JsonArray()
            .add(new JsonObject()
              .put("methods", new JsonArray().add("POST"))
              .put("pathPattern", "/timercall/{id}")
              .put("permissionsRequired", new JsonArray())
              .put("modulePermissions", new JsonArray().add("timercall.post.id").add("timercall.delete.id")))
            .add(new JsonObject()
              .put("methods", new JsonArray().add("DELETE"))
              .put("pathPattern", "/timercall/{id}")
              .put("permissionsRequired", new JsonArray().add("timercall.delete.id")))))
        .add(new JsonObject()
          .put("id", "mytest")
          .put("version", "1.0")
          .put("handlers", new JsonArray()
            .add(new JsonObject()
              .put("methods", new JsonArray().add("POST"))
              .put("pathPattern", "/regularcall")
              .put("permissionsRequired", new JsonArray())
              .put("modulePermissions", new JsonArray().add("regularcall.test.post")))))
        .add(new JsonObject()
            .put("id", "CORS-TEST")
            .put("version", "1.0")
            .put("handlers", new JsonArray()
              .add(new JsonObject()
                .put("methods", new JsonArray().add("POST"))
                .put("pathPattern", "/corscall")
                .put("permissionsRequired", new JsonArray())
                .put("delegateCORS", "true")))))
      .put("requires", new JsonArray())
      .put("permissionSets", new JsonArray()
        .add(new JsonObject()
          .put("permissionName", "timercall.post.id")
          .put("displayName", "d")))
      .encodePrettily();

    // registration
    RestAssuredClient c = api.createRestAssured3();
    c.given()
      .header("Content-Type", "application/json")
      .body(mdJson).post("/_/proxy/modules")
      .then().statusCode(201).log().ifValidationFails();

    // discovery
    String discoveryJson = new JsonObject()
      .put("instId", "localhost-" + Integer.toString(portTimer))
      .put("srvcId", moduleId)
      .put("url", "http://localhost:" + Integer.toString(portTimer))
      .encode();
    c.given()
      .header("Content-Type", "application/json")
      .body(discoveryJson).post("/_/discovery/modules")
      .then().statusCode(201).log().ifValidationFails();

    // install
    String installJson = new JsonArray()
      .add(new JsonObject().put("id", moduleId).put("action", "enable"))
      .encode();
    c.given()
      .header("Content-Type", "application/json")
      .body(installJson)
      .post("/_/proxy/tenants/" + tenant + "/install")
      .then().statusCode(200).log().ifValidationFails();
  }

}