From c8d62015ee390648255e1624cd3d14beba043932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Wed, 25 Nov 2020 20:09:17 +0000 Subject: [PATCH 1/6] repository interface definition and method implementation --- .../main/tv/codely/mooc/video/domain/VideoRepository.scala | 2 ++ .../infrastructure/repository/DoobieMySqlVideoRepository.scala | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/mooc/main/tv/codely/mooc/video/domain/VideoRepository.scala b/src/mooc/main/tv/codely/mooc/video/domain/VideoRepository.scala index d770462..efe05ca 100644 --- a/src/mooc/main/tv/codely/mooc/video/domain/VideoRepository.scala +++ b/src/mooc/main/tv/codely/mooc/video/domain/VideoRepository.scala @@ -6,4 +6,6 @@ trait VideoRepository { def all(): Future[Seq[Video]] def save(video: Video): Future[Unit] + + def shortest(): Future[Option[Video]] } diff --git a/src/mooc/main/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepository.scala b/src/mooc/main/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepository.scala index 0f2dfdd..c8ed56e 100644 --- a/src/mooc/main/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepository.scala +++ b/src/mooc/main/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepository.scala @@ -17,4 +17,7 @@ final class DoobieMySqlVideoRepository(db: DoobieDbConnection)(implicit executio .transact(db.transactor) .unsafeToFuture() .map(_ => ()) + + override def shortest(): Future[Optional[Video]] = + db.read(sql"SELECT video_id, title, duration_in_seconds, category, creator_id FROM videos ORDER BY duration_in_seconds ASC").query[Video].take(1).option } From a158492c21a4fd395dc345181d656ab7d2458b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Wed, 25 Nov 2020 20:09:56 +0000 Subject: [PATCH 2/6] added searcher functionality and test --- .../search/ShortestVideoSearcher.scala | 9 +++++++++ .../search/ShortestVideoSearcherShould.scala | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/mooc/main/tv/codely/mooc/video/application/search/ShortestVideoSearcher.scala create mode 100644 src/mooc/test/tv/codely/mooc/video/application/search/ShortestVideoSearcherShould.scala diff --git a/src/mooc/main/tv/codely/mooc/video/application/search/ShortestVideoSearcher.scala b/src/mooc/main/tv/codely/mooc/video/application/search/ShortestVideoSearcher.scala new file mode 100644 index 0000000..fe3b2a6 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/video/application/search/ShortestVideoSearcher.scala @@ -0,0 +1,9 @@ +package tv.codely.mooc.video.application.search + +import tv.codely.mooc.video.domain.{Video, VideoRepository} + +import scala.concurrent.Future + +final class ShortestVideoSearcher(repository: VideoRepository) { + def shortest(): Future[Option[Video]] = repository.shortest() +} diff --git a/src/mooc/test/tv/codely/mooc/video/application/search/ShortestVideoSearcherShould.scala b/src/mooc/test/tv/codely/mooc/video/application/search/ShortestVideoSearcherShould.scala new file mode 100644 index 0000000..6c1867b --- /dev/null +++ b/src/mooc/test/tv/codely/mooc/video/application/search/ShortestVideoSearcherShould.scala @@ -0,0 +1,19 @@ +package tv.codely.mooc.video.application.search + +import tv.codely.mooc.video.domain.VideoMother +import tv.codely.mooc.video.infrastructure.repository.VideoRepositoryMock +import tv.codely.shared.infrastructure.unit.UnitTestCase + +final class ShorterVideoSearcherShould extends UnitTestCase with VideoRepositoryMock { + private val searcher = new ShorterVideoSearcher(repository) + + "search the shortest existing video" in { + val fiveSecondsVideo = VideoMother.apply(duration: 5) + val threeSecondsVideo = VideoMother.apply(duration: 3) + val existingVideos = Seq(fiveSecondsVideo, threeSecondsVideo) + + repositoryShouldFind(existingVideos) + + searcher.shortest().futureValue shouldBe threeSecondsVideo + } +} From 23ece6888a7e3960b30becdb3305e5c1908910b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Fri, 27 Nov 2020 18:49:59 +0000 Subject: [PATCH 3/6] added repository functionality and tests --- .../repository/DoobieMySqlVideoRepository.scala | 4 ++-- .../DoobieMySqlVideoRepositoryShould.scala | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/mooc/main/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepository.scala b/src/mooc/main/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepository.scala index c8ed56e..0dff8c2 100644 --- a/src/mooc/main/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepository.scala +++ b/src/mooc/main/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepository.scala @@ -18,6 +18,6 @@ final class DoobieMySqlVideoRepository(db: DoobieDbConnection)(implicit executio .unsafeToFuture() .map(_ => ()) - override def shortest(): Future[Optional[Video]] = - db.read(sql"SELECT video_id, title, duration_in_seconds, category, creator_id FROM videos ORDER BY duration_in_seconds ASC").query[Video].take(1).option + override def shortest(): Future[Option[Video]] = + db.read(sql"SELECT video_id, title, duration_in_seconds, category, creator_id FROM videos ORDER BY duration_in_seconds ASC LIMIT 1".query[Video].option) } diff --git a/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepositoryShould.scala b/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepositoryShould.scala index f11e509..3c7e3be 100644 --- a/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepositoryShould.scala +++ b/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepositoryShould.scala @@ -1,7 +1,7 @@ package tv.codely.mooc.video.infrastructure.repository import tv.codely.mooc.video.VideoIntegrationTestCase -import tv.codely.mooc.video.domain.VideoMother +import tv.codely.mooc.video.domain.{ VideoMother, VideoDuration} import doobie.implicits._ import org.scalatest.BeforeAndAfterEach @@ -29,4 +29,18 @@ final class DoobieMySqlVideoRepositoryShould extends VideoIntegrationTestCase wi repository.all().futureValue shouldBe videos } + + "search shortest existing video" in { + val fiveSecondsVideo = VideoMother.random.copy(duration = VideoDuration(5)) + val threeSecondsVideo = VideoMother.random.copy(duration = VideoDuration(3)) + val existingVideos = Seq(fiveSecondsVideo, threeSecondsVideo) + + existingVideos.foreach(v => repository.save(v).futureValue) + + repository.shortest().futureValue shouldBe Option(threeSecondsVideo) + } + + "return an empty object when search shortest existing video and there are no videos" in { + repository.shortest().futureValue shouldBe None + } } From 22d5852771219574b3bb202ded5464bddda602d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Fri, 27 Nov 2020 18:51:41 +0000 Subject: [PATCH 4/6] added UC, controller dependency injection and tests --- .../video/VideoGetShortestController.scala | 12 ++++++++++++ .../VideoModuleDependencyContainer.scala | 2 ++ .../search/ShortestVideoSearcherShould.scala | 19 +++++++++++++------ .../repository/VideoRepositoryMock.scala | 10 ++++++++++ 4 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 app/main/tv/codely/mooc/api/controller/video/VideoGetShortestController.scala diff --git a/app/main/tv/codely/mooc/api/controller/video/VideoGetShortestController.scala b/app/main/tv/codely/mooc/api/controller/video/VideoGetShortestController.scala new file mode 100644 index 0000000..6845040 --- /dev/null +++ b/app/main/tv/codely/mooc/api/controller/video/VideoGetShortestController.scala @@ -0,0 +1,12 @@ +package tv.codely.mooc.api.controller.video + +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport +import akka.http.scaladsl.server.StandardRoute +import akka.http.scaladsl.server.Directives.complete +import spray.json.DefaultJsonProtocol +import tv.codely.mooc.video.application.search.ShortestVideoSearcher +import tv.codely.mooc.video.infrastructure.marshaller.VideoJsonFormatMarshaller._ + +final class VideoGetShortestController(searcher: ShortestVideoSearcher) extends SprayJsonSupport with DefaultJsonProtocol { + def get(): StandardRoute = complete(searcher.shortest()) +} diff --git a/src/mooc/main/tv/codely/mooc/video/infrastructure/dependency_injection/VideoModuleDependencyContainer.scala b/src/mooc/main/tv/codely/mooc/video/infrastructure/dependency_injection/VideoModuleDependencyContainer.scala index 26ba3d5..b11567a 100644 --- a/src/mooc/main/tv/codely/mooc/video/infrastructure/dependency_injection/VideoModuleDependencyContainer.scala +++ b/src/mooc/main/tv/codely/mooc/video/infrastructure/dependency_injection/VideoModuleDependencyContainer.scala @@ -3,6 +3,7 @@ package tv.codely.mooc.video.infrastructure.dependency_injection import tv.codely.shared.infrastructure.doobie.DoobieDbConnection import tv.codely.mooc.video.application.create.VideoCreator import tv.codely.mooc.video.application.search.VideosSearcher +import tv.codely.mooc.video.application.search.ShortestVideoSearcher import tv.codely.mooc.video.domain.VideoRepository import tv.codely.mooc.video.infrastructure.repository.DoobieMySqlVideoRepository import scala.concurrent.ExecutionContext @@ -17,4 +18,5 @@ final class VideoModuleDependencyContainer( val videosSearcher: VideosSearcher = new VideosSearcher(repository) val videoCreator: VideoCreator = new VideoCreator(repository, messagePublisher) + val shortestVideoSearcher: ShortestVideoSearcher = new ShortestVideoSearcher(repository) } diff --git a/src/mooc/test/tv/codely/mooc/video/application/search/ShortestVideoSearcherShould.scala b/src/mooc/test/tv/codely/mooc/video/application/search/ShortestVideoSearcherShould.scala index 6c1867b..73b7b8d 100644 --- a/src/mooc/test/tv/codely/mooc/video/application/search/ShortestVideoSearcherShould.scala +++ b/src/mooc/test/tv/codely/mooc/video/application/search/ShortestVideoSearcherShould.scala @@ -1,19 +1,26 @@ package tv.codely.mooc.video.application.search import tv.codely.mooc.video.domain.VideoMother +import tv.codely.mooc.video.domain.VideoDuration import tv.codely.mooc.video.infrastructure.repository.VideoRepositoryMock import tv.codely.shared.infrastructure.unit.UnitTestCase -final class ShorterVideoSearcherShould extends UnitTestCase with VideoRepositoryMock { - private val searcher = new ShorterVideoSearcher(repository) +final class ShortestVideoSearcherShould extends UnitTestCase with VideoRepositoryMock { + private val searcher = new ShortestVideoSearcher(repository) "search the shortest existing video" in { - val fiveSecondsVideo = VideoMother.apply(duration: 5) - val threeSecondsVideo = VideoMother.apply(duration: 3) + val fiveSecondsVideo = VideoMother.random.copy(duration = VideoDuration(5)) + val threeSecondsVideo = VideoMother.random.copy(duration = VideoDuration(3)) val existingVideos = Seq(fiveSecondsVideo, threeSecondsVideo) - repositoryShouldFind(existingVideos) + repositoryShouldFindShortest(existingVideos) - searcher.shortest().futureValue shouldBe threeSecondsVideo + searcher.shortest().futureValue shouldBe Option(threeSecondsVideo) + } + + "return None when search the shortest existing video and there is no video" in { + repositoryShouldFindShortestNone() + + searcher.shortest().futureValue shouldBe None } } diff --git a/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/VideoRepositoryMock.scala b/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/VideoRepositoryMock.scala index 6433762..932c732 100644 --- a/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/VideoRepositoryMock.scala +++ b/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/VideoRepositoryMock.scala @@ -20,4 +20,14 @@ protected[video] trait VideoRepositoryMock extends MockFactory { (repository.all _) .expects() .returning(Future.successful(videos)) + + protected def repositoryShouldFindShortest(videos: Seq[Video]): Unit = + (repository.shortest _) + .expects() + .returning(Future.successful(Option(videos(1)))) + + protected def repositoryShouldFindShortestNone(): Unit = + (repository.shortest _) + .expects() + .returning(Future.successful(None)) } From 5e76a3a5b706deeb021371b7d2a48050c4d2f6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Fri, 27 Nov 2020 18:52:38 +0000 Subject: [PATCH 5/6] added entry point and routest --- .../codely/mooc/api/EntryPointDependencyContainer.scala | 4 +++- app/main/tv/codely/mooc/api/Routes.scala | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/main/tv/codely/mooc/api/EntryPointDependencyContainer.scala b/app/main/tv/codely/mooc/api/EntryPointDependencyContainer.scala index c05109f..7cc58a6 100644 --- a/app/main/tv/codely/mooc/api/EntryPointDependencyContainer.scala +++ b/app/main/tv/codely/mooc/api/EntryPointDependencyContainer.scala @@ -2,7 +2,7 @@ package tv.codely.mooc.api import tv.codely.mooc.api.controller.status.StatusGetController import tv.codely.mooc.api.controller.user.{UserGetController, UserPostController} -import tv.codely.mooc.api.controller.video.{VideoGetController, VideoPostController} +import tv.codely.mooc.api.controller.video.{VideoGetController, VideoPostController, VideoGetShortestController} import tv.codely.mooc.user.infrastructure.dependency_injection.UserModuleDependencyContainer import tv.codely.mooc.video.infrastructure.dependency_injection.VideoModuleDependencyContainer @@ -17,4 +17,6 @@ final class EntryPointDependencyContainer( val videoGetController = new VideoGetController(videoDependencies.videosSearcher) val videoPostController = new VideoPostController(videoDependencies.videoCreator) + + val videoGetShortestController = new VideoGetShortestController(videoDependencies.shortestVideoSearcher) } diff --git a/app/main/tv/codely/mooc/api/Routes.scala b/app/main/tv/codely/mooc/api/Routes.scala index e6035e1..95f4be3 100644 --- a/app/main/tv/codely/mooc/api/Routes.scala +++ b/app/main/tv/codely/mooc/api/Routes.scala @@ -28,7 +28,13 @@ final class Routes(container: EntryPointDependencyContainer) { } private val video = get { - path("videos")(container.videoGetController.get()) + path("videos" / "shortest") { + (container.videoGetShortestController.get()) + } ~ + path("videos") { + (container.videoGetController.get()) + } + } ~ post { path("videos") { @@ -43,6 +49,7 @@ final class Routes(container: EntryPointDependencyContainer) { } } } + val all: Route = status ~ user ~ video From 9f0008f9bac5290a94026a666f8107ea1cdb4c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Fri, 27 Nov 2020 18:53:20 +0000 Subject: [PATCH 6/6] added entry point tests --- .../mooc/api/VideoEntryPointShould.scala | 15 ++++++++++++++ .../marshaller/VideoJsValueMarshaller.scala | 20 ++++++++----------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/app/test/tv/codely/mooc/api/VideoEntryPointShould.scala b/app/test/tv/codely/mooc/api/VideoEntryPointShould.scala index 9348f6a..4971eb3 100644 --- a/app/test/tv/codely/mooc/api/VideoEntryPointShould.scala +++ b/app/test/tv/codely/mooc/api/VideoEntryPointShould.scala @@ -5,6 +5,7 @@ import doobie.implicits._ import org.scalatest.BeforeAndAfterEach import spray.json._ import tv.codely.mooc.video.domain.VideoMother +import tv.codely.mooc.video.domain.VideoDuration import tv.codely.mooc.video.infrastructure.marshaller.VideoJsValueMarshaller import tv.codely.HttpSpec @@ -47,4 +48,18 @@ final class VideoEntryPointShould extends HttpSpec with BeforeAndAfterEach { entityAs[String].parseJson shouldBe VideoJsValueMarshaller.marshall(videos) } } + + "return the shortest video" in { + val fiveSecondsVideo = VideoMother.random.copy(duration = VideoDuration(5)) + val threeSecondsVideo = VideoMother.random.copy(duration = VideoDuration(3)) + val existingVideos = Seq(fiveSecondsVideo, threeSecondsVideo) + + existingVideos.foreach(v => videoDependencies.repository.save(v).futureValue) + + getting("/videos/shortest") { + status shouldBe StatusCodes.OK + contentType shouldBe ContentTypes.`application/json` + entityAs[String].parseJson shouldBe VideoJsValueMarshaller.marshall(threeSecondsVideo) + } + } } diff --git a/src/mooc/test/tv/codely/mooc/video/infrastructure/marshaller/VideoJsValueMarshaller.scala b/src/mooc/test/tv/codely/mooc/video/infrastructure/marshaller/VideoJsValueMarshaller.scala index c8ca6de..977bc76 100644 --- a/src/mooc/test/tv/codely/mooc/video/infrastructure/marshaller/VideoJsValueMarshaller.scala +++ b/src/mooc/test/tv/codely/mooc/video/infrastructure/marshaller/VideoJsValueMarshaller.scala @@ -4,18 +4,14 @@ import spray.json.{JsArray, JsNumber, JsObject, JsString} import tv.codely.mooc.video.domain.Video object VideoJsValueMarshaller { + def marshall(v: Video): JsObject = JsObject( + "id" -> JsString(v.id.value.toString), + "title" -> JsString(v.title.value), + "duration_in_seconds" -> JsNumber(v.duration.value.toSeconds), + "category" -> JsString(v.category.toString), + "creator_id" -> JsString(v.creatorId.value.toString) + ) def marshall(videos: Seq[Video]): JsArray = JsArray( - videos - .map( - v => - JsObject( - "id" -> JsString(v.id.value.toString), - "title" -> JsString(v.title.value), - "duration_in_seconds" -> JsNumber(v.duration.value.toSeconds), - "category" -> JsString(v.category.toString), - "creator_id" -> JsString(v.creatorId.value.toString) - ) - ) - .toVector + videos.map(v => marshall(v)).toVector ) }