aboutsummaryrefslogtreecommitdiff
path: root/src/test
diff options
context:
space:
mode:
Diffstat (limited to 'src/test')
-rw-r--r--src/test/scala/xyz/driver/common/BaseSuite.scala51
-rw-r--r--src/test/scala/xyz/driver/common/Mocks.scala89
-rw-r--r--src/test/scala/xyz/driver/common/concurrent/BridgeUploadQueueRepositoryAdapterSuite.scala221
-rw-r--r--src/test/scala/xyz/driver/common/db/QueryBuilderParametersSuite.scala249
-rw-r--r--src/test/scala/xyz/driver/common/db/SearchFilterExprSuite.scala32
-rw-r--r--src/test/scala/xyz/driver/common/error/UnexpectedFilterException.scala3
-rw-r--r--src/test/scala/xyz/driver/common/logging/PhiStringContextSuite.scala32
-rw-r--r--src/test/scala/xyz/driver/common/pdf/MockPdfRenderer.scala25
-rw-r--r--src/test/scala/xyz/driver/common/utils/DiffUtils.scala52
9 files changed, 754 insertions, 0 deletions
diff --git a/src/test/scala/xyz/driver/common/BaseSuite.scala b/src/test/scala/xyz/driver/common/BaseSuite.scala
new file mode 100644
index 0000000..b4c3d03
--- /dev/null
+++ b/src/test/scala/xyz/driver/common/BaseSuite.scala
@@ -0,0 +1,51 @@
+package xyz.driver.common
+
+import java.time.{LocalDateTime, ZoneId}
+
+import org.scalatest.FreeSpecLike
+import org.scalatest.concurrent.ScalaFutures
+import org.scalatest.time.{Millis, Span}
+import xyz.driver.common.db.{MysqlQueryBuilder, SearchFilterExpr, SqlContext, Transactions}
+import xyz.driver.common.domain.{Email, LongId, PasswordHash, User}
+import xyz.driver.common.error.UnexpectedFilterException
+import xyz.driver.common.utils.DiffUtils
+
+import scala.concurrent.ExecutionContext.Implicits._
+import scala.concurrent.Future
+
+trait BaseSuite extends FreeSpecLike with DiffUtils with ScalaFutures {
+
+ implicit val defaultPatience = PatienceConfig(timeout = Span(1000, Millis), interval = Span(20, Millis))
+ implicit val sqlContext = new MockSqlContext(global)
+
+ def sampleUser(role: User.Role,
+ email: String = "test@example.com",
+ password: String = "123") = User(
+ id = LongId(2001),
+ email = Email(email),
+ name = "Test",
+ role = role,
+ passwordHash = PasswordHash(password),
+ latestActivity = Some(LocalDateTime.now(ZoneId.of("Z"))),
+ deleted = None
+ )
+
+ def createMockQueryBuilder[T](isExpectedFilter: SearchFilterExpr => Boolean): MysqlQueryBuilder[T] = {
+ MockQueryBuilder[T] {
+ case (filter, _, _) if isExpectedFilter(filter) =>
+ Future.successful(Seq.empty)
+ case (filter, _, _) =>
+ Future.failed(new UnexpectedFilterException(s"Filter is unexpected: $filter"))
+ } {
+ case _ =>
+ Future.successful((0, Option.empty[LocalDateTime]))
+ }
+ }
+
+ def transactions = new Transactions {
+ override def run[T](f: (SqlContext) => T): Future[T] = {
+ Future(f(sqlContext))
+ }
+ }
+
+}
diff --git a/src/test/scala/xyz/driver/common/Mocks.scala b/src/test/scala/xyz/driver/common/Mocks.scala
new file mode 100644
index 0000000..480ab48
--- /dev/null
+++ b/src/test/scala/xyz/driver/common/Mocks.scala
@@ -0,0 +1,89 @@
+package xyz.driver.common
+
+import java.io.{Closeable, PrintWriter}
+import java.net.URL
+import java.sql.Connection
+import java.util.logging.Logger
+import javax.sql.DataSource
+
+import com.typesafe.config.ConfigFactory
+import xyz.driver.common.db._
+import xyz.driver.common.http.HttpFetcher
+
+import scala.concurrent.{ExecutionContext, Future}
+
+class MockDataSource extends DataSource with Closeable {
+ override def getConnection: Connection = throw new NotImplementedError("MockDataSource.getConnection")
+ override def getConnection(username: String, password: String): Connection = {
+ throw new NotImplementedError(s"MockDataSource.getConnection($username, $password)")
+ }
+ override def close(): Unit = throw new NotImplementedError("MockDataSource.close")
+ override def setLogWriter(out: PrintWriter): Unit = throw new NotImplementedError("MockDataSource.setLogWriter")
+ override def getLoginTimeout: Int = throw new NotImplementedError("MockDataSource.getLoginTimeout")
+ override def setLoginTimeout(seconds: Int): Unit = throw new NotImplementedError("MockDataSource.setLoginTimeout")
+ override def getParentLogger: Logger = throw new NotImplementedError("MockDataSource.getParentLogger")
+ override def getLogWriter: PrintWriter = throw new NotImplementedError("MockDataSource.getLogWriter")
+ override def unwrap[T](iface: Class[T]): T = throw new NotImplementedError("MockDataSource.unwrap")
+ override def isWrapperFor(iface: Class[_]): Boolean = throw new NotImplementedError("MockDataSource.isWrapperFor")
+}
+
+object MockSqlContext {
+
+ val Settings = SqlContext.Settings(
+ credentials = SqlContext.DbCredentials(
+ user = "test",
+ password = "test",
+ host = "localhost",
+ port = 3248,
+ dbName = "test",
+ dbCreateFlag = false,
+ dbContext = "test",
+ connectionParams = "",
+ url = ""
+ ),
+ connection = ConfigFactory.empty(),
+ connectionAttemptsOnStartup = 1,
+ threadPoolSize = 10
+ )
+
+}
+
+class MockSqlContext(ec: ExecutionContext) extends SqlContext(new MockDataSource, MockSqlContext.Settings) {
+ override implicit val executionContext = ec
+ override protected def withConnection[T](f: Connection => T) = {
+ throw new NotImplementedError("MockSqlContext.withConnection")
+ }
+}
+
+class MockFactory()(implicit val sqlContext: SqlContext) {
+ val MockHttpFetcher: HttpFetcher = (url: URL) => {
+ Future.successful(Array.empty[Byte])
+ }
+}
+
+object MockQueryBuilder {
+
+ type MockRunnerIn = (SearchFilterExpr, Sorting, Option[Pagination])
+ type MockRunnerOut[T] = Future[Seq[T]]
+ type MockCountRunnerOut = Future[QueryBuilder.CountResult]
+
+ def apply[T](matcher: PartialFunction[MockRunnerIn, MockRunnerOut[T]])
+ (countMatcher: PartialFunction[MockRunnerIn, MockCountRunnerOut])
+ (implicit context: SqlContext): MysqlQueryBuilder[T] = {
+ def runner(parameters: QueryBuilderParameters): MockRunnerOut[T] = {
+ matcher((parameters.filter, parameters.sorting, parameters.pagination))
+ }
+ def countRunner(parameters: QueryBuilderParameters): MockCountRunnerOut = {
+ countMatcher((parameters.filter, parameters.sorting, parameters.pagination))
+ }
+ MysqlQueryBuilder[T](
+ tableName = "",
+ lastUpdateFieldName = Option.empty[String],
+ nullableFields = Set.empty[String],
+ links = Set.empty[TableLink],
+ runner = runner _,
+ countRunner = countRunner _
+ )(context.executionContext)
+ }
+}
+
diff --git a/src/test/scala/xyz/driver/common/concurrent/BridgeUploadQueueRepositoryAdapterSuite.scala b/src/test/scala/xyz/driver/common/concurrent/BridgeUploadQueueRepositoryAdapterSuite.scala
new file mode 100644
index 0000000..e81d0b3
--- /dev/null
+++ b/src/test/scala/xyz/driver/common/concurrent/BridgeUploadQueueRepositoryAdapterSuite.scala
@@ -0,0 +1,221 @@
+package xyz.driver.common.concurrent
+
+import java.util.concurrent.ThreadLocalRandom
+
+import xyz.driver.common.BaseSuite
+import xyz.driver.common.concurrent.BridgeUploadQueue.Item
+import xyz.driver.common.concurrent.BridgeUploadQueueRepositoryAdapter.Strategy
+import xyz.driver.common.concurrent.BridgeUploadQueueRepositoryAdapter.Strategy.{OnAttempt, OnComplete}
+import xyz.driver.common.db.repositories.BridgeUploadQueueRepository
+import xyz.driver.common.domain.LongId
+
+import scala.concurrent.Future
+import scala.concurrent.duration.DurationInt
+
+class BridgeUploadQueueRepositoryAdapterSuite extends BaseSuite {
+
+ // IDEA have some issue here with imports
+ private implicit val executionContext = scala.concurrent.ExecutionContext.global
+
+ "Strategy" - {
+ "LimitExponential" - {
+ "calculateNextInterval" - {
+ val strategy = Strategy.LimitExponential(
+ startInterval = 10.seconds,
+ intervalFactor = 1.4,
+ maxInterval = 50.seconds,
+ onComplete = OnComplete.Delete
+ )
+
+ "a new interval should be greater than the previous one if the limit not reached" in {
+ val previous = strategy.on(1)
+ val current = strategy.on(2)
+
+ (previous, current) match {
+ case (OnAttempt.Continue(a), OnAttempt.Continue(b)) => assert(a < b)
+ case x => fail(s"Unexpected result: $x")
+ }
+ }
+
+ "should limit intervals" in {
+ assert(strategy.on(20) == OnAttempt.Continue(strategy.maxInterval))
+ }
+ }
+ }
+ }
+
+ "tryRetry" - {
+
+ "when all attempts are not out" - {
+
+ val defaultStrategy = Strategy.Constant(10.seconds)
+
+ "should return an updated item" in {
+ val repository = new BridgeUploadQueueRepository {
+ override def update(draft: EntityT): EntityT = draft
+ override def delete(id: IdT): Unit = {}
+ override def add(draft: EntityT): EntityT = fail("add should not be used!")
+ override def getById(id: LongId[EntityT]): Option[EntityT] = fail("getById should not be used!")
+ override def isCompleted(kind: String, tag: String): Future[Boolean] = fail("isCompleted should not be used!")
+ override def getOne(kind: String): Future[Option[Item]] = fail("getOne should not be used!")
+ }
+
+ val adapter = new BridgeUploadQueueRepositoryAdapter(
+ strategy = defaultStrategy,
+ repository = repository,
+ transactions = transactions
+ )
+
+ val item = defaultItem
+ val r = adapter.tryRetry(item).futureValue
+ assert(r.isDefined)
+ assert(!r.contains(item))
+ }
+
+ "should add an item with increased attempts" in {
+ val item = defaultItem
+
+ val repository = new BridgeUploadQueueRepository {
+ override def update(draft: EntityT): EntityT = {
+ assert(draft.attempts === (item.attempts + 1), "repository.add")
+ draft
+ }
+ override def delete(id: IdT): Unit = {}
+ override def add(draft: EntityT): EntityT = fail("add should not be used!")
+ override def getById(id: LongId[EntityT]): Option[EntityT] = fail("getById should not be used!")
+ override def isCompleted(kind: String, tag: String): Future[Boolean] = fail("isCompleted should not be used!")
+ override def getOne(kind: String): Future[Option[Item]] = fail("getOne should not be used!")
+ }
+
+ val adapter = new BridgeUploadQueueRepositoryAdapter(
+ strategy = defaultStrategy,
+ repository = repository,
+ transactions = transactions
+ )
+
+ adapter.tryRetry(item).isReadyWithin(100.millis)
+ }
+
+ "should remove an old item" in {
+ val item = defaultItem
+
+ val repository = new BridgeUploadQueueRepository {
+ override def update(draft: EntityT): EntityT = draft
+ override def delete(id: IdT): Unit = {
+ assert(id == item.id, "repository.delete")
+ }
+ override def add(draft: EntityT): EntityT = fail("add should not be used!")
+ override def getById(id: LongId[EntityT]): Option[EntityT] = fail("getById should not be used!")
+ override def isCompleted(kind: String, tag: String): Future[Boolean] = fail("isCompleted should not be used!")
+ override def getOne(kind: String): Future[Option[Item]] = fail("getOne should not be used!")
+ }
+
+ val adapter = new BridgeUploadQueueRepositoryAdapter(
+ strategy = defaultStrategy,
+ repository = repository,
+ transactions = transactions
+ )
+
+ adapter.tryRetry(item).isReadyWithin(100.millis)
+ }
+
+ "should update time of the next attempt" in {
+ val item = defaultItem
+
+ val repository = new BridgeUploadQueueRepository {
+ override def update(draft: EntityT): EntityT = {
+ assert(draft.nextAttempt.isAfter(item.nextAttempt), "repository.add")
+ draft
+ }
+ override def delete(id: IdT): Unit = {}
+ override def add(draft: EntityT): EntityT = fail("add should not be used!")
+ override def getById(id: LongId[EntityT]): Option[EntityT] = fail("getById should not be used!")
+ override def isCompleted(kind: String, tag: String): Future[Boolean] = fail("isCompleted should not be used!")
+ override def getOne(kind: String): Future[Option[Item]] = fail("getOne should not be used!")
+ }
+
+ val adapter = new BridgeUploadQueueRepositoryAdapter(
+ strategy = defaultStrategy,
+ repository = repository,
+ transactions = transactions
+ )
+
+ adapter.tryRetry(item).isReadyWithin(100.millis)
+ }
+
+ }
+
+ "when all attempts are out" - {
+
+ val defaultStrategy = Strategy.Ignore
+
+ "should not return an item" in {
+ val repository = new BridgeUploadQueueRepository {
+ override def delete(id: IdT): Unit = {}
+ override def update(entity: EntityT): EntityT = fail("update should not be used!")
+ override def add(draft: EntityT): EntityT = fail("add should not be used!")
+ override def getById(id: LongId[EntityT]): Option[EntityT] = fail("getById should not be used!")
+ override def isCompleted(kind: String, tag: String): Future[Boolean] = fail("isCompleted should not be used!")
+ override def getOne(kind: String): Future[Option[Item]] = fail("getOne should not be used!")
+ }
+
+ val adapter = new BridgeUploadQueueRepositoryAdapter(
+ strategy = defaultStrategy,
+ repository = repository,
+ transactions = transactions
+ )
+
+ val r = adapter.tryRetry(defaultItem).futureValue
+ assert(r.isEmpty)
+ }
+
+ "should not add any item to the queue" in {
+ val repository = new BridgeUploadQueueRepository {
+ override def update(draft: EntityT): EntityT = throw new IllegalAccessException("add should not be called")
+ override def delete(id: IdT): Unit = {}
+ override def add(draft: EntityT): EntityT = fail("add should not be used!")
+ override def getById(id: LongId[EntityT]): Option[EntityT] = fail("getById should not be used!")
+ override def isCompleted(kind: String, tag: String): Future[Boolean] = fail("isCompleted should not be used!")
+ override def getOne(kind: String): Future[Option[Item]] = fail("getOne should not be used!")
+ }
+
+ val adapter = new BridgeUploadQueueRepositoryAdapter(
+ strategy = defaultStrategy,
+ repository = repository,
+ transactions = transactions
+ )
+
+ adapter.tryRetry(defaultItem).isReadyWithin(100.millis)
+ }
+
+ "should remove the item from the queue" in {
+ val repository = new BridgeUploadQueueRepository {
+ override def delete(id: IdT): Unit = {
+ assert(id == defaultItem.id, "repository.delete")
+ }
+ override def update(entity: EntityT): EntityT = fail("update should not be used!")
+ override def add(draft: EntityT): EntityT = fail("add should not be used!")
+ override def getById(id: LongId[EntityT]): Option[EntityT] = fail("getById should not be used!")
+ override def isCompleted(kind: String, tag: String): Future[Boolean] = fail("isCompleted should not be used!")
+ override def getOne(kind: String): Future[Option[Item]] = fail("getOne should not be used!")
+ }
+
+ val adapter = new BridgeUploadQueueRepositoryAdapter(
+ strategy = defaultStrategy,
+ repository = repository,
+ transactions = transactions
+ )
+
+ adapter.tryRetry(defaultItem).isReadyWithin(100.millis)
+ }
+
+ }
+
+ }
+
+ private def defaultItem = BridgeUploadQueue.Item(
+ "test",
+ ThreadLocalRandom.current().nextInt().toString
+ )
+
+}
diff --git a/src/test/scala/xyz/driver/common/db/QueryBuilderParametersSuite.scala b/src/test/scala/xyz/driver/common/db/QueryBuilderParametersSuite.scala
new file mode 100644
index 0000000..e49ccd9
--- /dev/null
+++ b/src/test/scala/xyz/driver/common/db/QueryBuilderParametersSuite.scala
@@ -0,0 +1,249 @@
+package xyz.driver.common.db
+
+import java.time.LocalDateTime
+
+import io.getquill.MysqlEscape
+import org.scalatest.FreeSpecLike
+import xyz.driver.common.db.QueryBuilder.TableData
+import xyz.driver.common.domain.{Email, LongId, User}
+
+class QueryBuilderParametersSuite extends FreeSpecLike {
+
+ import SearchFilterBinaryOperation._
+ import SearchFilterExpr.{Dimension => _, _}
+ import SearchFilterNAryOperation._
+ import Sorting._
+ import SortingOrder._
+
+ val tableName = "Entity"
+
+ case class Entity(id: LongId[Entity],
+ name: String,
+ email: Email,
+ optionUser: Option[LongId[User]],
+ date: LocalDateTime,
+ optionDate: Option[LocalDateTime],
+ kindId: Long)
+
+ def queryBuilderParameters = MysqlQueryBuilderParameters(
+ tableData = TableData(
+ tableName = tableName,
+ nullableFields = Set("optionUser", "optionDate")
+ ),
+ links = Map(
+ "Kind" -> TableLink("kindId", "Kind", "id"),
+ "User" -> TableLink("optionUser", "User", "id")
+ )
+ )
+
+ val queryBasis =
+ s"""select `$tableName`.*
+ |from `$tableName`""".stripMargin.trim
+
+ "toSql" - {
+ "should generate correct SQL query" - {
+ "with default parameters" in {
+ val (sql, _) = queryBuilderParameters.toSql(namingStrategy = MysqlEscape)
+ assert(sql == queryBasis)
+ }
+
+ "with filtering: " - {
+ "single atom filter" in {
+ val (sql, _) = queryBuilderParameters.copy(filter = Atom.Binary("name", Eq, "x")).toSql(namingStrategy = MysqlEscape)
+ assert(sql ==
+ s"""$queryBasis
+ |where `$tableName`.`name` = ?""".stripMargin)
+ }
+
+ "single atom filter for optional field with NotEq operation" in {
+ val (sql, _) = queryBuilderParameters.copy(filter = Atom.Binary("optionUser", NotEq, "x")).toSql(namingStrategy = MysqlEscape)
+ assert(sql ==
+ s"""$queryBasis
+ |where (`$tableName`.`optionUser` is null or `$tableName`.`optionUser` != ?)""".stripMargin)
+ }
+
+ "single atom filter for field with IN operation" in {
+ val (sql, _) = queryBuilderParameters.copy(filter = Atom.NAry("date", In, Seq("x", "x", "x"))).toSql(namingStrategy = MysqlEscape)
+ assert(sql ==
+ s"""$queryBasis
+ |where `$tableName`.`date` in (?, ?, ?)""".stripMargin)
+ }
+
+ "multiple intersected filters" in {
+ val (sql, _) = queryBuilderParameters.copy(filter = Intersection(Seq(
+ Atom.Binary("name", Gt, "x"),
+ Atom.Binary("optionDate", GtEq, "x")
+ ))).toSql(namingStrategy = MysqlEscape)
+ assert(sql ==
+ s"""$queryBasis
+ |where (`$tableName`.`name` > ? and `$tableName`.`optionDate` >= ?)""".stripMargin)
+ }
+
+ "multiple intersected nested filters" in {
+ val (sql, _) = queryBuilderParameters.copy(filter = Intersection(Seq(
+ Atom.Binary("name", Gt, "x"),
+ Atom.Binary("optionDate", GtEq, "x"),
+ Intersection(Seq(
+ Atom.Binary("optionUser", Eq, "x"),
+ Atom.Binary("date", LtEq, "x")
+ ))
+ ))).toSql(namingStrategy = MysqlEscape)
+ assert(sql ==
+ s"$queryBasis\nwhere (`$tableName`.`name` > ? and `$tableName`.`optionDate` >= ?" +
+ s" and (`$tableName`.`optionUser` = ? and `$tableName`.`date` <= ?))")
+ }
+
+ "multiple unionized filters" in {
+ val (sql, _) = queryBuilderParameters.copy(filter = Union(Seq(
+ Atom.Binary("name", Gt, "x"),
+ Atom.Binary("optionDate", GtEq, "x")
+ ))).toSql(namingStrategy = MysqlEscape)
+ assert(sql ==
+ s"""$queryBasis
+ |where (`$tableName`.`name` > ? or `$tableName`.`optionDate` >= ?)""".stripMargin.trim)
+ }
+
+ "multiple unionized nested filters" in {
+ val (sql, _) = queryBuilderParameters.copy(filter = Union(Seq(
+ Atom.Binary("name", Gt, "x"),
+ Atom.Binary("optionDate", GtEq, "x"),
+ Union(Seq(
+ Atom.Binary("optionUser", Eq, "x"),
+ Atom.Binary("date", LtEq, "x")
+ ))
+ ))).toSql(namingStrategy = MysqlEscape)
+ assert(sql ==
+ s"""$queryBasis
+ |where (`$tableName`.`name` > ? or `$tableName`.`optionDate` >= ? or (`$tableName`.`optionUser` = ? or `$tableName`.`date` <= ?))""".stripMargin)
+ }
+
+ "multiple unionized and intersected nested filters" in {
+ val (sql, _) = queryBuilderParameters.copy(filter = Union(Seq(
+ Intersection(Seq(
+ Atom.Binary("name", Gt, "x"),
+ Atom.Binary("optionDate", GtEq, "x")
+ )),
+ Intersection(Seq(
+ Atom.Binary("optionUser", Eq, "x"),
+ Atom.Binary("date", LtEq, "x")
+ ))
+ ))).toSql(namingStrategy = MysqlEscape)
+
+ assert(sql ==
+ s"$queryBasis\nwhere ((`$tableName`.`name` > ? and `$tableName`.`optionDate` >= ?) " +
+ s"or (`$tableName`.`optionUser` = ? and `$tableName`.`date` <= ?))")
+ }
+
+ "single field from foreign table" in {
+ val (sql, _) = queryBuilderParameters
+ .copy(filter = Atom.Binary(SearchFilterExpr.Dimension(Some("Kind"), "name"), Eq, "x"))
+ .toSql(namingStrategy = MysqlEscape)
+ val pattern =
+ s"""select `$tableName`.*
+ |from `$tableName`
+ |inner join `Kind` on `Entity`.`kindId` = `Kind`.`id`
+ |where `Kind`.`name` = ?""".stripMargin
+ assert(sql == pattern)
+ }
+ }
+
+ "with sorting:" - {
+ "single field sorting" in {
+ val (sql, _) = queryBuilderParameters.copy(sorting = Dimension(None, "name", Ascending)).toSql(namingStrategy = MysqlEscape)
+
+ assert(sql ==
+ s"""$queryBasis
+ |order by `$tableName`.`name` asc""".stripMargin)
+ }
+
+ "single foreign sorting field" in {
+ val (sql, _) = queryBuilderParameters.copy(sorting = Dimension(Some("Kind"), "name", Ascending)).toSql(namingStrategy = MysqlEscape)
+
+ assert(sql ==
+ s"""select `$tableName`.*
+ |from `$tableName`
+ |inner join `Kind` on `Entity`.`kindId` = `Kind`.`id`
+ |order by `Kind`.`name` asc""".stripMargin)
+ }
+
+ "multiple fields sorting" in {
+ val (sql, _) = queryBuilderParameters.copy(sorting = Sequential(Seq(
+ Dimension(None, "name", Ascending),
+ Dimension(None, "date", Descending)
+ ))).toSql(namingStrategy = MysqlEscape)
+ assert(sql ==
+ s"""$queryBasis
+ |order by `$tableName`.`name` asc, `$tableName`.`date` desc""".stripMargin)
+ }
+
+ "multiple foreign sorting field" in {
+ val (sql, _) = queryBuilderParameters.copy(sorting = Sequential(Seq(
+ Dimension(Some("Kind"), "name", Ascending),
+ Dimension(Some("User"), "name", Descending)
+ ))).toSql(namingStrategy = MysqlEscape)
+
+ assert(sql ==
+ s"""select `$tableName`.*
+ |from `$tableName`
+ |inner join `Kind` on `$tableName`.`kindId` = `Kind`.`id`
+ |inner join `User` on `$tableName`.`optionUser` = `User`.`id`
+ |order by `Kind`.`name` asc, `User`.`name` desc""".stripMargin)
+ }
+
+ "multiple field sorting (including foreign tables)" in {
+ val (sql, _) = queryBuilderParameters.copy(sorting = Sequential(Seq(
+ Dimension(Some("Kind"), "name", Ascending),
+ Dimension(None, "date", Descending)
+ ))).toSql(namingStrategy = MysqlEscape)
+
+ assert(sql ==
+ s"""select `$tableName`.*
+ |from `$tableName`
+ |inner join `Kind` on `$tableName`.`kindId` = `Kind`.`id`
+ |order by `Kind`.`name` asc, `$tableName`.`date` desc""".stripMargin)
+ }
+ }
+
+ "with pagination" in {
+ val (sql, _) = queryBuilderParameters.copy(pagination = Some(Pagination(5, 3))).toSql(namingStrategy = MysqlEscape)
+ assert(sql ==
+ s"""$queryBasis
+ |limit 10, 5""".stripMargin)
+ }
+
+ "combined" in {
+ val filter = Union(Seq(
+ Intersection(Seq(
+ Atom.Binary("name", Gt, "x"),
+ Atom.Binary("optionDate", GtEq, "x")
+ )),
+ Intersection(Seq(
+ Atom.Binary("optionUser", Eq, "x"),
+ Atom.Binary("date", LtEq, "x")
+ ))
+ ))
+ val sorting = Sequential(Seq(
+ Dimension(Some("Kind"), "name", Ascending),
+ Dimension(None, "name", Ascending),
+ Dimension(None, "date", Descending)
+ ))
+
+ val (sql, _) = queryBuilderParameters.copy(
+ filter = filter,
+ sorting = sorting,
+ pagination = Some(Pagination(5, 3))
+ ).toSql(namingStrategy = MysqlEscape)
+
+ assert(sql ==
+ s"""select `$tableName`.*
+ |from `$tableName`
+ |inner join `Kind` on `$tableName`.`kindId` = `Kind`.`id`
+ |where ((`$tableName`.`name` > ? and `$tableName`.`optionDate` >= ?) or (`$tableName`.`optionUser` = ? and `$tableName`.`date` <= ?))
+ |order by `Kind`.`name` asc, `$tableName`.`name` asc, `$tableName`.`date` desc
+ |limit 10, 5""".stripMargin)
+ }
+
+ }
+ }
+
+}
diff --git a/src/test/scala/xyz/driver/common/db/SearchFilterExprSuite.scala b/src/test/scala/xyz/driver/common/db/SearchFilterExprSuite.scala
new file mode 100644
index 0000000..3073b61
--- /dev/null
+++ b/src/test/scala/xyz/driver/common/db/SearchFilterExprSuite.scala
@@ -0,0 +1,32 @@
+package xyz.driver.common.db
+
+import org.scalatest.{FreeSpecLike, MustMatchers}
+
+class SearchFilterExprSuite extends FreeSpecLike with MustMatchers {
+
+ "replace" - {
+ "all entities are changed" in {
+ val ast = SearchFilterExpr.Union(Seq(
+ SearchFilterExpr.Intersection(Seq(
+ SearchFilterExpr.Atom.Binary("foo", SearchFilterBinaryOperation.Gt, "10"),
+ SearchFilterExpr.Atom.Binary("foo", SearchFilterBinaryOperation.Lt, "20")
+ )),
+ SearchFilterExpr.Atom.NAry("bar", SearchFilterNAryOperation.In, Seq("x", "y", "z")),
+ SearchFilterExpr.Atom.Binary("foo", SearchFilterBinaryOperation.Eq, "40")
+ ))
+
+ val newAst = ast.replace {
+ case x: SearchFilterExpr.Atom.Binary if x.dimension.name == "foo" =>
+ x.copy(dimension = x.dimension.copy(name = "bar"))
+ }
+
+ val result = newAst.find {
+ case x: SearchFilterExpr.Atom.Binary => x.dimension.name == "foo"
+ case _ => false
+ }
+
+ result mustBe empty
+ }
+ }
+
+}
diff --git a/src/test/scala/xyz/driver/common/error/UnexpectedFilterException.scala b/src/test/scala/xyz/driver/common/error/UnexpectedFilterException.scala
new file mode 100644
index 0000000..0562b8e
--- /dev/null
+++ b/src/test/scala/xyz/driver/common/error/UnexpectedFilterException.scala
@@ -0,0 +1,3 @@
+package xyz.driver.common.error
+
+class UnexpectedFilterException(message: String) extends RuntimeException(message)
diff --git a/src/test/scala/xyz/driver/common/logging/PhiStringContextSuite.scala b/src/test/scala/xyz/driver/common/logging/PhiStringContextSuite.scala
new file mode 100644
index 0000000..de60cc9
--- /dev/null
+++ b/src/test/scala/xyz/driver/common/logging/PhiStringContextSuite.scala
@@ -0,0 +1,32 @@
+package xyz.driver.common.logging
+
+import org.scalatest.FreeSpecLike
+
+class PhiStringContextSuite extends FreeSpecLike {
+
+ class Foo(x: Int, y: String) {
+ val z: Boolean = true
+ }
+
+ case class Bar(y: Boolean)
+
+ implicit def fooToPhiString(foo: Foo): PhiString = new PhiString(s"Foo(z=${foo.z})")
+
+ "should not compile if there is no PhiString implicit" in assertDoesNotCompile(
+ """val bar = Bar(true)
+ |phi"bar is $bar"""".stripMargin
+ )
+
+ "should compile if there is a PhiString implicit" in assertCompiles(
+ """val foo = new Foo(1, "test")
+ |println(phi"foo is $foo}")""".stripMargin
+ )
+
+ "should not contain private info" in {
+ val foo = new Foo(42, "test")
+ val result = phi"foo is $foo".text
+ assert(!result.contains("test"))
+ assert(!result.contains("42"))
+ }
+
+}
diff --git a/src/test/scala/xyz/driver/common/pdf/MockPdfRenderer.scala b/src/test/scala/xyz/driver/common/pdf/MockPdfRenderer.scala
new file mode 100644
index 0000000..c22817f
--- /dev/null
+++ b/src/test/scala/xyz/driver/common/pdf/MockPdfRenderer.scala
@@ -0,0 +1,25 @@
+package xyz.driver.common.pdf
+
+import java.nio.file.{Path, Paths}
+
+import xyz.driver.common.logging._
+
+object MockPdfRenderer extends PdfRenderer with PhiLogging {
+
+ private lazy val defaultDocument: Path = {
+ val uri = getClass.getResource("/pdf/example.pdf").toURI
+ Paths.get(uri)
+ }
+
+ override def render(html: String, documentName: String, force: Boolean = false): Path = {
+ logger.trace(phi"render(html, documentName=${Unsafe(documentName)})")
+ defaultDocument
+ }
+
+ override def delete(documentName: String): Unit = {
+ logger.trace(phi"delete(${Unsafe(documentName)})")
+ }
+
+ override def getPath(documentName: String): Path = defaultDocument
+
+}
diff --git a/src/test/scala/xyz/driver/common/utils/DiffUtils.scala b/src/test/scala/xyz/driver/common/utils/DiffUtils.scala
new file mode 100644
index 0000000..06199bb
--- /dev/null
+++ b/src/test/scala/xyz/driver/common/utils/DiffUtils.scala
@@ -0,0 +1,52 @@
+package xyz.driver.common.utils
+
+import java.net.URI
+import java.time.{LocalDate, LocalDateTime}
+
+import ai.x.diff._
+import org.scalatest.Assertions
+import xyz.driver.common.domain.PasswordHash
+
+import scala.io.AnsiColor
+
+trait DiffUtils {
+
+ this: Assertions =>
+
+ def assertIdentical[T: DiffShow](left: T, right: T): Unit = {
+ val diff = DiffShow.diff(left, right)
+ assert(diff.isIdentical, s"\n${AnsiColor.RESET}$diff") // reset red color
+ }
+
+ implicit def localTimeDiffShow: DiffShow[LocalDateTime] = new DiffShow[LocalDateTime] {
+ def show(x: LocalDateTime): String = s"LocalTime($x)"
+ def diff(left: LocalDateTime, right: LocalDateTime): Comparison = {
+ if (left.isEqual(right)) Identical(show(left))
+ else Different(showChange(left, right))
+ }
+ }
+
+ implicit def localDateDiffShow: DiffShow[LocalDate] = new DiffShow[LocalDate] {
+ def show(x: LocalDate): String = s"LocalDate($x)"
+ def diff(left: LocalDate, right: LocalDate): Comparison = {
+ if (left.isEqual(right)) Identical(show(left))
+ else Different(showChange(left, right))
+ }
+ }
+
+ implicit def urlDiffShow: DiffShow[URI] = new DiffShow[URI] {
+ def show(x: URI): String = s"URI($x)"
+ def diff(left: URI, right: URI): Comparison = {
+ if (left.equals(right)) Identical(show(left))
+ else Different(showChange(left, right))
+ }
+ }
+
+ implicit def passwordHashDiffShow: DiffShow[PasswordHash] = new DiffShow[PasswordHash] {
+ def show(x: PasswordHash): String = s"PasswordHash($x)"
+ def diff(left: PasswordHash, right: PasswordHash): Comparison = {
+ if (left.equals(right)) Identical(show(left))
+ else Different(showChange(left, right))
+ }
+ }
+}