aboutsummaryrefslogtreecommitdiff
path: root/src/test/scala/xyz/driver/core
diff options
context:
space:
mode:
Diffstat (limited to 'src/test/scala/xyz/driver/core')
-rw-r--r--src/test/scala/xyz/driver/core/AuthTest.scala77
-rw-r--r--src/test/scala/xyz/driver/core/CoreTest.scala61
-rw-r--r--src/test/scala/xyz/driver/core/FileTest.scala126
-rw-r--r--src/test/scala/xyz/driver/core/GeneratorsTest.scala234
-rw-r--r--src/test/scala/xyz/driver/core/JsonTest.scala101
-rw-r--r--src/test/scala/xyz/driver/core/MessagesTest.scala80
-rw-r--r--src/test/scala/xyz/driver/core/StatsTest.scala43
-rw-r--r--src/test/scala/xyz/driver/core/TimeTest.scala87
8 files changed, 809 insertions, 0 deletions
diff --git a/src/test/scala/xyz/driver/core/AuthTest.scala b/src/test/scala/xyz/driver/core/AuthTest.scala
new file mode 100644
index 0000000..fef3eda
--- /dev/null
+++ b/src/test/scala/xyz/driver/core/AuthTest.scala
@@ -0,0 +1,77 @@
+package xyz.driver.core
+
+import akka.http.scaladsl.testkit.ScalatestRouteTest
+import akka.http.scaladsl.server._
+import Directives._
+import akka.http.scaladsl.model.headers.{HttpChallenges, RawHeader}
+import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected
+import org.scalatest.mock.MockitoSugar
+import org.scalatest.{FlatSpec, Matchers}
+import xyz.driver.core.auth._
+
+import scala.concurrent.Future
+import scalaz.OptionT
+
+class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRouteTest {
+
+ val authStatusService: AuthService[User] = new AuthService[User] {
+ override def authStatus(authToken: AuthToken): OptionT[Future, User] = OptionT.optionT[Future] {
+ Future.successful(Some(new User() {
+ override def id: Id[User] = Id[User](1L)
+ override def roles: Set[Role] = Set(PathologistRole)
+ }))
+ }
+ }
+
+ import authStatusService._
+
+ "'authorize' directive" should "throw error is auth token is not in the request" in {
+
+ Get("/naive/attempt") ~>
+ authorize(CanSignOutReport) {
+ case (authToken, user) =>
+ complete("Never going to be here")
+ } ~>
+ check {
+ handled shouldBe false
+ rejections should contain(MissingHeaderRejection("WWW-Authenticate"))
+ }
+ }
+
+ it should "throw error is authorized user is not having the requested permission" in {
+
+ val referenceAuthToken = AuthToken(Base64("I am a pathologist's token"))
+
+ Post("/administration/attempt").addHeader(
+ RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value.value)
+ ) ~>
+ authorize(CanAssignRoles) {
+ case (authToken, user) =>
+ complete("Never going to get here")
+ } ~>
+ check {
+ handled shouldBe false
+ rejections should contain(
+ AuthenticationFailedRejection(
+ CredentialsRejected,
+ HttpChallenges.basic("User does not have the required permissions: CanAssignRoles")))
+ }
+ }
+
+ it should "pass and retrieve the token to client code, if token is in request and user has permission" in {
+
+ val referenceAuthToken = AuthToken(Base64("I am token"))
+
+ Get("/valid/attempt/?a=2&b=5").addHeader(
+ RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value.value)
+ ) ~>
+ authorize(CanSignOutReport) {
+ case (authToken, user) =>
+ complete("Alright, \"" + authToken.value.value + "\" is handled")
+ } ~>
+ check {
+ handled shouldBe true
+ responseAs[String] shouldBe "Alright, \"I am token\" is handled"
+ }
+ }
+}
diff --git a/src/test/scala/xyz/driver/core/CoreTest.scala b/src/test/scala/xyz/driver/core/CoreTest.scala
new file mode 100644
index 0000000..f9a1aab
--- /dev/null
+++ b/src/test/scala/xyz/driver/core/CoreTest.scala
@@ -0,0 +1,61 @@
+package xyz.driver.core
+
+import java.io.ByteArrayOutputStream
+
+import org.mockito.Mockito._
+import org.scalatest.mock.MockitoSugar
+import org.scalatest.{FlatSpec, Matchers}
+import xyz.driver.core.revision.Revision
+
+class CoreTest extends FlatSpec with Matchers with MockitoSugar {
+
+ "'make' function" should "allow initialization for objects" in {
+
+ val createdAndInitializedValue = make(new ByteArrayOutputStream(128)) { baos =>
+ baos.write(Array(1.toByte, 1.toByte, 0.toByte))
+ }
+
+ createdAndInitializedValue.toByteArray should be(Array(1.toByte, 1.toByte, 0.toByte))
+ }
+
+ "'using' function" should "call close after performing action on resource" in {
+
+ val baos = mock[ByteArrayOutputStream]
+
+ using(baos /* usually new ByteArrayOutputStream(128) */ ) { baos =>
+ baos.write(Array(1.toByte, 1.toByte, 0.toByte))
+ }
+
+ verify(baos).close()
+ }
+
+ "Id" should "have equality and ordering working correctly" in {
+
+ (Id[String](1234213L) === Id[String](1234213L)) should be(true)
+ (Id[String](1234213L) === Id[String](213414L)) should be(false)
+ (Id[String](213414L) === Id[String](1234213L)) should be(false)
+
+ Seq(Id[String](4L), Id[String](3L), Id[String](2L), Id[String](1L)).sorted should contain
+ theSameElementsInOrderAs(Seq(Id[String](1L), Id[String](2L), Id[String](3L), Id[String](4L)))
+ }
+
+ "Name" should "have equality and ordering working correctly" in {
+
+ (Name[String]("foo") === Name[String]("foo")) should be(true)
+ (Name[String]("foo") === Name[String]("bar")) should be(false)
+ (Name[String]("bar") === Name[String]("foo")) should be(false)
+
+ Seq(Name[String]("d"), Name[String]("cc"), Name[String]("a"), Name[String]("bbb")).sorted should contain
+ theSameElementsInOrderAs(Seq(Name[String]("a"), Name[String]("bbb"), Name[String]("cc"), Name[String]("d")))
+ }
+
+ "Revision" should "have equality working correctly" in {
+
+ val bla = Revision[String]("85569dab-a3dc-401b-9f95-d6fb4162674b")
+ val foo = Revision[String]("f54b3558-bdcd-4646-a14b-8beb11f6b7c4")
+
+ (bla === bla) should be(true)
+ (bla === foo) should be(false)
+ (foo === bla) should be(false)
+ }
+}
diff --git a/src/test/scala/xyz/driver/core/FileTest.scala b/src/test/scala/xyz/driver/core/FileTest.scala
new file mode 100644
index 0000000..aba79f7
--- /dev/null
+++ b/src/test/scala/xyz/driver/core/FileTest.scala
@@ -0,0 +1,126 @@
+package xyz.driver.core
+
+import java.io.File
+import java.nio.file.Paths
+
+import com.amazonaws.services.s3.AmazonS3
+import com.amazonaws.services.s3.model._
+import org.mockito.Matchers._
+import org.mockito.Mockito._
+import org.scalatest.mock.MockitoSugar
+import org.scalatest.{FlatSpec, Matchers}
+import xyz.driver.core.file.{FileSystemStorage, S3Storage}
+
+import scala.concurrent.Await
+import scala.concurrent.duration._
+
+class FileTest extends FlatSpec with Matchers with MockitoSugar {
+
+ "S3 Storage" should "create and download local files and do other operations" in {
+ import scala.collection.JavaConverters._
+
+ val tempDir = System.getProperty("java.io.tmpdir")
+ val sourceTestFile = generateTestLocalFile(tempDir)
+ val testFileName = "uploadTestFile"
+
+ val randomFolderName = java.util.UUID.randomUUID().toString
+ val testDirPath = Paths.get(randomFolderName)
+ val testFilePath = Paths.get(randomFolderName, testFileName)
+
+ val testBucket = Name[Bucket]("IamBucket")
+
+ val s3PutMock = mock[PutObjectResult]
+ when(s3PutMock.getETag).thenReturn("IAmEtag")
+
+ val s3ObjectSummaryMock = mock[S3ObjectSummary]
+ when(s3ObjectSummaryMock.getKey).thenReturn(testFileName)
+ when(s3ObjectSummaryMock.getETag).thenReturn("IAmEtag")
+ when(s3ObjectSummaryMock.getLastModified).thenReturn(new java.util.Date())
+
+ val s3ResultsMock = mock[ListObjectsV2Result]
+ when(s3ResultsMock.getNextContinuationToken).thenReturn("continuationToken")
+ when(s3ResultsMock.isTruncated).thenReturn(false, // before file created it is empty (zero pages)
+ true,
+ false, // after file is uploaded it contains this one file (one page)
+ false) // after file is deleted it is empty (zero pages) again
+ when(s3ResultsMock.getObjectSummaries).thenReturn(
+ // before file created it is empty, `getObjectSummaries` is never called
+ List[S3ObjectSummary](s3ObjectSummaryMock).asJava, // after file is uploaded it contains this one file
+ List.empty[S3ObjectSummary].asJava) // after file is deleted it is empty again
+
+ val s3ObjectMetadataMock = mock[ObjectMetadata]
+ val amazonS3Mock = mock[AmazonS3]
+ when(amazonS3Mock.listObjectsV2(any[ListObjectsV2Request]())).thenReturn(s3ResultsMock)
+ when(amazonS3Mock.putObject(testBucket, testFilePath.toString, sourceTestFile)).thenReturn(s3PutMock)
+ when(amazonS3Mock.getObject(any[GetObjectRequest](), any[File]())).thenReturn(s3ObjectMetadataMock)
+
+ val s3Storage = new S3Storage(amazonS3Mock, testBucket, scala.concurrent.ExecutionContext.global)
+
+ val filesBefore = Await.result(s3Storage.list(testDirPath).run, 10 seconds)
+ filesBefore shouldBe empty
+
+ Await.result(s3Storage.upload(sourceTestFile, testFilePath), 10 seconds)
+
+ val filesAfterUpload = Await.result(s3Storage.list(testDirPath).run, 10 seconds)
+ filesAfterUpload.size should be(1)
+ val uploadedFileLine = filesAfterUpload.head
+ uploadedFileLine.name should be(Name[File](testFileName))
+ uploadedFileLine.location should be(testFilePath)
+ uploadedFileLine.revision.id.length should be > 0
+ uploadedFileLine.lastModificationDate.millis should be > 0L
+
+ val downloadedFile = Await.result(s3Storage.download(testFilePath).run, 10 seconds)
+ downloadedFile shouldBe defined
+ downloadedFile.foreach {
+ _.getAbsolutePath.endsWith(testFilePath.toString) should be(true)
+ }
+
+ Await.result(s3Storage.delete(testFilePath), 10 seconds)
+
+ val filesAfterRemoval = Await.result(s3Storage.list(testDirPath).run, 10 seconds)
+ filesAfterRemoval shouldBe empty
+ }
+
+ "Filesystem files storage" should "create and download local files and do other operations" in {
+
+ val tempDir = System.getProperty("java.io.tmpdir")
+ val sourceTestFile = generateTestLocalFile(tempDir)
+
+ val randomFolderName = java.util.UUID.randomUUID().toString
+ val testDirPath = Paths.get(tempDir, randomFolderName)
+ val testFilePath = Paths.get(tempDir, randomFolderName, "uploadTestFile")
+
+ val fileStorage = new FileSystemStorage(scala.concurrent.ExecutionContext.global)
+
+ val filesBefore = Await.result(fileStorage.list(testDirPath).run, 10 seconds)
+ filesBefore shouldBe empty
+
+ Await.result(fileStorage.upload(sourceTestFile, testFilePath), 10 seconds)
+
+ val filesAfterUpload = Await.result(fileStorage.list(testDirPath).run, 10 seconds)
+ filesAfterUpload.size should be(1)
+ val uploadedFileLine = filesAfterUpload.head
+ uploadedFileLine.name should be(Name[File]("uploadTestFile"))
+ uploadedFileLine.location should be(testFilePath)
+ uploadedFileLine.revision.id.length should be > 0
+ uploadedFileLine.lastModificationDate.millis should be > 0L
+
+ val downloadedFile = Await.result(fileStorage.download(testFilePath).run, 10 seconds)
+ downloadedFile shouldBe defined
+ downloadedFile.map(_.getAbsolutePath) should be(Some(testFilePath.toString))
+
+ Await.result(fileStorage.delete(testFilePath), 10 seconds)
+
+ val filesAfterRemoval = Await.result(fileStorage.list(testDirPath).run, 10 seconds)
+ filesAfterRemoval shouldBe empty
+ }
+
+ private def generateTestLocalFile(path: String): File = {
+ val randomSourceFolderName = java.util.UUID.randomUUID().toString
+ val sourceTestFile = new File(Paths.get(path, randomSourceFolderName, "uploadTestFile").toString)
+ sourceTestFile.getParentFile.mkdirs() should be(true)
+ sourceTestFile.createNewFile() should be(true)
+ using(new java.io.PrintWriter(sourceTestFile)) { _.append("Test File Contents") }
+ sourceTestFile
+ }
+}
diff --git a/src/test/scala/xyz/driver/core/GeneratorsTest.scala b/src/test/scala/xyz/driver/core/GeneratorsTest.scala
new file mode 100644
index 0000000..0432b2a
--- /dev/null
+++ b/src/test/scala/xyz/driver/core/GeneratorsTest.scala
@@ -0,0 +1,234 @@
+package xyz.driver.core
+
+import org.scalatest.{Assertions, FlatSpec, Matchers}
+
+class GeneratorsTest extends FlatSpec with Matchers with Assertions {
+ import generators._
+
+ "Generators" should "be able to generate com.drivergrp.core.Id identifiers" in {
+
+ val generatedId1 = nextId[String]()
+ val generatedId2 = nextId[String]()
+ val generatedId3 = nextId[Long]()
+
+ generatedId1 should be >= 0L
+ generatedId2 should be >= 0L
+ generatedId3 should be >= 0L
+ generatedId1 should not be generatedId2
+ generatedId2 should !==(generatedId3)
+ }
+
+ it should "be able to generate com.drivergrp.core.Id identifiers with max value" in {
+
+ val generatedLimitedId1 = nextId[String](10000)
+ val generatedLimitedId2 = nextId[String](1000)
+ val generatedLimitedId3 = nextId[Long](2000)
+
+ generatedLimitedId1 should be >= 0L
+ generatedLimitedId1 should be < 10000L
+ generatedLimitedId2 should be >= 0L
+ generatedLimitedId2 should be < 1000L
+ generatedLimitedId3 should be >= 0L
+ generatedLimitedId3 should be < 2000L
+ generatedLimitedId1 should not be generatedLimitedId2
+ generatedLimitedId2 should !==(generatedLimitedId3)
+ }
+
+ it should "be able to generate com.drivergrp.core.Name names" in {
+
+ nextName[String]() should not be nextName[String]()
+ nextName[String]().length should be >= 0
+
+ val fixedLengthName = nextName[String](10)
+ fixedLengthName.length should be <= 10
+ assert(!fixedLengthName.exists(_.isControl))
+ }
+
+ it should "be able to generate proper UUIDs" in {
+
+ nextUuid() should not be nextUuid()
+ nextUuid().toString.length should be(36)
+ }
+
+ it should "be able to generate new Revisions" in {
+
+ nextRevision[String]() should not be nextRevision[String]()
+ nextRevision[String]().id.length should be > 0
+ }
+
+ it should "be able to generate strings" in {
+
+ nextString() should not be nextString()
+ nextString().length should be >= 0
+
+ val fixedLengthString = nextString(20)
+ fixedLengthString.length should be <= 20
+ assert(!fixedLengthString.exists(_.isControl))
+ }
+
+ it should "be able to generate options which are sometimes have values and sometimes not" in {
+
+ val generatedOption = nextOption("2")
+
+ generatedOption should not contain "1"
+ assert(generatedOption === Some("2") || generatedOption === None)
+ }
+
+ it should "be able to generate a pair of two generated values" in {
+
+ val constantPair = nextPair("foo", 1L)
+ constantPair._1 should be("foo")
+ constantPair._2 should be(1L)
+
+ val generatedPair = nextPair(nextId[Int](), nextName[Int]())
+
+ generatedPair._1 should be > 0L
+ generatedPair._2.length should be > 0
+
+ nextPair(nextId[Int](), nextName[Int]()) should not be
+ nextPair(nextId[Int](), nextName[Int]())
+ }
+
+ it should "be able to generate a triad of two generated values" in {
+
+ val constantTriad = nextTriad("foo", "bar", 1L)
+ constantTriad._1 should be("foo")
+ constantTriad._2 should be("bar")
+ constantTriad._3 should be(1L)
+
+ val generatedTriad = nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal())
+
+ generatedTriad._1 should be > 0L
+ generatedTriad._2.length should be > 0
+ generatedTriad._3 should be >= BigDecimal(0.00)
+
+ nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal()) should not be
+ nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal())
+ }
+
+ it should "be able to generate a time value" in {
+
+ val generatedTime = nextTime()
+ val currentTime = System.currentTimeMillis()
+
+ generatedTime.millis should be >= 0L
+ generatedTime.millis should be <= currentTime
+ }
+
+ it should "be able to generate a time range value" in {
+
+ val generatedTimeRange = nextTimeRange()
+ val currentTime = System.currentTimeMillis()
+
+ generatedTimeRange.start.millis should be >= 0L
+ generatedTimeRange.start.millis should be <= currentTime
+ generatedTimeRange.end.millis should be >= 0L
+ generatedTimeRange.end.millis should be <= currentTime
+ generatedTimeRange.start.millis should be <= generatedTimeRange.end.millis
+ }
+
+ it should "be able to generate a BigDecimal value" in {
+
+ val defaultGeneratedBigDecimal = nextBigDecimal()
+
+ defaultGeneratedBigDecimal should be >= BigDecimal(0.00)
+ defaultGeneratedBigDecimal should be <= BigDecimal(1000000.00)
+ defaultGeneratedBigDecimal.precision should be(2)
+
+ val unitIntervalBigDecimal = nextBigDecimal(1.00, 8)
+
+ unitIntervalBigDecimal should be >= BigDecimal(0.00)
+ unitIntervalBigDecimal should be <= BigDecimal(1.00)
+ unitIntervalBigDecimal.precision should be(8)
+ }
+
+ it should "be able to generate a specific value from a set of values" in {
+
+ val possibleOptions = Set(1, 3, 5, 123, 0, 9)
+
+ val pick1 = generators.oneOf(possibleOptions)
+ val pick2 = generators.oneOf(possibleOptions)
+ val pick3 = generators.oneOf(possibleOptions)
+
+ possibleOptions should contain(pick1)
+ possibleOptions should contain(pick2)
+ possibleOptions should contain(pick3)
+
+ val pick4 = generators.oneOf(1, 3, 5, 123, 0, 9)
+ val pick5 = generators.oneOf(1, 3, 5, 123, 0, 9)
+ val pick6 = generators.oneOf(1, 3, 5, 123, 0, 9)
+
+ possibleOptions should contain(pick4)
+ possibleOptions should contain(pick5)
+ possibleOptions should contain(pick6)
+
+ Set(pick1, pick2, pick3, pick4, pick5, pick6).size should be >= 1
+ }
+
+ it should "be able to generate array with values generated by generators" in {
+
+ val arrayOfTimes = arrayOf(nextTime(), 16)
+ arrayOfTimes.length should be <= 16
+
+ val arrayOfBigDecimals = arrayOf(nextBigDecimal(), 8)
+ arrayOfBigDecimals.length should be <= 8
+ }
+
+ it should "be able to generate seq with values generated by generators" in {
+
+ val seqOfTimes = seqOf(nextTime(), 16)
+ seqOfTimes.size should be <= 16
+
+ val seqOfBigDecimals = seqOf(nextBigDecimal(), 8)
+ seqOfBigDecimals.size should be <= 8
+ }
+
+ it should "be able to generate vector with values generated by generators" in {
+
+ val vectorOfTimes = vectorOf(nextTime(), 16)
+ vectorOfTimes.size should be <= 16
+
+ val vectorOfStrings = seqOf(nextString(), 8)
+ vectorOfStrings.size should be <= 8
+ }
+
+ it should "be able to generate list with values generated by generators" in {
+
+ val listOfTimes = listOf(nextTime(), 16)
+ listOfTimes.size should be <= 16
+
+ val listOfBigDecimals = seqOf(nextBigDecimal(), 8)
+ listOfBigDecimals.size should be <= 8
+ }
+
+ it should "be able to generate set with values generated by generators" in {
+
+ val setOfTimes = vectorOf(nextTime(), 16)
+ setOfTimes.size should be <= 16
+
+ val setOfBigDecimals = seqOf(nextBigDecimal(), 8)
+ setOfBigDecimals.size should be <= 8
+ }
+
+ it should "be able to generate maps with keys and values generated by generators" in {
+
+ val generatedConstantMap = mapOf(10, "key", 123)
+ generatedConstantMap.size should be <= 1
+ assert(generatedConstantMap.keys.forall(_ == "key"))
+ assert(generatedConstantMap.values.forall(_ == 123))
+
+ val generatedMap = mapOf(10, nextString(10), nextBigDecimal())
+ assert(generatedMap.keys.forall(_.length <= 10))
+ assert(generatedMap.values.forall(_ >= BigDecimal(0.00)))
+ }
+
+ it should "compose deeply" in {
+
+ val generatedNestedMap = mapOf(10, nextString(10), nextPair(nextBigDecimal(), nextOption(123)))
+
+ generatedNestedMap.size should be <= 10
+ generatedNestedMap.keySet.size should be <= 10
+ generatedNestedMap.values.size should be <= 10
+ assert(generatedNestedMap.values.forall(value => !value._2.exists(_ != 123)))
+ }
+}
diff --git a/src/test/scala/xyz/driver/core/JsonTest.scala b/src/test/scala/xyz/driver/core/JsonTest.scala
new file mode 100644
index 0000000..bcdcd5d
--- /dev/null
+++ b/src/test/scala/xyz/driver/core/JsonTest.scala
@@ -0,0 +1,101 @@
+package xyz.driver.core
+
+import org.scalatest.{FlatSpec, Matchers}
+import xyz.driver.core.json.{EnumJsonFormat, ValueClassFormat}
+import xyz.driver.core.revision.Revision
+import xyz.driver.core.time.provider.SystemTimeProvider
+
+class JsonTest extends FlatSpec with Matchers {
+
+ "Json format for Id" should "read and write correct JSON" in {
+
+ val referenceId = Id[String](1312L)
+
+ val writtenJson = json.idFormat.write(referenceId)
+ writtenJson.prettyPrint should be("1312")
+
+ val parsedId = json.idFormat.read(writtenJson)
+ parsedId should be(referenceId)
+ }
+
+ "Json format for Name" should "read and write correct JSON" in {
+
+ val referenceName = Name[String]("Homer")
+
+ val writtenJson = json.nameFormat.write(referenceName)
+ writtenJson.prettyPrint should be("\"Homer\"")
+
+ val parsedName = json.nameFormat.read(writtenJson)
+ parsedName should be(referenceName)
+ }
+
+ "Json format for Time" should "read and write correct JSON" in {
+
+ val referenceTime = new SystemTimeProvider().currentTime()
+
+ val writtenJson = json.timeFormat.write(referenceTime)
+ writtenJson.prettyPrint should be("{\n \"timestamp\": " + referenceTime.millis + "\n}")
+
+ val parsedTime = json.timeFormat.read(writtenJson)
+ parsedTime should be(referenceTime)
+ }
+
+ "Json format for Revision" should "read and write correct JSON" in {
+
+ val referenceRevision = Revision[String]("037e2ec0-8901-44ac-8e53-6d39f6479db4")
+
+ val writtenJson = json.revisionFormat.write(referenceRevision)
+ writtenJson.prettyPrint should be("\"" + referenceRevision.id + "\"")
+
+ val parsedRevision = json.revisionFormat.read(writtenJson)
+ parsedRevision should be(referenceRevision)
+ }
+
+ "Json format for Enums" should "read and write correct JSON" in {
+
+ sealed trait EnumVal
+ case object Val1 extends EnumVal
+ case object Val2 extends EnumVal
+ case object Val3 extends EnumVal
+
+ val format = new EnumJsonFormat[EnumVal]("a" -> Val1, "b" -> Val2, "c" -> Val3)
+
+ val referenceEnumValue1 = Val2
+ val referenceEnumValue2 = Val3
+
+ val writtenJson1 = format.write(referenceEnumValue1)
+ writtenJson1.prettyPrint should be("\"b\"")
+
+ val writtenJson2 = format.write(referenceEnumValue2)
+ writtenJson2.prettyPrint should be("\"c\"")
+
+ val parsedEnumValue1 = format.read(writtenJson1)
+ val parsedEnumValue2 = format.read(writtenJson2)
+
+ parsedEnumValue1 should be(referenceEnumValue1)
+ parsedEnumValue2 should be(referenceEnumValue2)
+ }
+
+ // Should be defined outside of case to have a TypeTag
+ case class CustomWrapperClass(value: Int)
+
+ "Json format for Value classes" should "read and write correct JSON" in {
+
+ val format = new ValueClassFormat[CustomWrapperClass](v => BigDecimal(v.value), d => CustomWrapperClass(d.toInt))
+
+ val referenceValue1 = CustomWrapperClass(-2)
+ val referenceValue2 = CustomWrapperClass(10)
+
+ val writtenJson1 = format.write(referenceValue1)
+ writtenJson1.prettyPrint should be("-2")
+
+ val writtenJson2 = format.write(referenceValue2)
+ writtenJson2.prettyPrint should be("10")
+
+ val parsedValue1 = format.read(writtenJson1)
+ val parsedValue2 = format.read(writtenJson2)
+
+ parsedValue1 should be(referenceValue1)
+ parsedValue2 should be(referenceValue2)
+ }
+}
diff --git a/src/test/scala/xyz/driver/core/MessagesTest.scala b/src/test/scala/xyz/driver/core/MessagesTest.scala
new file mode 100644
index 0000000..dc44ee1
--- /dev/null
+++ b/src/test/scala/xyz/driver/core/MessagesTest.scala
@@ -0,0 +1,80 @@
+package xyz.driver.core
+
+import java.util.Locale
+
+import com.typesafe.config.{ConfigException, ConfigFactory}
+import org.mockito.Mockito._
+import org.scalatest.mock.MockitoSugar
+import org.scalatest.{FlatSpec, Matchers}
+import xyz.driver.core.logging.Logger
+import xyz.driver.core.messages.Messages
+
+import scala.collection.JavaConversions._
+
+class MessagesTest extends FlatSpec with Matchers with MockitoSugar {
+
+ val englishLocaleMessages =
+ Map("en.greeting" -> "Hello {0}!", "en.greetingFullName" -> "Hello {0} {1} {2}!", "en.hello" -> "Hello world!")
+
+ "Messages" should "read messages from config and format with parameters" in {
+
+ val log = mock[Logger]
+ val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages)
+
+ val messages = Messages.messages(messagesConfig, log, Locale.US)
+
+ messages("hello") should be("Hello world!")
+ messages("greeting", "Homer") should be("Hello Homer!")
+ messages("greetingFullName", "Homer", "J", "Simpson") should be("Hello Homer J Simpson!")
+ }
+
+ it should "be able to read messages for different locales" in {
+
+ val log = mock[Logger]
+
+ val messagesConfig = ConfigFactory.parseMap(
+ englishLocaleMessages ++ Map(
+ "zh.hello" -> "你好,世界!",
+ "zh.greeting" -> "你好,{0}!",
+ "zh.greetingFullName" -> "你好,{0} {1} {2}!"
+ ))
+
+ val englishMessages = Messages.messages(messagesConfig, log, Locale.US)
+ val englishMessagesToo = Messages.messages(messagesConfig, log, Locale.ENGLISH)
+ val chineseMessages = Messages.messages(messagesConfig, log, Locale.CHINESE)
+
+ englishMessages("hello") should be("Hello world!")
+ englishMessages("greeting", "Homer") should be("Hello Homer!")
+ englishMessages("greetingFullName", "Homer", "J", "Simpson") should be("Hello Homer J Simpson!")
+
+ englishMessagesToo("hello") should be(englishMessages("hello"))
+ englishMessagesToo("greeting", "Homer") should be(englishMessages("greeting", "Homer"))
+ englishMessagesToo("greetingFullName", "Homer", "J", "Simpson") should be(
+ englishMessages("greetingFullName", "Homer", "J", "Simpson"))
+
+ chineseMessages("hello") should be("你好,世界!")
+ chineseMessages("greeting", "Homer") should be("你好,Homer!")
+ chineseMessages("greetingFullName", "Homer", "J", "Simpson") should be("你好,Homer J Simpson!")
+ }
+
+ it should "raise exception when locale is not available" in {
+
+ val log = mock[Logger]
+ val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages)
+
+ an[ConfigException.Missing] should be thrownBy
+ Messages.messages(messagesConfig, log, Locale.GERMAN)
+ }
+
+ it should "log a problem, when there is no message for key" in {
+
+ val log = mock[Logger]
+ val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages)
+
+ val messages = Messages.messages(messagesConfig, log, Locale.US)
+
+ messages("howdy") should be("howdy")
+
+ verify(log).error(s"Message with key 'howdy' not found for locale 'en'")
+ }
+}
diff --git a/src/test/scala/xyz/driver/core/StatsTest.scala b/src/test/scala/xyz/driver/core/StatsTest.scala
new file mode 100644
index 0000000..27ea1bd
--- /dev/null
+++ b/src/test/scala/xyz/driver/core/StatsTest.scala
@@ -0,0 +1,43 @@
+package xyz.driver.core
+
+import org.mockito.Mockito._
+import org.scalatest.mock.MockitoSugar
+import org.scalatest.{FlatSpec, Matchers}
+import xyz.driver.core.logging.Logger
+import xyz.driver.core.stats.LogStats
+import xyz.driver.core.time.{Time, TimeRange}
+
+class StatsTest extends FlatSpec with Matchers with MockitoSugar {
+
+ "Stats" should "format and store all recorded data" in {
+
+ val log = mock[Logger]
+ val stats = new LogStats(log)
+
+ stats.recordStats(Seq(), TimeRange(Time(2L), Time(5L)), BigDecimal(123.324))
+ verify(log).audit(s"(2-5)=123.324")
+
+ stats.recordStats("stat", TimeRange(Time(5L), Time(5L)), BigDecimal(333L))
+ verify(log).audit(s"stat(5-5)=333")
+
+ stats.recordStats("stat", Time(934L), 123)
+ verify(log).audit(s"stat(934-934)=123")
+
+ stats.recordStats("stat", Time(0L), 123)
+ verify(log).audit(s"stat(0-0)=123")
+ }
+
+ it should "format BigDecimal with all precision digits" in {
+
+ val log = mock[Logger]
+ val stats = new LogStats(log)
+
+ stats.recordStats(Seq("root", "group", "stat", "substat"),
+ TimeRange(Time(1467381889834L), Time(1468937089834L)),
+ BigDecimal(3.333333333333333))
+ verify(log).audit(s"root.group.stat.substat(1467381889834-1468937089834)=3.333333333333333")
+
+ stats.recordStats("stat", Time(1233L), BigDecimal(0.00000000000000000000001))
+ verify(log).audit(s"stat(1233-1233)=0.000000000000000000000010")
+ }
+}
diff --git a/src/test/scala/xyz/driver/core/TimeTest.scala b/src/test/scala/xyz/driver/core/TimeTest.scala
new file mode 100644
index 0000000..76ef42c
--- /dev/null
+++ b/src/test/scala/xyz/driver/core/TimeTest.scala
@@ -0,0 +1,87 @@
+package xyz.driver.core
+
+import java.util.TimeZone
+
+import org.scalacheck.Arbitrary._
+import org.scalacheck.Prop.BooleanOperators
+import org.scalacheck.{Arbitrary, Gen}
+import org.scalatest.prop.Checkers
+import org.scalatest.{FlatSpec, Matchers}
+import xyz.driver.core.time.{Time, _}
+
+import scala.concurrent.duration._
+
+class TimeTest extends FlatSpec with Matchers with Checkers {
+
+ implicit val arbitraryDuration = Arbitrary[Duration](Gen.chooseNum(0L, 9999999999L).map(_.milliseconds))
+ implicit val arbitraryTime = Arbitrary[Time](Gen.chooseNum(0L, 9999999999L).map(millis => Time(millis)))
+
+ "Time" should "have correct methods to compare" in {
+
+ Time(234L).isAfter(Time(123L)) should be(true)
+ Time(123L).isAfter(Time(123L)) should be(false)
+ Time(123L).isAfter(Time(234L)) should be(false)
+
+ check((a: Time, b: Time) => (a.millis > b.millis) ==> a.isAfter(b))
+
+ Time(234L).isBefore(Time(123L)) should be(false)
+ Time(123L).isBefore(Time(123L)) should be(false)
+ Time(123L).isBefore(Time(234L)) should be(true)
+
+ check { (a: Time, b: Time) =>
+ (a.millis < b.millis) ==> a.isBefore(b)
+ }
+ }
+
+ it should "not modify time" in {
+
+ Time(234L).millis should be(234L)
+
+ check { millis: Long =>
+ Time(millis).millis == millis
+ }
+ }
+
+ it should "support arithmetic with scala.concurrent.duration" in {
+
+ Time(123L).advanceBy(0 minutes).millis should be(123L)
+ Time(123L).advanceBy(1 second).millis should be(123L + Second)
+ Time(123L).advanceBy(4 days).millis should be(123L + 4 * Days)
+
+ check { (time: Time, duration: Duration) =>
+ time.advanceBy(duration).millis == (time.millis + duration.toMillis)
+ }
+ }
+
+ it should "have ordering defined correctly" in {
+
+ Seq(Time(321L), Time(123L), Time(231L)).sorted should
+ contain theSameElementsInOrderAs Seq(Time(123L), Time(231L), Time(321L))
+
+ check { times: List[Time] =>
+ times.sorted.sliding(2).filter(_.size == 2).forall {
+ case Seq(a, b) =>
+ a.millis <= b.millis
+ }
+ }
+ }
+
+ it should "reset to the start of the period, e.g. month" in {
+
+ startOfMonth(Time(1468937089834L)) should be(Time(1467381889834L))
+ startOfMonth(Time(1467381889834L)) should be(Time(1467381889834L)) // idempotent
+ }
+
+ it should "have correct textual representations" in {
+
+ textualDate(TimeZone.getTimeZone("EDT"))(Time(1468937089834L)) should be("July 19, 2016")
+ textualTime(TimeZone.getTimeZone("PDT"))(Time(1468937089834L)) should be("Jul 19, 2016 02:04:49 PM")
+ }
+
+ "TimeRange" should "have duration defined as a difference of start and end times" in {
+
+ TimeRange(Time(321L), Time(432L)).duration should be(111.milliseconds)
+ TimeRange(Time(432L), Time(321L)).duration should be((-111).milliseconds)
+ TimeRange(Time(333L), Time(333L)).duration should be(0.milliseconds)
+ }
+}