Skip to content

Commit

Permalink
Add Programmatic API suite
Browse files Browse the repository at this point in the history
  • Loading branch information
mathieuancelin committed May 16, 2018
1 parent 48321db commit a571f2e
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 25 deletions.
36 changes: 31 additions & 5 deletions otoroshi/app/api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,42 @@ package otoroshi.api

import actions._
import com.softwaremill.macwire.wire
import com.typesafe.config.{Config, ConfigFactory}
import controllers._
import env._
import gateway.{CircuitBreakersHolder, ErrorHandler, GatewayRequestHandler, WebSocketHandler}
import play.api.http.{DefaultHttpFilters, HttpErrorHandler, HttpRequestHandler}
import play.api.libs.ws.ahc.AhcWSComponents
import play.api.mvc.{ControllerComponents, DefaultControllerComponents}
import play.api.routing.Router
import play.api.{BuiltInComponents, Logger, LoggerConfigurator}
import play.api.{BuiltInComponents, Configuration, LoggerConfigurator}
import play.core.server.{AkkaHttpServerComponents, ServerConfig}
import play.filters.HttpFiltersComponents
import router.Routes

private class ProgrammaticComponents(_serverConfig: play.core.server.ServerConfig)
class ProgrammaticOtoroshiComponents(_serverConfig: play.core.server.ServerConfig, _configuration: Config)
extends AkkaHttpServerComponents
with BuiltInComponents
with AssetsComponents
with AhcWSComponents
with HttpFiltersComponents {

override lazy val configuration: Configuration = {
val sslConfig = serverConfig.sslPort.map { sslPort =>
s"""
|https.port=$sslPort
|play.server.https.port=$sslPort
""".stripMargin
}.getOrElse("")
val httpConfig = serverConfig.port.map { httpPort =>
s"""
|http.port=$httpPort
|play.server.http.port=$httpPort
""".stripMargin
}.getOrElse("")
Configuration(ConfigFactory.load()) ++ Configuration(_configuration) ++ Configuration(ConfigFactory.parseString(httpConfig + sslConfig))
}

LoggerConfigurator(environment.classLoader).foreach {
_.configure(environment, configuration, Map.empty)
}
Expand Down Expand Up @@ -66,17 +83,22 @@ private class ProgrammaticComponents(_serverConfig: play.core.server.ServerConfi
}
}

class Otoroshi(serverConfig: ServerConfig) {
class Otoroshi(serverConfig: ServerConfig, configuration: Config = ConfigFactory.empty) {

private lazy val components = new ProgrammaticComponents(serverConfig)
lazy val components = new ProgrammaticOtoroshiComponents(serverConfig, configuration)

private lazy val server = components.server
lazy val server = components.server

def start(): Otoroshi = {
server.httpPort.get + 1
this
}

def startAndStopOnShutdown(): Otoroshi = {
server.httpPort.get + 1
stopOnShutdown()
}

def stop(): Unit = server.stop()

def stopOnShutdown(): Otoroshi = {
Expand All @@ -87,6 +109,10 @@ class Otoroshi(serverConfig: ServerConfig) {
}
}

object Otoroshi {
def apply(serverConfig: ServerConfig, configuration: Config = ConfigFactory.empty): Otoroshi = new Otoroshi(serverConfig, configuration)
}

object Main {

def main(args: Array[String]): Unit = {
Expand Down
1 change: 1 addition & 0 deletions otoroshi/app/storage/cassandra/CassandraRedis.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class CassandraRedis(actorSystem: ActorSystem,

val poolingOptions = new PoolingOptions()
.setMaxQueueSize(2048)
.setPoolTimeoutMillis(10000)
.setMaxRequestsPerConnection(HostDistance.LOCAL, 512)
.setMaxRequestsPerConnection(HostDistance.REMOTE, 256)
.setConnectionsPerHost(HostDistance.LOCAL, 256, 512)
Expand Down
8 changes: 5 additions & 3 deletions otoroshi/test/Suites.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import java.io.File

import com.typesafe.config.ConfigFactory
import functional.{OtoroshiApiSpec, OtoroshiBasicSpec}
import functional.{OtoroshiApiSpec, OtoroshiBasicSpec, ProgrammaticApiSpec}
import org.apache.commons.io.FileUtils
import org.scalatest.{BeforeAndAfterAll, Suite, Suites}
import play.api.Configuration
Expand Down Expand Up @@ -60,12 +60,14 @@ object OtoroshiTests {
if (name == "LevelDB") {
Seq(
new OtoroshiBasicSpec(name, Configurations.LevelDBConfiguration),
new OtoroshiApiSpec(name, Configurations.LevelDBConfiguration)
new OtoroshiApiSpec(name, Configurations.LevelDBConfiguration),
new ProgrammaticApiSpec(name, Configurations.LevelDBConfiguration)
)
} else {
Seq(
new OtoroshiBasicSpec(name, config), // add private path, additional header, routing headers, matching root, target root, wildcard domain, whitelist, blacklist
new OtoroshiApiSpec(name, config)
new OtoroshiApiSpec(name, config),
new ProgrammaticApiSpec(name, config)
// alerts spec
// audit spec
// websocket spec
Expand Down
99 changes: 99 additions & 0 deletions otoroshi/test/functional/ProgrammaticApiSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package functional

import java.nio.file.Files
import java.util.concurrent.atomic.AtomicInteger

import akka.actor.ActorSystem
import com.typesafe.config.ConfigFactory
import models.{ServiceDescriptor, Target}
import org.apache.commons.io.FileUtils
import org.scalatest.concurrent.IntegrationPatience
import org.scalatestplus.play.PlaySpec
import otoroshi.api.Otoroshi
import play.api.Configuration
import play.core.server.ServerConfig

class ProgrammaticApiSpec(name: String, configurationSpec: => Configuration)
extends PlaySpec
with OneServerPerSuiteWithMyComponents
with OtoroshiSpecHelper
with IntegrationPatience {

lazy val serviceHost = "basictest.foo.bar"
lazy val ws = otoroshiComponents.wsClient

override def getConfiguration(configuration: Configuration) = configuration ++ configurationSpec ++ Configuration(
ConfigFactory
.parseString(s"""
|http.port=$port
|play.server.http.port=$port
""".stripMargin).resolve()
)

s"[$name] Otoroshi Programmatic API" should {

"just works" in {

import scala.concurrent.duration._

implicit val system = ActorSystem("otoroshi-prog-api-test")
val dir = Files.createTempDirectory("otoroshi-prog-api-test").toFile

val callCounter = new AtomicInteger(0)
val basicTestExpectedBody = """{"message":"hello world"}"""
val basicTestServer = TargetService(Some(serviceHost), "/api", "application/json", { _ =>
callCounter.incrementAndGet()
basicTestExpectedBody
}).await()

val initialDescriptor = ServiceDescriptor(
id = "basic-test",
name = "basic-test",
env = "prod",
subdomain = "basictest",
domain = "foo.bar",
targets = Seq(
Target(
host = s"127.0.0.1:${basicTestServer.port}",
scheme = "http"
)
),
localHost = s"127.0.0.1:${basicTestServer.port}",
forceHttps = false,
enforceSecureCommunication = false,
publicPatterns = Seq("/.*")
)

val otoroshi = Otoroshi(
ServerConfig(
address = "0.0.0.0",
port = Some(8888),
rootDir = dir
)
).start()

awaitF(3.seconds).futureValue

val services = getOtoroshiServices(Some(8888), otoroshi.components.wsClient).futureValue

services.size mustBe 1

val (_, status) = createOtoroshiService(initialDescriptor, Some(8888), otoroshi.components.wsClient).futureValue

status mustBe 200

val basicTestResponse1 = otoroshi.components.wsClient.url(s"http://127.0.0.1:8888/api").withHttpHeaders(
"Host" -> serviceHost
).get().futureValue

basicTestResponse1.status mustBe 200
basicTestResponse1.body mustBe basicTestExpectedBody
callCounter.get() mustBe 1

basicTestServer.stop()
otoroshi.stop()
system.terminate()
FileUtils.deleteDirectory(dir)
}
}
}
50 changes: 33 additions & 17 deletions otoroshi/test/functional/utils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ import org.scalatestplus.play.components.{OneServerPerSuiteWithComponents, OneSe
import org.slf4j.LoggerFactory
import play.api.ApplicationLoader.Context
import play.api.libs.json.{JsArray, JsValue, Json}
import play.api.libs.ws.WSAuthScheme
import play.api.libs.ws.{WSAuthScheme, WSClient}
import play.api.{BuiltInComponents, Configuration, Logger}

import scala.concurrent.duration._
import scala.concurrent.{Await, Future}
import scala.concurrent.{Await, Future, Promise}
import scala.util.{Random, Try}

trait AddConfiguration {
Expand Down Expand Up @@ -58,13 +58,29 @@ trait OtoroshiSpecHelper { suite: OneServerPerSuiteWithMyComponents =>
lazy implicit val ec = otoroshiComponents.env.internalActorSystem.dispatcher
lazy val logger = Logger("otoroshi-spec-helper")

def otoroshiApiCall(method: String, path: String, payload: Option[JsValue] = None): Future[(JsValue, Int)] = {
def await(duration: FiniteDuration): Unit = {
val p = Promise[Unit]
otoroshiComponents.env.internalActorSystem.scheduler.scheduleOnce(duration) {
p.trySuccess(())
}
Await.result(p.future, duration + 1.second)
}

def awaitF(duration: FiniteDuration)(implicit system: ActorSystem): Future[Unit] = {
val p = Promise[Unit]
system.scheduler.scheduleOnce(duration) {
p.trySuccess(())
}
p.future
}

def otoroshiApiCall(method: String, path: String, payload: Option[JsValue] = None, customPort: Option[Int] = None): Future[(JsValue, Int)] = {
val headers = Seq(
"Host" -> "otoroshi-api.foo.bar",
"Accept" -> "application/json"
)
if (payload.isDefined) {
suite.otoroshiComponents.wsClient.url(s"http://127.0.0.1:$port$path")
suite.otoroshiComponents.wsClient.url(s"http://127.0.0.1:${customPort.getOrElse(port)}$path")
.withHttpHeaders(headers :+ ("Content-Type" -> "application/json"): _*)
.withAuth("admin-api-apikey-id", "admin-api-apikey-secret", WSAuthScheme.BASIC)
.withFollowRedirects(false)
Expand All @@ -78,7 +94,7 @@ trait OtoroshiSpecHelper { suite: OneServerPerSuiteWithMyComponents =>
(response.json, response.status)
}
} else {
suite.otoroshiComponents.wsClient.url(s"http://127.0.0.1:$port$path")
suite.otoroshiComponents.wsClient.url(s"http://127.0.0.1:${customPort.getOrElse(port)}$path")
.withHttpHeaders(headers: _*)
.withAuth("admin-api-apikey-id", "admin-api-apikey-secret", WSAuthScheme.BASIC)
.withFollowRedirects(false)
Expand All @@ -93,8 +109,8 @@ trait OtoroshiSpecHelper { suite: OneServerPerSuiteWithMyComponents =>
}
}

def getOtoroshiServices(): Future[Seq[ServiceDescriptor]] = {
suite.otoroshiComponents.wsClient.url(s"http://localhost:$port/api/services").withHttpHeaders(
def getOtoroshiServices(customPort: Option[Int] = None, ws: WSClient = suite.otoroshiComponents.wsClient): Future[Seq[ServiceDescriptor]] = {
ws.url(s"http://localhost:${customPort.getOrElse(port)}/api/services").withHttpHeaders(
"Host" -> "otoroshi-api.foo.bar",
"Accept" -> "application/json"
).withAuth("admin-api-apikey-id", "admin-api-apikey-secret", WSAuthScheme.BASIC).get().map { response =>
Expand All @@ -105,26 +121,26 @@ trait OtoroshiSpecHelper { suite: OneServerPerSuiteWithMyComponents =>
}
}

def getOtoroshiServiceGroups(): Future[Seq[ServiceGroup]] = {
suite.otoroshiComponents.wsClient.url(s"http://localhost:$port/api/groups").withHttpHeaders(
def getOtoroshiServiceGroups(customPort: Option[Int] = None): Future[Seq[ServiceGroup]] = {
suite.otoroshiComponents.wsClient.url(s"http://localhost:${customPort.getOrElse(port)}/api/groups").withHttpHeaders(
"Host" -> "otoroshi-api.foo.bar",
"Accept" -> "application/json"
).withAuth("admin-api-apikey-id", "admin-api-apikey-secret", WSAuthScheme.BASIC).get().map { response =>
response.json.as[JsArray].value.map(e => ServiceGroup.fromJsons(e))
}
}

def getOtoroshiApiKeys(): Future[Seq[ApiKey]] = {
suite.otoroshiComponents.wsClient.url(s"http://localhost:$port/api/apikeys").withHttpHeaders(
def getOtoroshiApiKeys(customPort: Option[Int] = None): Future[Seq[ApiKey]] = {
suite.otoroshiComponents.wsClient.url(s"http://localhost:${customPort.getOrElse(port)}/api/apikeys").withHttpHeaders(
"Host" -> "otoroshi-api.foo.bar",
"Accept" -> "application/json"
).withAuth("admin-api-apikey-id", "admin-api-apikey-secret", WSAuthScheme.BASIC).get().map { response =>
response.json.as[JsArray].value.map(e => ApiKey.fromJsons(e))
}
}

def createOtoroshiService(service: ServiceDescriptor): Future[(JsValue, Int)] = {
suite.otoroshiComponents.wsClient.url(s"http://localhost:$port/api/services")
def createOtoroshiService(service: ServiceDescriptor, customPort: Option[Int] = None, ws: WSClient = suite.otoroshiComponents.wsClient): Future[(JsValue, Int)] = {
ws.url(s"http://localhost:${customPort.getOrElse(port)}/api/services")
.withHttpHeaders(
"Host" -> "otoroshi-api.foo.bar",
"Content-Type" -> "application/json"
Expand All @@ -136,8 +152,8 @@ trait OtoroshiSpecHelper { suite: OneServerPerSuiteWithMyComponents =>
}
}

def updateOtoroshiService(service: ServiceDescriptor): Future[(JsValue, Int)] = {
suite.otoroshiComponents.wsClient.url(s"http://localhost:$port/api/services/${service.id}")
def updateOtoroshiService(service: ServiceDescriptor, customPort: Option[Int] = None): Future[(JsValue, Int)] = {
suite.otoroshiComponents.wsClient.url(s"http://localhost:${customPort.getOrElse(port)}/api/services/${service.id}")
.withHttpHeaders(
"Host" -> "otoroshi-api.foo.bar",
"Content-Type" -> "application/json"
Expand All @@ -149,8 +165,8 @@ trait OtoroshiSpecHelper { suite: OneServerPerSuiteWithMyComponents =>
}
}

def deleteOtoroshiService(service: ServiceDescriptor): Future[(JsValue, Int)] = {
suite.otoroshiComponents.wsClient.url(s"http://localhost:$port/api/services/${service.id}")
def deleteOtoroshiService(service: ServiceDescriptor, customPort: Option[Int] = None): Future[(JsValue, Int)] = {
suite.otoroshiComponents.wsClient.url(s"http://localhost:${customPort.getOrElse(port)}/api/services/${service.id}")
.withHttpHeaders(
"Host" -> "otoroshi-api.foo.bar",
"Content-Type" -> "application/json"
Expand Down

0 comments on commit a571f2e

Please sign in to comment.