-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #23 from palficsabapeter/sssldr-10-implement-appro…
…ve-message-api SSSLDR-10: implement ApproveMessageApi
- Loading branch information
Showing
8 changed files
with
305 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
71 changes: 71 additions & 0 deletions
71
src/main/scala/hu/bme/sch/sssl/doktor/api/ApproveMessageApi.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
src/main/scala/hu/bme/sch/sssl/doktor/service/ApproveMessageService.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
117 changes: 117 additions & 0 deletions
117
src/test/scala/hu/bme/sch/sssl/doktor/api/ApproveMessageApiSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | ||
} | ||
} | ||
} | ||
} |
72 changes: 72 additions & 0 deletions
72
src/test/scala/hu/bme/sch/sssl/doktor/service/ApproveMessageServiceSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} | ||
} |