Skip to content

Commit

Permalink
Merge pull request #23 from palficsabapeter/sssldr-10-implement-appro…
Browse files Browse the repository at this point in the history
…ve-message-api

SSSLDR-10: implement ApproveMessageApi
  • Loading branch information
palficsabapeter authored Sep 12, 2020
2 parents f2ae0a0 + 5803b2c commit b6fa123
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/main/scala/hu/bme/sch/sssl/doktor/api/ApiBase.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ trait ApiBase {
statusMapping(StatusCode.InternalServerError, jsonBody[DbActionUnsuccessful].description("Database action was unsuccessful!")),
statusMapping(StatusCode.BadRequest, jsonBody[UnsuccessfulAction].description("Unsuccessful action!")),
statusMapping(StatusCode.NotFound, jsonBody[TicketNotFound].description("Ticket not found!")),
statusMapping(StatusCode.NotFound, jsonBody[MessageNotFound].description("Message not found!")),
),
)
}
Expand Down
71 changes: 71 additions & 0 deletions src/main/scala/hu/bme/sch/sssl/doktor/api/ApproveMessageApi.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package hu.bme.sch.sssl.doktor.api

import java.util.UUID

import cats.data.EitherT
import cats.implicits._
import hu.bme.sch.sssl.doktor.`enum`.Authorities
import hu.bme.sch.sssl.doktor.api.ApiBase.EmptyResponseBody
import hu.bme.sch.sssl.doktor.auth.JwtAuth
import hu.bme.sch.sssl.doktor.service.ApproveMessageService
import hu.bme.sch.sssl.doktor.util.ErrorUtil.AuthError

import scala.concurrent.{ExecutionContext, Future}

class ApproveMessageApi(
implicit
jwtAuth: JwtAuth,
service: ApproveMessageService,
ec: ExecutionContext,
) extends ApiBase {
import JwtAuth._
import io.circe.generic.auto._
import sttp.tapir._
import sttp.tapir.json.circe._

def endpoints =
List(
approveMessage,
declineMessage,
)

private def approveMessage =
endpoint.post
.in("tickets" / path[UUID] / "messages" / path[UUID] / "approve")
.in(auth.bearer)
.out(jsonBody[EmptyResponseBody])
.withGeneralErrorHandler()
.serverLogic {
case (ticketId: UUID, messageId: UUID, bearer: String) =>
jwtAuth
.auth(bearer)
.withPayload { jwt =>
if (jwt.authorities.contains(Authorities.Admin))
service.approveMessage(ticketId, messageId, true, jwt.user, jwt.uid)
else
EitherT.leftT[Future, Unit](AuthError("No sufficient authorities!"))
}
.handleUnit
.value
}

private def declineMessage =
endpoint.post
.in("tickets" / path[UUID] / "messages" / path[UUID] / "decline")
.in(auth.bearer)
.out(jsonBody[EmptyResponseBody])
.withGeneralErrorHandler()
.serverLogic {
case (ticketId: UUID, messageId: UUID, bearer: String) =>
jwtAuth
.auth(bearer)
.withPayload { jwt =>
if (jwt.authorities.contains(Authorities.Admin))
service.approveMessage(ticketId, messageId, false, jwt.user, jwt.uid)
else
EitherT.leftT[Future, Unit](AuthError("No sufficient authorities!"))
}
.handleUnit
.value
}
}
1 change: 1 addition & 0 deletions src/main/scala/hu/bme/sch/sssl/doktor/app/Apis.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Apis(auths: Auths, services: Services)(
new TicketApi(),
new AssignTicketApi(),
new NewMessageApi(),
new ApproveMessageApi(),
).flatMap(_.endpoints)

val swaggerRoute: Route = {
Expand Down
1 change: 1 addition & 0 deletions src/main/scala/hu/bme/sch/sssl/doktor/app/Services.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ class Services(config: Config, repositories: Repositories, auths: Auths)(
implicit val ticketDetailsService: TicketDetailsService = new TicketDetailsService()
implicit val assignTicketService: AssignTicketService = new AssignTicketService()
implicit val newMessageService: NewMessageService = new NewMessageService()
implicit val approveMessageService: ApproveMessageService = new ApproveMessageService
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package hu.bme.sch.sssl.doktor.service

import java.util.UUID

import cats.data.EitherT
import hu.bme.sch.sssl.doktor.`enum`.MessageStatus
import hu.bme.sch.sssl.doktor.repository.MessageRepository
import hu.bme.sch.sssl.doktor.util.ErrorUtil.{AppErrorOr, MessageNotFound}
import hu.bme.sch.sssl.doktor.util.TimeProvider

import scala.concurrent.ExecutionContext

class ApproveMessageService(
implicit
repo: MessageRepository,
tp: TimeProvider,
ec: ExecutionContext,
) {
def approveMessage(ticketId: UUID, messageId: UUID, approved: Boolean, reviewedBy: String, reviewedByUid: String): AppErrorOr[Unit] = {
val res = repo
.findByTicketId(ticketId)
.map { messages =>
messages
.find(_.messageId == messageId)
.map { message =>
repo.upsert(
message.copy(
status = if (approved) MessageStatus.Shown else MessageStatus.Discarded,
reviewedAt = Some(tp.epochMillis),
reviewedBy = Some(reviewedBy),
reviewedByUid = Some(reviewedByUid),
),
)
}
.map(_ => Right({}))
.getOrElse(Left(MessageNotFound(s"Message with id '$messageId' and related ticket id '$ticketId' was not found!")))
}

EitherT(res)
}
}
1 change: 1 addition & 0 deletions src/main/scala/hu/bme/sch/sssl/doktor/util/ErrorUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ object ErrorUtil {
case class DbActionUnsuccessful(message: String) extends AppError
case class UnsuccessfulAction(message: String) extends AppError
case class TicketNotFound(message: String) extends AppError
case class MessageNotFound(message: String) extends AppError
}
117 changes: 117 additions & 0 deletions src/test/scala/hu/bme/sch/sssl/doktor/api/ApproveMessageApiSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package hu.bme.sch.sssl.doktor.api

import java.util.UUID

import cats.implicits._
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.headers.OAuth2BearerToken
import akka.http.scaladsl.server.Route
import hu.bme.sch.sssl.doktor.service.ApproveMessageService
import hu.bme.sch.sssl.doktor.testutil.{ApiTestBase, AuthTestUtil}
import hu.bme.sch.sssl.doktor.util.ErrorUtil.{AuthError, MessageNotFound}
import io.circe.Decoder
import io.circe.generic.semiauto.deriveDecoder

class ApproveMessageApiSpec extends ApiTestBase with AuthTestUtil {
trait TestScope {
implicit val service: ApproveMessageService = mock[ApproveMessageService]

val api: ApproveMessageApi = new ApproveMessageApi()
val route: Route = api.route

val ticketId: UUID = UUID.randomUUID()
val messageId: UUID = UUID.randomUUID()
}

"ApproveMessageApi" should {
"POST /tickets/{ticketId}/messages/{messageId}/approve" should {
"return OK" in new TestScope {
whenF(service.approveMessage(any[UUID], any[UUID], any[Boolean], any[String], any[String])).thenReturn({})

Post(s"/tickets/$ticketId/messages/$messageId/approve") ~> addCredentials(OAuth2BearerToken(validToken)) ~> route ~> check {
status shouldEqual StatusCodes.OK
}

verify(service).approveMessage(ticketId, messageId, true, user, uid)
}

"return NotFound if there was no message found for the provided ids" in new TestScope {
private val error = MessageNotFound(s"Message with id '$messageId' and related ticket id '$ticketId' was not found!")
whenF(service.approveMessage(any[UUID], any[UUID], any[Boolean], any[String], any[String])).thenFailWith(error)

Post(s"/tickets/$ticketId/messages/$messageId/approve") ~> addCredentials(OAuth2BearerToken(validToken)) ~> route ~> check {
status shouldEqual StatusCodes.NotFound
implicit val decoder: Decoder[MessageNotFound] = deriveDecoder
responseShouldBeDto[MessageNotFound](error)
}

verify(service).approveMessage(ticketId, messageId, true, user, uid)
}

"return Unauthorized if the user did not have Admin authority" in new TestScope {
Post(s"/tickets/$ticketId/messages/$messageId/approve") ~> addCredentials(OAuth2BearerToken(validTokenWithClerkAuth)) ~> route ~> check {
status shouldEqual StatusCodes.Unauthorized
implicit val decoder: Decoder[AuthError] = deriveDecoder
responseShouldBeDto[AuthError](AuthError("No sufficient authorities!"))
}

verify(service, times(0)).approveMessage(any[UUID], any[UUID], any[Boolean], any[String], any[String])
}

"return Unauthorized if there were invalid credentials provided" in new TestScope {
Post(s"/tickets/$ticketId/messages/$messageId/approve") ~> addCredentials(OAuth2BearerToken("invalidToken")) ~> route ~> check {
status shouldEqual StatusCodes.Unauthorized
implicit val decoder: Decoder[AuthError] = deriveDecoder
responseShouldBeDto[AuthError](AuthError("Invalid JWT token!"))
}

verify(service, times(0)).approveMessage(any[UUID], any[UUID], any[Boolean], any[String], any[String])
}
}

"POST /tickets/{ticketId}/messages/{messageId}/decline" should {
"return OK" in new TestScope {
whenF(service.approveMessage(any[UUID], any[UUID], any[Boolean], any[String], any[String])).thenReturn({})

Post(s"/tickets/$ticketId/messages/$messageId/decline") ~> addCredentials(OAuth2BearerToken(validToken)) ~> route ~> check {
status shouldEqual StatusCodes.OK
}

verify(service).approveMessage(ticketId, messageId, false, user, uid)
}

"return NotFound if there was no message found for the provided ids" in new TestScope {
private val error = MessageNotFound(s"Message with id '$messageId' and related ticket id '$ticketId' was not found!")
whenF(service.approveMessage(any[UUID], any[UUID], any[Boolean], any[String], any[String])).thenFailWith(error)

Post(s"/tickets/$ticketId/messages/$messageId/decline") ~> addCredentials(OAuth2BearerToken(validToken)) ~> route ~> check {
status shouldEqual StatusCodes.NotFound
implicit val decoder: Decoder[MessageNotFound] = deriveDecoder
responseShouldBeDto[MessageNotFound](error)
}

verify(service).approveMessage(ticketId, messageId, false, user, uid)
}

"return Unauthorized if the user did not have Admin authority" in new TestScope {
Post(s"/tickets/$ticketId/messages/$messageId/decline") ~> addCredentials(OAuth2BearerToken(validTokenWithClerkAuth)) ~> route ~> check {
status shouldEqual StatusCodes.Unauthorized
implicit val decoder: Decoder[AuthError] = deriveDecoder
responseShouldBeDto[AuthError](AuthError("No sufficient authorities!"))
}

verify(service, times(0)).approveMessage(any[UUID], any[UUID], any[Boolean], any[String], any[String])
}

"return Unauthorized if there were invalid credentials provided" in new TestScope {
Post(s"/tickets/$ticketId/messages/$messageId/decline") ~> addCredentials(OAuth2BearerToken("invalidToken")) ~> route ~> check {
status shouldEqual StatusCodes.Unauthorized
implicit val decoder: Decoder[AuthError] = deriveDecoder
responseShouldBeDto[AuthError](AuthError("Invalid JWT token!"))
}

verify(service, times(0)).approveMessage(any[UUID], any[UUID], any[Boolean], any[String], any[String])
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package hu.bme.sch.sssl.doktor.service

import java.util.UUID

import cats.implicits._
import hu.bme.sch.sssl.doktor.`enum`.MessageStatus
import hu.bme.sch.sssl.doktor.repository.MessageRepository
import hu.bme.sch.sssl.doktor.repository.MessageRepository.MessageDbo
import hu.bme.sch.sssl.doktor.testutil.TestBase
import hu.bme.sch.sssl.doktor.util.ErrorUtil.{AppError, MessageNotFound}
import hu.bme.sch.sssl.doktor.util.TimeProvider

class ApproveMessageServiceSpec extends TestBase {
trait TestScope {
implicit val repo: MessageRepository = mock[MessageRepository]
implicit val tp: TimeProvider = new TimeProvider {
override def epochMillis: Long = 0L
override def epochSecs: Long = 0L
}

val service: ApproveMessageService = new ApproveMessageService()

val messageId: UUID = UUID.randomUUID()
val ticketId: UUID = UUID.randomUUID()
val dbo: MessageDbo = MessageDbo(
messageId,
ticketId,
"uid",
"user",
0L,
MessageStatus.Unreviewed,
"Message text",
None,
None,
None,
)
}

"ApproveMessageService" should {
"#approveMessage" should {
"return OK" in new TestScope {
whenF(repo.findByTicketId(any[UUID])).thenReturn(Seq(dbo))

await(service.approveMessage(ticketId, messageId, true, "user", "uid").value) shouldBe
Right({})

verify(repo).findByTicketId(ticketId)
}

"return AppError if there was no messages found for the provided ticketId" in new TestScope {
private val error: AppError = MessageNotFound(s"Message with id '$messageId' and related ticket id '$ticketId' was not found!")
whenF(repo.findByTicketId(any[UUID])).thenReturn(Seq.empty[MessageDbo])

await(service.approveMessage(ticketId, messageId, true, "user", "uid").value) shouldBe
Left(error)

verify(repo).findByTicketId(ticketId)
}

"return an AppError if there was no messages found for the provided messageId" in new TestScope {
private val invalidMessageId = UUID.fromString("00000000-0000-0000-0000-000000000000")
private val error: AppError = MessageNotFound(s"Message with id '$invalidMessageId' and related ticket id '$ticketId' was not found!")
whenF(repo.findByTicketId(any[UUID])).thenReturn(Seq(dbo))

await(service.approveMessage(ticketId, invalidMessageId, true, "user", "uid").value) shouldBe
Left(error)

verify(repo).findByTicketId(ticketId)
}
}
}
}

0 comments on commit b6fa123

Please sign in to comment.