diff --git a/src/main/scala/com/zeta/playthegame/AppointmentsRoutes.scala b/src/main/scala/com/zeta/playthegame/AppointmentsRoutes.scala index 4fba52b..9fbfde7 100644 --- a/src/main/scala/com/zeta/playthegame/AppointmentsRoutes.scala +++ b/src/main/scala/com/zeta/playthegame/AppointmentsRoutes.scala @@ -2,14 +2,14 @@ package com.zeta.playthegame import cats.effect.IO -import com.zeta.playthegame.repository.AppointmentRepository +import com.zeta.playthegame.repository.AppointmentsRepository import io.circe.generic.auto._ import org.http4s.HttpRoutes import org.http4s.circe.CirceEntityDecoder._ import org.http4s.circe.CirceEntityEncoder._ import org.http4s.dsl.Http4sDsl -class AppointmentsRoutes(gameAppointmentRepository: AppointmentRepository) { +class AppointmentsRoutes(gameAppointmentRepository: AppointmentsRepository) { def routes: HttpRoutes[IO] = { object dsl extends Http4sDsl[IO] diff --git a/src/main/scala/com/zeta/playthegame/Server.scala b/src/main/scala/com/zeta/playthegame/Server.scala index a108c18..131da84 100644 --- a/src/main/scala/com/zeta/playthegame/Server.scala +++ b/src/main/scala/com/zeta/playthegame/Server.scala @@ -5,7 +5,7 @@ import java.util.concurrent.Executors import cats.effect.{ContextShift, IO, Timer} import com.typesafe.config.ConfigFactory -import com.zeta.playthegame.repository.{AppointmentRepository, MongoConnection} +import com.zeta.playthegame.repository.{AppointmentsRepository, MongoConnection} import org.http4s.implicits._ import org.http4s.server.blaze.BlazeServerBuilder @@ -28,7 +28,7 @@ object Server { implicit lazy val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(poolSize)) //Exclusive ExecutionPool for Database access only - private lazy val repository = new AppointmentRepository(MongoConnection) + private lazy val repository = new AppointmentsRepository(MongoConnection) private lazy val httpApp = new AppointmentsRoutes(repository).routes.orNotFound diff --git a/src/main/scala/com/zeta/playthegame/model/Game.scala b/src/main/scala/com/zeta/playthegame/model/Game.scala index 8c01181..bbee71e 100644 --- a/src/main/scala/com/zeta/playthegame/model/Game.scala +++ b/src/main/scala/com/zeta/playthegame/model/Game.scala @@ -50,17 +50,19 @@ object Sport { } -case class GameAppointment(appointmentId: String, authorId: String, appointmentDate: Long, createdDate: Long, game: Game) { +case class Appointment(appointmentId: String, authorId: String, appointmentDate: Long, createdDate: Long, game: Game) { - def addPlayer(user: String): GameAppointment = this.copy(game = game.addPlayer(user)) + def addPlayer(user: String): Appointment = this.copy(game = game.addPlayer(user)) def getPlayers: List[String] = this.game.players - def removePlayer(user: String): GameAppointment = this.copy(game = game.removePlayer(user)) + def removePlayer(user: String): Appointment = this.copy(game = game.removePlayer(user)) - def changeAuthor(user: String): GameAppointment = this.copy(authorId = user) + def changeAuthor(user: String): Appointment = this.copy(authorId = user) - def addResult(aResult: Result): GameAppointment = this.copy(game = game.copy(result = Some(aResult))) + def addResult(aResult: Result): Appointment = this.copy(game = game.copy(result = Some(aResult))) + + def changeAppointmentDate(newAppointmentDate: Long) = this.copy(appointmentDate = newAppointmentDate) def sport = game.sport diff --git a/src/main/scala/com/zeta/playthegame/repository/AppointmentRepository.scala b/src/main/scala/com/zeta/playthegame/repository/AppointmentsRepository.scala similarity index 91% rename from src/main/scala/com/zeta/playthegame/repository/AppointmentRepository.scala rename to src/main/scala/com/zeta/playthegame/repository/AppointmentsRepository.scala index 1d9d6d6..b718da7 100644 --- a/src/main/scala/com/zeta/playthegame/repository/AppointmentRepository.scala +++ b/src/main/scala/com/zeta/playthegame/repository/AppointmentsRepository.scala @@ -10,7 +10,7 @@ import org.mongodb.scala.bson.collection.immutable.Document import scala.concurrent.ExecutionContext -class AppointmentRepository(mongoConnection: MongoConnection)(implicit executionContext: ExecutionContext) extends LoggerPerClassAware { +class AppointmentsRepository(mongoConnection: MongoConnection)(implicit executionContext: ExecutionContext) extends LoggerPerClassAware { def getAppointmentById(id: String) = mongoConnection.appointmentsCollection map { _.find(Document("_id" -> new ObjectId(id))) diff --git a/src/main/scala/com/zeta/playthegame/repository/Entities.scala b/src/main/scala/com/zeta/playthegame/repository/Entities.scala index 2484f86..c6d3a66 100644 --- a/src/main/scala/com/zeta/playthegame/repository/Entities.scala +++ b/src/main/scala/com/zeta/playthegame/repository/Entities.scala @@ -6,7 +6,7 @@ import org.mongodb.scala.bson.ObjectId object Entities { case class AppointmentDocument(_id: ObjectId, authorId: String, appointmentDate: Long, createdDate: Long, game: GameDocument) { - def toModel = GameAppointment(_id.toString, authorId, appointmentDate, createdDate, game.toModel) + def toModel = Appointment(_id.toString, authorId, appointmentDate, createdDate, game.toModel) } case class GameDocument(sport: String, players: List[String], result: Option[Result]) { diff --git a/src/main/scala/com/zeta/playthegame/repository/MongoConnection.scala b/src/main/scala/com/zeta/playthegame/repository/MongoConnection.scala index 57e14a3..488f357 100644 --- a/src/main/scala/com/zeta/playthegame/repository/MongoConnection.scala +++ b/src/main/scala/com/zeta/playthegame/repository/MongoConnection.scala @@ -2,6 +2,7 @@ package com.zeta.playthegame.repository import cats.effect.IO import com.typesafe.config.ConfigFactory +import com.zeta.playthegame.repository.Codecs.codecRegistry import com.zeta.playthegame.repository.Entities.AppointmentDocument import org.mongodb.scala._ @@ -11,20 +12,22 @@ trait MongoConnection { private lazy val mongoConfig: com.typesafe.config.Config = ConfigFactory.defaultApplication().resolve().getConfig("mongodb") private lazy val mongoUri: String = mongoConfig.getString("uri") - private val mongoClient: IO[MongoClient] = IO { + val mongoClient: IO[MongoClient] = IO { MongoClient(mongoUri) } val databaseName: String - - val database: IO[MongoDatabase] = mongoClient.map(_.getDatabase(databaseName).withCodecRegistry(codecRegistry)) - + val appointmentsCollectionName: String + val database: IO[MongoDatabase] val appointmentsCollection: IO[MongoCollection[AppointmentDocument]] + } object MongoConnection extends MongoConnection { val databaseName = "play_the_game" val appointmentsCollectionName = "appointments" + + val database: IO[MongoDatabase] = mongoClient.map(_.getDatabase(databaseName).withCodecRegistry(codecRegistry)) val appointmentsCollection: IO[MongoCollection[AppointmentDocument]] = database.map(_.getCollection(appointmentsCollectionName)) } diff --git a/src/test/scala/com/zeta/playthegame/integration/AppointmentsIT.scala b/src/test/scala/com/zeta/playthegame/integration/AppointmentsIT.scala new file mode 100644 index 0000000..3ed92ec --- /dev/null +++ b/src/test/scala/com/zeta/playthegame/integration/AppointmentsIT.scala @@ -0,0 +1,82 @@ +package com.zeta.playthegame.integration + +import java.util.concurrent.TimeUnit.DAYS + +import cats.effect.IO +import com.zeta.playthegame.model.Sport.FootballFive +import com.zeta.playthegame.model.{Appointment, Game} +import com.zeta.playthegame.repository.AppointmentsRepository +import com.zeta.playthegame.util.Generators +import com.zeta.playthegame.{AppointmentRequest, AppointmentsRoutes} +import io.circe.Json +import io.circe.generic.auto._ +import io.circe.syntax._ +import org.http4s.circe._ +import org.http4s.client.blaze.BlazeClientBuilder +import org.http4s.{HttpRoutes, Method, Request, Uri} +import org.scalatest.{FlatSpecLike, Matchers} + +import scala.concurrent.ExecutionContext.Implicits.global + +class AppointmentsIT extends FlatSpecLike + with Matchers + with Generators + with ServerApp + with MongoManager { + + import RequestUtils._ + + private val testRepository = new AppointmentsRepository(MongoTestConnection)(global) + override val router: HttpRoutes[IO] = new AppointmentsRoutes(testRepository).routes + + override def beforeAll() = { + tearDown.unsafeRunSync() + startServer + } + + override def afterAll() = stopServer + + "POST and Retrieve appointment" should "success" in { + val authorId = randomStringId + val createdDate = millisNow + val appointmentDate = millisNowPlus(2, DAYS) + val requestBody = AppointmentRequest( + authorId, + appointmentDate, + createdDate, + Game(FootballFive, List(randomStringId))).asJson + + val postRequest = post(baseUri / "appointments", requestBody) + val maybeCreatedAppointment = response(postRequest).unsafeRunSync().as[Appointment] + maybeCreatedAppointment shouldBe 'right + val createdGameAppointment = maybeCreatedAppointment.right.get + + createdGameAppointment.authorId shouldBe authorId + createdGameAppointment.appointmentDate shouldBe appointmentDate + createdGameAppointment.createdDate shouldBe createdDate + createdGameAppointment.game.sport shouldBe FootballFive + + val getRequest = get(baseUri / "appointments" / createdGameAppointment.appointmentId) + val maybeRetrievedAppointment = response(getRequest).unsafeRunSync().as[Appointment] + maybeRetrievedAppointment shouldBe 'right + val retrievedGameAppointment = maybeCreatedAppointment.right.get + + retrievedGameAppointment shouldBe createdGameAppointment + } + + object RequestUtils { + + def request(uri: Uri, method: Method) = Request[IO](method, uri) + + def post(uri: Uri) = request(uri, Method.POST) + + def post(uri: Uri, body: Json): Request[IO] = request(uri, Method.POST).withEntity(body) + + def get(uri: Uri) = request(uri, Method.GET) + + def response(request: Request[IO]) = BlazeClientBuilder[IO](global).resource.use(_.expect[Json](request)) + + def status(request: Request[IO]) = BlazeClientBuilder[IO](global).resource.use(_.status(request)) + } + +} diff --git a/src/test/scala/com/zeta/playthegame/integration/MongoManager.scala b/src/test/scala/com/zeta/playthegame/integration/MongoManager.scala new file mode 100644 index 0000000..0ec4c54 --- /dev/null +++ b/src/test/scala/com/zeta/playthegame/integration/MongoManager.scala @@ -0,0 +1,20 @@ +package com.zeta.playthegame.integration + +import cats.effect.IO +import com.zeta.playthegame.repository.Codecs.codecRegistry +import com.zeta.playthegame.repository.{Entities, MongoConnection} +import com.zeta.playthegame.util.IOExt._ +import org.mongodb.scala.{MongoCollection, MongoDatabase} + +trait MongoManager { + import MongoTestConnection._ + + def tearDown = database.map(_.drop().head()) toIO +} + +object MongoTestConnection extends MongoConnection { + override val databaseName: String = "test" + val appointmentsCollectionName = "appointments" + override val database: IO[MongoDatabase] = mongoClient.map(_.getDatabase("test").withCodecRegistry(codecRegistry)) + override val appointmentsCollection: IO[MongoCollection[Entities.AppointmentDocument]] = database.map(_.getCollection(appointmentsCollectionName)) +} \ No newline at end of file diff --git a/src/test/scala/com/zeta/playthegame/integration/ServerApp.scala b/src/test/scala/com/zeta/playthegame/integration/ServerApp.scala new file mode 100644 index 0000000..f20ce12 --- /dev/null +++ b/src/test/scala/com/zeta/playthegame/integration/ServerApp.scala @@ -0,0 +1,28 @@ +package com.zeta.playthegame.integration + +import cats.effect.{ContextShift, Fiber, IO, Timer} +import org.http4s.{HttpRoutes, Uri} +import org.http4s.implicits._ +import org.http4s.server.blaze.BlazeServerBuilder +import org.scalatest.{BeforeAndAfterAll, TestSuite} + +trait ServerApp extends BeforeAndAfterAll { self: TestSuite => + + implicit val ec = scala.concurrent.ExecutionContext.Implicits.global + implicit val cs: ContextShift[IO] = IO.contextShift(ec) + implicit val timer: Timer[IO] = IO.timer(ec) + + val baseUri = Uri.unsafeFromString("http://localhost:8080") + val router: HttpRoutes[IO] + var serverTask: Fiber[IO, Nothing] = _ + + private def createServer = { + BlazeServerBuilder[IO] + .bindHttp(8080, "0.0.0.0") + .withHttpApp(router.orNotFound) + } + + def startServer = serverTask = createServer.resource.use(_ => IO.never).start.unsafeRunSync() + + def stopServer = serverTask.cancel +} diff --git a/src/test/scala/com/zeta/playthegame/model/GameSpec.scala b/src/test/scala/com/zeta/playthegame/model/GameSpec.scala new file mode 100644 index 0000000..fe5560d --- /dev/null +++ b/src/test/scala/com/zeta/playthegame/model/GameSpec.scala @@ -0,0 +1,58 @@ +package com.zeta.playthegame.model + +import java.util.concurrent.TimeUnit.DAYS + +import com.zeta.playthegame.model.Sport.{FootballFive, TennisSingle} +import com.zeta.playthegame.util.Generators +import org.scalatest.{FlatSpecLike, Matchers} + +class GameSpec extends FlatSpecLike with Matchers with Generators { + + val zeta = randomStringId + val palan = randomStringId + val footballFive = Game(FootballFive, List(zeta)) + val appointment = Appointment(randomStringId, zeta, millisNowPlus(2, DAYS), millisNow, footballFive) + + it should "Add Players to appointment" in { + appointment.getPlayers.size shouldBe 1 + val updatedAppointment = appointment.addPlayer(palan) + updatedAppointment.getPlayers.size shouldBe 2 + updatedAppointment.getPlayers should contain theSameElementsAs List(zeta, palan) + } + + it should "Delete Players of appointment" in { + val updatedAppointment = appointment.removePlayer(zeta) + updatedAppointment.getPlayers.size shouldBe 0 + } + + it should "Change author of appointment" in { + val palan = randomStringId + appointment.authorId shouldBe zeta + val updatedAppointment = appointment.changeAuthor(palan) + updatedAppointment.authorId shouldBe palan + } + + it should "Change appointment date" in { + val updated = appointment.changeAppointmentDate(millisNowPlus(4, DAYS)) + updated.appointmentDate should be > appointment.appointmentDate + } + + it should "Change Sport" in { + val updated = appointment.changeSport(TennisSingle) + updated.sport shouldBe TennisSingle + } + + it should "Set a result" in { + val updated = appointment.addPlayer(palan) + .changeSport(TennisSingle) + .addResult(Result(List(palan), List(zeta), 3, 2)) + + updated.winner shouldBe 'defined + updated.loser shouldBe 'defined + + updated.winner.get should contain theSameElementsAs List(palan) + updated.loser.get should contain theSameElementsAs List(zeta) + + } + +}