From 477804e21c3c61666a48b74f17caef04233c2363 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Thu, 4 Oct 2018 13:52:14 -0700 Subject: Fix dependencies in tests to accomodate project split --- .../xyz/driver/core/database/DatabaseTest.scala | 1 - .../src/test/scala/xyz/driver/core/AuthTest.scala | 5 +- .../scala/xyz/driver/core/rest/DriverAppTest.scala | 89 ------------- .../xyz/driver/core/rest/DriverRouteTest.scala | 14 +- .../driver/core/storage/AliyunBlobStorage.scala | 17 ++- .../main/scala/xyz/driver/core/deprecations.scala | 5 + .../main/scala/xyz/driver/core/generators.scala | 143 +++++++++++++++++++++ src/test/scala/xyz/driver/core/DriverAppTest.scala | 89 +++++++++++++ 8 files changed, 250 insertions(+), 113 deletions(-) delete mode 100644 core-rest/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala create mode 100644 core-types/src/main/scala/xyz/driver/core/deprecations.scala create mode 100644 core-types/src/main/scala/xyz/driver/core/generators.scala create mode 100644 src/test/scala/xyz/driver/core/DriverAppTest.scala diff --git a/core-database/src/test/scala/xyz/driver/core/database/DatabaseTest.scala b/core-database/src/test/scala/xyz/driver/core/database/DatabaseTest.scala index 8d2a4ac..1fee902 100644 --- a/core-database/src/test/scala/xyz/driver/core/database/DatabaseTest.scala +++ b/core-database/src/test/scala/xyz/driver/core/database/DatabaseTest.scala @@ -2,7 +2,6 @@ package xyz.driver.core.database import org.scalatest.{FlatSpec, Matchers} import org.scalatest.prop.Checkers -import xyz.driver.core.rest.errors.DatabaseException class DatabaseTest extends FlatSpec with Matchers with Checkers { import xyz.driver.core.generators._ diff --git a/core-rest/src/test/scala/xyz/driver/core/AuthTest.scala b/core-rest/src/test/scala/xyz/driver/core/AuthTest.scala index 2e772fb..8c32aa7 100644 --- a/core-rest/src/test/scala/xyz/driver/core/AuthTest.scala +++ b/core-rest/src/test/scala/xyz/driver/core/AuthTest.scala @@ -9,11 +9,12 @@ import akka.http.scaladsl.model.headers.{ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ import akka.http.scaladsl.testkit.ScalatestRouteTest +import com.typesafe.scalalogging.Logger import org.scalatest.{FlatSpec, Matchers} +import org.slf4j.helpers.NOPLogger import pdi.jwt.{Jwt, JwtAlgorithm} import xyz.driver.core.auth._ import xyz.driver.core.domain.Email -import xyz.driver.core.logging._ import xyz.driver.core.rest._ import xyz.driver.core.rest.auth._ import xyz.driver.core.time.Time @@ -53,7 +54,7 @@ class AuthTest extends FlatSpec with Matchers with ScalatestRouteTest { val authorization = new ChainedAuthorization[User](tokenAuthorization, basicAuthorization) - val authStatusService = new AuthProvider[User](authorization, NoLogger) { + val authStatusService = new AuthProvider[User](authorization, Logger(NOPLogger.NOP_LOGGER)) { override def authenticatedUser(implicit ctx: ServiceRequestContext): OptionT[Future, User] = OptionT.optionT[Future] { if (ctx.contextHeaders.keySet.contains(AuthProvider.AuthenticationTokenHeader)) { diff --git a/core-rest/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala b/core-rest/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala deleted file mode 100644 index 324c8d8..0000000 --- a/core-rest/src/test/scala/xyz/driver/core/rest/DriverAppTest.scala +++ /dev/null @@ -1,89 +0,0 @@ -package xyz.driver.core.rest - -import akka.http.scaladsl.model.headers._ -import akka.http.scaladsl.model.{HttpMethod, StatusCodes} -import akka.http.scaladsl.server.{Directives, Route} -import akka.http.scaladsl.testkit.ScalatestRouteTest -import com.typesafe.config.ConfigFactory -import org.scalatest.{AsyncFlatSpec, Matchers} -import xyz.driver.core.app.{DriverApp, SimpleModule} - -class DriverAppTest extends AsyncFlatSpec with ScalatestRouteTest with Matchers with Directives { - val config = ConfigFactory.parseString(""" - |application { - | cors { - | allowedOrigins: ["example.com"] - | } - |} - """.stripMargin).withFallback(ConfigFactory.load) - - val origin = Origin(HttpOrigin("https", Host("example.com"))) - val allowedOrigins = Set(HttpOrigin("https", Host("example.com"))) - val allowedMethods: collection.immutable.Seq[HttpMethod] = { - import akka.http.scaladsl.model.HttpMethods._ - collection.immutable.Seq(GET, PUT, POST, PATCH, DELETE, OPTIONS, TRACE) - } - - import scala.reflect.runtime.universe.typeOf - class TestApp(testRoute: Route) - extends DriverApp( - appName = "test-app", - version = "0.0.1", - gitHash = "deadb33f", - modules = Seq(new SimpleModule("test-module", theRoute = testRoute, routeType = typeOf[DriverApp])), - config = config, - log = xyz.driver.core.logging.NoLogger - ) - - it should "respond with the correct CORS headers for the swagger OPTIONS route" in { - val route = new TestApp(get(complete(StatusCodes.OK))) - Options(s"/api-docs/swagger.json").withHeaders(origin) ~> route.appRoute ~> check { - status shouldBe StatusCodes.OK - headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigins.toSeq: _*))) - header[`Access-Control-Allow-Methods`].get.methods should contain theSameElementsAs allowedMethods - } - } - - it should "respond with the correct CORS headers for the test route" in { - val route = new TestApp(get(complete(StatusCodes.OK))) - Get(s"/api/v1/test").withHeaders(origin) ~> route.appRoute ~> check { - status shouldBe StatusCodes.OK - headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigins.toSeq: _*))) - } - } - - it should "respond with the correct CORS headers for a concatenated route" in { - val route = new TestApp(get(complete(StatusCodes.OK)) ~ post(complete(StatusCodes.OK))) - Post(s"/api/v1/test").withHeaders(origin) ~> route.appRoute ~> check { - status shouldBe StatusCodes.OK - headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigins.toSeq: _*))) - } - } - - it should "allow subdomains of allowed origin suffixes" in { - val route = new TestApp(get(complete(StatusCodes.OK))) - Get(s"/api/v1/test") - .withHeaders(Origin(HttpOrigin("https", Host("foo.example.com")))) ~> route.appRoute ~> check { - status shouldBe StatusCodes.OK - headers should contain(`Access-Control-Allow-Origin`(HttpOrigin("https", Host("foo.example.com")))) - } - } - - it should "respond with default domains for invalid origins" in { - val route = new TestApp(get(complete(StatusCodes.OK))) - Get(s"/api/v1/test") - .withHeaders(Origin(HttpOrigin("https", Host("invalid.foo.bar.com")))) ~> route.appRoute ~> check { - status shouldBe StatusCodes.OK - headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange.*)) - } - } - - it should "respond with Pragma and Cache-Control (no-cache) headers" in { - val route = new TestApp(get(complete(StatusCodes.OK))) - Get(s"/api/v1/test") ~> route.appRoute ~> check { - status shouldBe StatusCodes.OK - header("Pragma").map(_.value()) should contain("no-cache") - header[`Cache-Control`].map(_.value()) should contain("no-cache") - } - } -} diff --git a/core-rest/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala b/core-rest/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala index cc0019a..d1172fa 100644 --- a/core-rest/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala +++ b/core-rest/src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala @@ -8,8 +8,8 @@ import akka.http.scaladsl.server.{Directives, RejectionHandler, Route} import akka.http.scaladsl.testkit.ScalatestRouteTest import com.typesafe.scalalogging.Logger import org.scalatest.{AsyncFlatSpec, Matchers} +import org.slf4j.helpers.NOPLogger import xyz.driver.core.json.serviceExceptionFormat -import xyz.driver.core.logging.NoLogger import xyz.driver.core.rest.errors._ import scala.concurrent.Future @@ -17,7 +17,7 @@ import scala.concurrent.Future class DriverRouteTest extends AsyncFlatSpec with ScalatestRouteTest with SprayJsonSupport with Matchers with Directives { class TestRoute(override val route: Route) extends DriverRoute { - override def log: Logger = NoLogger + override def log: Logger = Logger(NOPLogger.NOP_LOGGER) } "DriverRoute" should "respond with 200 OK for a basic route" in { @@ -94,16 +94,6 @@ class DriverRouteTest } } - it should "respond with a 500 for DatabaseException" in { - val route = new TestRoute(akkaComplete(Future.failed[String](DatabaseException()))) - - Post("/api/v1/foo/bar") ~> route.routeWithDefaults ~> check { - handled shouldBe true - status shouldBe StatusCodes.InternalServerError - responseAs[ServiceException] shouldBe DatabaseException() - } - } - it should "add a `Connection: close` header to avoid clashing with envoy's timeouts" in { val rejectionHandler = RejectionHandler.newBuilder().handleNotFound(complete(StatusCodes.NotFound)).result() val route = new TestRoute(handleRejections(rejectionHandler)((get & path("foo"))(complete("OK")))) diff --git a/core-storage/src/main/scala/xyz/driver/core/storage/AliyunBlobStorage.scala b/core-storage/src/main/scala/xyz/driver/core/storage/AliyunBlobStorage.scala index 7e59df4..fd0e7c6 100644 --- a/core-storage/src/main/scala/xyz/driver/core/storage/AliyunBlobStorage.scala +++ b/core-storage/src/main/scala/xyz/driver/core/storage/AliyunBlobStorage.scala @@ -18,11 +18,11 @@ import scala.concurrent.duration.Duration import scala.concurrent.{ExecutionContext, Future} class AliyunBlobStorage( - client: OSSClient, - bucketId: String, - clock: Clock, - chunkSize: Int = AliyunBlobStorage.DefaultChunkSize)(implicit ec: ExecutionContext) - extends SignedBlobStorage { + client: OSSClient, + bucketId: String, + clock: Clock, + chunkSize: Int = AliyunBlobStorage.DefaultChunkSize)(implicit ec: ExecutionContext) + extends SignedBlobStorage { override def uploadContent(name: String, content: Array[Byte]): Future[String] = Future { client.putObject(bucketId, name, new ByteArrayInputStream(content)) name @@ -61,7 +61,7 @@ class AliyunBlobStorage( Future { client.putObject(bucketId, name, is) Done - }) + }) } override def delete(name: String): Future[String] = Future { @@ -92,8 +92,7 @@ class AliyunBlobStorage( object AliyunBlobStorage { val DefaultChunkSize: Int = 8192 - def apply(config: Config, bucketId: String, clock: Clock)( - implicit ec: ExecutionContext): AliyunBlobStorage = { + def apply(config: Config, bucketId: String, clock: Clock)(implicit ec: ExecutionContext): AliyunBlobStorage = { val clientId = config.getString("storage.aliyun.clientId") val clientSecret = config.getString("storage.aliyun.clientSecret") val endpoint = config.getString("storage.aliyun.endpoint") @@ -101,7 +100,7 @@ object AliyunBlobStorage { } def apply(clientId: String, clientSecret: String, endpoint: String, bucketId: String, clock: Clock)( - implicit ec: ExecutionContext): AliyunBlobStorage = { + implicit ec: ExecutionContext): AliyunBlobStorage = { val client = new OSSClient(endpoint, clientId, clientSecret) new AliyunBlobStorage(client, bucketId, clock) } diff --git a/core-types/src/main/scala/xyz/driver/core/deprecations.scala b/core-types/src/main/scala/xyz/driver/core/deprecations.scala new file mode 100644 index 0000000..401e6c6 --- /dev/null +++ b/core-types/src/main/scala/xyz/driver/core/deprecations.scala @@ -0,0 +1,5 @@ +package xyz.driver.core.rest + +package object errors { + //type DatabaseException = xyz.driver.core.errors.DatabaseException +} diff --git a/core-types/src/main/scala/xyz/driver/core/generators.scala b/core-types/src/main/scala/xyz/driver/core/generators.scala new file mode 100644 index 0000000..d00b6dd --- /dev/null +++ b/core-types/src/main/scala/xyz/driver/core/generators.scala @@ -0,0 +1,143 @@ +package xyz.driver.core + +import enumeratum._ +import java.math.MathContext +import java.time.{Instant, LocalDate, ZoneOffset} +import java.util.UUID + +import xyz.driver.core.time.{Time, TimeOfDay, TimeRange} +import xyz.driver.core.date.{Date, DayOfWeek} + +import scala.reflect.ClassTag +import scala.util.Random +import eu.timepit.refined.refineV +import eu.timepit.refined.api.Refined +import eu.timepit.refined.collection._ + +object generators { + + private val random = new Random + import random._ + private val secureRandom = new java.security.SecureRandom() + + private val DefaultMaxLength = 10 + private val StringLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ".toSet + private val NonAmbigiousCharacters = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789" + private val Numbers = "0123456789" + + private def nextTokenString(length: Int, chars: IndexedSeq[Char]): String = { + val builder = new StringBuilder + for (_ <- 0 until length) { + builder += chars(secureRandom.nextInt(chars.length)) + } + builder.result() + } + + /** Creates a random invitation token. + * + * This token is meant fo human input and avoids using ambiguous characters such as 'O' and '0'. It + * therefore contains less entropy and is not meant to be used as a cryptographic secret. */ + @deprecated( + "The term 'token' is too generic and security and readability conventions are not well defined. " + + "Services should implement their own version that suits their security requirements.", + "1.11.0" + ) + def nextToken(length: Int): String = nextTokenString(length, NonAmbigiousCharacters) + + @deprecated( + "The term 'token' is too generic and security and readability conventions are not well defined. " + + "Services should implement their own version that suits their security requirements.", + "1.11.0" + ) + def nextNumericToken(length: Int): String = nextTokenString(length, Numbers) + + def nextInt(maxValue: Int, minValue: Int = 0): Int = random.nextInt(maxValue - minValue) + minValue + + def nextBoolean(): Boolean = random.nextBoolean() + + def nextDouble(): Double = random.nextDouble() + + def nextId[T](): Id[T] = Id[T](nextUuid().toString) + + def nextId[T](maxLength: Int): Id[T] = Id[T](nextString(maxLength)) + + def nextNumericId[T](): Id[T] = Id[T](nextLong.abs.toString) + + def nextNumericId[T](maxValue: Int): Id[T] = Id[T](nextInt(maxValue).toString) + + def nextName[T](maxLength: Int = DefaultMaxLength): Name[T] = Name[T](nextString(maxLength)) + + def nextNonEmptyName[T](maxLength: Int = DefaultMaxLength): NonEmptyName[T] = + NonEmptyName[T](nextNonEmptyString(maxLength)) + + def nextUuid(): UUID = java.util.UUID.randomUUID + + def nextRevision[T](): Revision[T] = Revision[T](nextUuid().toString) + + def nextString(maxLength: Int = DefaultMaxLength): String = + (oneOf[Char](StringLetters) +: arrayOf(oneOf[Char](StringLetters), maxLength - 1)).mkString + + def nextNonEmptyString(maxLength: Int = DefaultMaxLength): String Refined NonEmpty = { + refineV[NonEmpty]( + (oneOf[Char](StringLetters) +: arrayOf(oneOf[Char](StringLetters), maxLength - 1)).mkString + ).right.get + } + + def nextOption[T](value: => T): Option[T] = if (nextBoolean()) Option(value) else None + + def nextPair[L, R](left: => L, right: => R): (L, R) = (left, right) + + def nextTriad[F, S, T](first: => F, second: => S, third: => T): (F, S, T) = (first, second, third) + + def nextInstant(): Instant = Instant.ofEpochMilli(math.abs(nextLong() % System.currentTimeMillis)) + + def nextTime(): Time = nextInstant() + + def nextTimeOfDay: TimeOfDay = TimeOfDay(java.time.LocalTime.MIN.plusSeconds(nextLong), java.util.TimeZone.getDefault) + + def nextTimeRange(): TimeRange = { + val oneTime = nextTime() + val anotherTime = nextTime() + + TimeRange( + Time(scala.math.min(oneTime.millis, anotherTime.millis)), + Time(scala.math.max(oneTime.millis, anotherTime.millis))) + } + + def nextDate(): Date = nextTime().toDate(java.util.TimeZone.getTimeZone("UTC")) + + def nextLocalDate(): LocalDate = nextInstant().atZone(ZoneOffset.UTC).toLocalDate + + def nextDayOfWeek(): DayOfWeek = oneOf(DayOfWeek.All) + + def nextBigDecimal(multiplier: Double = 1000000.00, precision: Int = 2): BigDecimal = + BigDecimal(multiplier * nextDouble, new MathContext(precision)) + + def oneOf[T](items: T*): T = oneOf(items.toSet) + + def oneOf[T](items: Set[T]): T = items.toSeq(nextInt(items.size)) + + def oneOf[T <: EnumEntry](enum: Enum[T]): T = oneOf(enum.values: _*) + + def arrayOf[T: ClassTag](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): Array[T] = + Array.fill(nextInt(maxLength, minLength))(generator) + + def seqOf[T](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): Seq[T] = + Seq.fill(nextInt(maxLength, minLength))(generator) + + def vectorOf[T](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): Vector[T] = + Vector.fill(nextInt(maxLength, minLength))(generator) + + def listOf[T](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): List[T] = + List.fill(nextInt(maxLength, minLength))(generator) + + def setOf[T](generator: => T, maxLength: Int = DefaultMaxLength, minLength: Int = 0): Set[T] = + seqOf(generator, maxLength, minLength).toSet + + def mapOf[K, V]( + keyGenerator: => K, + valueGenerator: => V, + maxLength: Int = DefaultMaxLength, + minLength: Int = 0): Map[K, V] = + seqOf(nextPair(keyGenerator, valueGenerator), maxLength, minLength).toMap +} diff --git a/src/test/scala/xyz/driver/core/DriverAppTest.scala b/src/test/scala/xyz/driver/core/DriverAppTest.scala new file mode 100644 index 0000000..986e5d3 --- /dev/null +++ b/src/test/scala/xyz/driver/core/DriverAppTest.scala @@ -0,0 +1,89 @@ +package xyz.driver.core + +import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.model.{HttpMethod, StatusCodes} +import akka.http.scaladsl.server.{Directives, Route} +import akka.http.scaladsl.testkit.ScalatestRouteTest +import com.typesafe.config.ConfigFactory +import org.scalatest.{AsyncFlatSpec, Matchers} +import xyz.driver.core.app.{DriverApp, SimpleModule} + +class DriverAppTest extends AsyncFlatSpec with ScalatestRouteTest with Matchers with Directives { + val config = ConfigFactory.parseString(""" + |application { + | cors { + | allowedOrigins: ["example.com"] + | } + |} + """.stripMargin).withFallback(ConfigFactory.load) + + val origin = Origin(HttpOrigin("https", Host("example.com"))) + val allowedOrigins = Set(HttpOrigin("https", Host("example.com"))) + val allowedMethods: collection.immutable.Seq[HttpMethod] = { + import akka.http.scaladsl.model.HttpMethods._ + collection.immutable.Seq(GET, PUT, POST, PATCH, DELETE, OPTIONS, TRACE) + } + + import scala.reflect.runtime.universe.typeOf + class TestApp(testRoute: Route) + extends DriverApp( + appName = "test-app", + version = "0.0.1", + gitHash = "deadb33f", + modules = Seq(new SimpleModule("test-module", theRoute = testRoute, routeType = typeOf[DriverApp])), + config = config, + log = xyz.driver.core.logging.NoLogger + ) + + it should "respond with the correct CORS headers for the swagger OPTIONS route" in { + val route = new TestApp(get(complete(StatusCodes.OK))) + Options(s"/api-docs/swagger.json").withHeaders(origin) ~> route.appRoute ~> check { + status shouldBe StatusCodes.OK + headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigins.toSeq: _*))) + header[`Access-Control-Allow-Methods`].get.methods should contain theSameElementsAs allowedMethods + } + } + + it should "respond with the correct CORS headers for the test route" in { + val route = new TestApp(get(complete(StatusCodes.OK))) + Get(s"/api/v1/test").withHeaders(origin) ~> route.appRoute ~> check { + status shouldBe StatusCodes.OK + headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigins.toSeq: _*))) + } + } + + it should "respond with the correct CORS headers for a concatenated route" in { + val route = new TestApp(get(complete(StatusCodes.OK)) ~ post(complete(StatusCodes.OK))) + Post(s"/api/v1/test").withHeaders(origin) ~> route.appRoute ~> check { + status shouldBe StatusCodes.OK + headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange(allowedOrigins.toSeq: _*))) + } + } + + it should "allow subdomains of allowed origin suffixes" in { + val route = new TestApp(get(complete(StatusCodes.OK))) + Get(s"/api/v1/test") + .withHeaders(Origin(HttpOrigin("https", Host("foo.example.com")))) ~> route.appRoute ~> check { + status shouldBe StatusCodes.OK + headers should contain(`Access-Control-Allow-Origin`(HttpOrigin("https", Host("foo.example.com")))) + } + } + + it should "respond with default domains for invalid origins" in { + val route = new TestApp(get(complete(StatusCodes.OK))) + Get(s"/api/v1/test") + .withHeaders(Origin(HttpOrigin("https", Host("invalid.foo.bar.com")))) ~> route.appRoute ~> check { + status shouldBe StatusCodes.OK + headers should contain(`Access-Control-Allow-Origin`(HttpOriginRange.*)) + } + } + + it should "respond with Pragma and Cache-Control (no-cache) headers" in { + val route = new TestApp(get(complete(StatusCodes.OK))) + Get(s"/api/v1/test") ~> route.appRoute ~> check { + status shouldBe StatusCodes.OK + header("Pragma").map(_.value()) should contain("no-cache") + header[`Cache-Control`].map(_.value()) should contain("no-cache") + } + } +} -- cgit v1.2.3