From cd1b635b2ae90d9ac2d8b1779183a1fbd8c5fd5c Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 13 Jun 2017 16:12:20 -0700 Subject: Adding domain entities --- src/test/scala/xyz/driver/common/BaseSuite.scala | 51 ----- src/test/scala/xyz/driver/common/Mocks.scala | 89 -------- .../BridgeUploadQueueRepositoryAdapterSuite.scala | 221 ------------------ .../common/db/QueryBuilderParametersSuite.scala | 249 --------------------- .../driver/common/db/SearchFilterExprSuite.scala | 32 --- .../common/error/UnexpectedFilterException.scala | 3 - .../common/logging/PhiStringContextSuite.scala | 32 --- .../xyz/driver/common/pdf/MockPdfRenderer.scala | 25 --- .../scala/xyz/driver/common/utils/DiffUtils.scala | 52 ----- .../scala/xyz/driver/pdsuicommon/BaseSuite.scala | 51 +++++ src/test/scala/xyz/driver/pdsuicommon/Mocks.scala | 89 ++++++++ .../BridgeUploadQueueRepositoryAdapterSuite.scala | 221 ++++++++++++++++++ .../db/QueryBuilderParametersSuite.scala | 249 +++++++++++++++++++++ .../pdsuicommon/db/SearchFilterExprSuite.scala | 32 +++ .../error/UnexpectedFilterException.scala | 3 + .../logging/PhiStringContextSuite.scala | 32 +++ .../driver/pdsuicommon/pdf/MockPdfRenderer.scala | 25 +++ .../xyz/driver/pdsuicommon/utils/DiffUtils.scala | 52 +++++ .../xyz/driver/pdsuidomain/DocumentSuite.scala | 55 +++++ 19 files changed, 809 insertions(+), 754 deletions(-) delete mode 100644 src/test/scala/xyz/driver/common/BaseSuite.scala delete mode 100644 src/test/scala/xyz/driver/common/Mocks.scala delete mode 100644 src/test/scala/xyz/driver/common/concurrent/BridgeUploadQueueRepositoryAdapterSuite.scala delete mode 100644 src/test/scala/xyz/driver/common/db/QueryBuilderParametersSuite.scala delete mode 100644 src/test/scala/xyz/driver/common/db/SearchFilterExprSuite.scala delete mode 100644 src/test/scala/xyz/driver/common/error/UnexpectedFilterException.scala delete mode 100644 src/test/scala/xyz/driver/common/logging/PhiStringContextSuite.scala delete mode 100644 src/test/scala/xyz/driver/common/pdf/MockPdfRenderer.scala delete mode 100644 src/test/scala/xyz/driver/common/utils/DiffUtils.scala create mode 100644 src/test/scala/xyz/driver/pdsuicommon/BaseSuite.scala create mode 100644 src/test/scala/xyz/driver/pdsuicommon/Mocks.scala create mode 100644 src/test/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueueRepositoryAdapterSuite.scala create mode 100644 src/test/scala/xyz/driver/pdsuicommon/db/QueryBuilderParametersSuite.scala create mode 100644 src/test/scala/xyz/driver/pdsuicommon/db/SearchFilterExprSuite.scala create mode 100644 src/test/scala/xyz/driver/pdsuicommon/error/UnexpectedFilterException.scala create mode 100644 src/test/scala/xyz/driver/pdsuicommon/logging/PhiStringContextSuite.scala create mode 100644 src/test/scala/xyz/driver/pdsuicommon/pdf/MockPdfRenderer.scala create mode 100644 src/test/scala/xyz/driver/pdsuicommon/utils/DiffUtils.scala create mode 100644 src/test/scala/xyz/driver/pdsuidomain/DocumentSuite.scala (limited to 'src/test/scala/xyz') diff --git a/src/test/scala/xyz/driver/common/BaseSuite.scala b/src/test/scala/xyz/driver/common/BaseSuite.scala deleted file mode 100644 index b4c3d03..0000000 --- a/src/test/scala/xyz/driver/common/BaseSuite.scala +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index 480ab48..0000000 --- a/src/test/scala/xyz/driver/common/Mocks.scala +++ /dev/null @@ -1,89 +0,0 @@ -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 deleted file mode 100644 index e81d0b3..0000000 --- a/src/test/scala/xyz/driver/common/concurrent/BridgeUploadQueueRepositoryAdapterSuite.scala +++ /dev/null @@ -1,221 +0,0 @@ -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 deleted file mode 100644 index e49ccd9..0000000 --- a/src/test/scala/xyz/driver/common/db/QueryBuilderParametersSuite.scala +++ /dev/null @@ -1,249 +0,0 @@ -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 deleted file mode 100644 index 3073b61..0000000 --- a/src/test/scala/xyz/driver/common/db/SearchFilterExprSuite.scala +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 0562b8e..0000000 --- a/src/test/scala/xyz/driver/common/error/UnexpectedFilterException.scala +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index de60cc9..0000000 --- a/src/test/scala/xyz/driver/common/logging/PhiStringContextSuite.scala +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index c22817f..0000000 --- a/src/test/scala/xyz/driver/common/pdf/MockPdfRenderer.scala +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 06199bb..0000000 --- a/src/test/scala/xyz/driver/common/utils/DiffUtils.scala +++ /dev/null @@ -1,52 +0,0 @@ -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)) - } - } -} diff --git a/src/test/scala/xyz/driver/pdsuicommon/BaseSuite.scala b/src/test/scala/xyz/driver/pdsuicommon/BaseSuite.scala new file mode 100644 index 0000000..78a9168 --- /dev/null +++ b/src/test/scala/xyz/driver/pdsuicommon/BaseSuite.scala @@ -0,0 +1,51 @@ +package xyz.driver.pdsuicommon + +import java.time.{LocalDateTime, ZoneId} + +import org.scalatest.FreeSpecLike +import org.scalatest.concurrent.ScalaFutures +import org.scalatest.time.{Millis, Span} +import xyz.driver.pdsuicommon.db.{MysqlQueryBuilder, SearchFilterExpr, SqlContext, Transactions} +import xyz.driver.pdsuicommon.domain.{Email, LongId, PasswordHash, User} +import xyz.driver.pdsuicommon.error.UnexpectedFilterException +import xyz.driver.pdsuicommon.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/pdsuicommon/Mocks.scala b/src/test/scala/xyz/driver/pdsuicommon/Mocks.scala new file mode 100644 index 0000000..2154e3d --- /dev/null +++ b/src/test/scala/xyz/driver/pdsuicommon/Mocks.scala @@ -0,0 +1,89 @@ +package xyz.driver.pdsuicommon + +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.pdsuicommon.db._ +import xyz.driver.pdsuicommon.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/pdsuicommon/concurrent/BridgeUploadQueueRepositoryAdapterSuite.scala b/src/test/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueueRepositoryAdapterSuite.scala new file mode 100644 index 0000000..c0bb1a2 --- /dev/null +++ b/src/test/scala/xyz/driver/pdsuicommon/concurrent/BridgeUploadQueueRepositoryAdapterSuite.scala @@ -0,0 +1,221 @@ +package xyz.driver.pdsuicommon.concurrent + +import java.util.concurrent.ThreadLocalRandom + +import xyz.driver.pdsuicommon.BaseSuite +import xyz.driver.pdsuicommon.concurrent.BridgeUploadQueue.Item +import xyz.driver.pdsuicommon.concurrent.BridgeUploadQueueRepositoryAdapter.Strategy +import xyz.driver.pdsuicommon.concurrent.BridgeUploadQueueRepositoryAdapter.Strategy.{OnAttempt, OnComplete} +import xyz.driver.pdsuicommon.db.repositories.BridgeUploadQueueRepository +import xyz.driver.pdsuicommon.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/pdsuicommon/db/QueryBuilderParametersSuite.scala b/src/test/scala/xyz/driver/pdsuicommon/db/QueryBuilderParametersSuite.scala new file mode 100644 index 0000000..0748e8a --- /dev/null +++ b/src/test/scala/xyz/driver/pdsuicommon/db/QueryBuilderParametersSuite.scala @@ -0,0 +1,249 @@ +package xyz.driver.pdsuicommon.db + +import java.time.LocalDateTime + +import io.getquill.MysqlEscape +import org.scalatest.FreeSpecLike +import xyz.driver.pdsuicommon.db.QueryBuilder.TableData +import xyz.driver.pdsuicommon.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/pdsuicommon/db/SearchFilterExprSuite.scala b/src/test/scala/xyz/driver/pdsuicommon/db/SearchFilterExprSuite.scala new file mode 100644 index 0000000..ee467af --- /dev/null +++ b/src/test/scala/xyz/driver/pdsuicommon/db/SearchFilterExprSuite.scala @@ -0,0 +1,32 @@ +package xyz.driver.pdsuicommon.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/pdsuicommon/error/UnexpectedFilterException.scala b/src/test/scala/xyz/driver/pdsuicommon/error/UnexpectedFilterException.scala new file mode 100644 index 0000000..567661a --- /dev/null +++ b/src/test/scala/xyz/driver/pdsuicommon/error/UnexpectedFilterException.scala @@ -0,0 +1,3 @@ +package xyz.driver.pdsuicommon.error + +class UnexpectedFilterException(message: String) extends RuntimeException(message) diff --git a/src/test/scala/xyz/driver/pdsuicommon/logging/PhiStringContextSuite.scala b/src/test/scala/xyz/driver/pdsuicommon/logging/PhiStringContextSuite.scala new file mode 100644 index 0000000..f623b81 --- /dev/null +++ b/src/test/scala/xyz/driver/pdsuicommon/logging/PhiStringContextSuite.scala @@ -0,0 +1,32 @@ +package xyz.driver.pdsuicommon.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/pdsuicommon/pdf/MockPdfRenderer.scala b/src/test/scala/xyz/driver/pdsuicommon/pdf/MockPdfRenderer.scala new file mode 100644 index 0000000..8dfd29c --- /dev/null +++ b/src/test/scala/xyz/driver/pdsuicommon/pdf/MockPdfRenderer.scala @@ -0,0 +1,25 @@ +package xyz.driver.pdsuicommon.pdf + +import java.nio.file.{Path, Paths} + +import xyz.driver.pdsuicommon.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/pdsuicommon/utils/DiffUtils.scala b/src/test/scala/xyz/driver/pdsuicommon/utils/DiffUtils.scala new file mode 100644 index 0000000..d42bca6 --- /dev/null +++ b/src/test/scala/xyz/driver/pdsuicommon/utils/DiffUtils.scala @@ -0,0 +1,52 @@ +package xyz.driver.pdsuicommon.utils + +import java.net.URI +import java.time.{LocalDate, LocalDateTime} + +import ai.x.diff._ +import org.scalatest.Assertions +import xyz.driver.pdsuicommon.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)) + } + } +} diff --git a/src/test/scala/xyz/driver/pdsuidomain/DocumentSuite.scala b/src/test/scala/xyz/driver/pdsuidomain/DocumentSuite.scala new file mode 100644 index 0000000..7187400 --- /dev/null +++ b/src/test/scala/xyz/driver/pdsuidomain/DocumentSuite.scala @@ -0,0 +1,55 @@ +package xyz.driver.pdsuidomain + +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +import xyz.driver.pdsuicommon.BaseSuite +import xyz.driver.pdsuicommon.domain.{LongId, TextJson} +import xyz.driver.pdsuidomain.entities.Document + +class DocumentSuite extends BaseSuite { + + "validation" - { + "can't submit invalid data" - { + val base = sampleDocument + + val now = LocalDateTime.now() + val past1 = now.minus(2, ChronoUnit.DAYS) + val past2 = past1.plus(1, ChronoUnit.DAYS) + val future1 = now.plus(1, ChronoUnit.DAYS) + val future2 = future1.plus(1, ChronoUnit.DAYS) + + Seq( + "startDate should be non-empty" -> base.copy(startDate = None), + "startDate should be greater, than endDate" -> base.copy(startDate = Some(past2), endDate = Some(past1)), + "startDate and endDate should be in the past" -> base.copy(startDate = Some(future1), endDate = Some(future2)) + ).foreach { case (title, orig) => + s"$title" in { + val r = Document.validator(orig) + assert(r.isLeft, s"should fail, but: ${r.right}") + } + } + } + } + + private def sampleDocument = { + val lastUpdate = LocalDateTime.now() + + Document( + id = LongId(2002), + status = Document.Status.New, + previousStatus = None, + assignee = None, + previousAssignee = None, + recordId = LongId(2003), + physician = None, + typeId = Some(LongId(123)), + providerName = Some("etst"), + providerTypeId = Some(LongId(123)), + startDate = Some(lastUpdate.minusDays(2)), + endDate = None, + lastUpdate = lastUpdate, + meta = Some(TextJson(Document.Meta(None, 1.1, 2.2))) + ) + } +} -- cgit v1.2.3