Skip to content

Commit

Permalink
refactor: Migrate PUT /admin/groups/<groupIri>/status to Tapir (DEV-1588
Browse files Browse the repository at this point in the history
) (#3075)

Signed-off-by: Marcin Procyk <marcin.procyk@dasch.swiss>
Co-authored-by: Christian Kleinbölting <seakayone@users.noreply.github.com>
  • Loading branch information
mpro7 and seakayone committed Mar 4, 2024
1 parent f60d937 commit 2ca95ed
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 102 deletions.
Expand Up @@ -5,8 +5,6 @@

package org.knora.webapi.responders.admin

import org.apache.pekko.actor.Status.Failure

import java.util.UUID

import dsp.errors.*
Expand All @@ -19,6 +17,7 @@ import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2
import org.knora.webapi.routing.UnsafeZioRun
import org.knora.webapi.sharedtestdata.SharedTestDataADM.*
import org.knora.webapi.slice.admin.api.GroupsRequests.GroupCreateRequest
import org.knora.webapi.slice.admin.api.GroupsRequests.GroupStatusUpdateRequest
import org.knora.webapi.slice.admin.api.GroupsRequests.GroupUpdateRequest
import org.knora.webapi.slice.admin.domain.model.GroupIri
import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri
Expand Down Expand Up @@ -193,7 +192,7 @@ class GroupsResponderADMSpec extends CoreSpec {

"used to query members" should {
"return all members of a group identified by IRI" in {
val iri = (GroupIri.unsafeFrom(imagesReviewerGroup.id))
val iri = GroupIri.unsafeFrom(imagesReviewerGroup.id)
val received =
UnsafeZioRun.runOrThrow(GroupsResponderADM.groupMembersGetRequest(iri, rootUser))

Expand All @@ -204,42 +203,44 @@ class GroupsResponderADMSpec extends CoreSpec {
}

"remove all members when group is deactivated" in {
appActor ! GroupMembersGetRequestADM(
groupIri = imagesReviewerGroup.id,
requestingUser = rootUser
val group = UnsafeZioRun.runOrThrow(
GroupsResponderADM.groupMembersGetRequestADM(
GroupIri.unsafeFrom(imagesReviewerGroup.id).value,
rootUser
)
)
group.members.size shouldBe 2

val membersBeforeStatusChange: GroupMembersGetResponseADM = expectMsgType[GroupMembersGetResponseADM](timeout)
membersBeforeStatusChange.members.size shouldBe 2

appActor ! GroupChangeStatusRequestADM(
groupIri = imagesReviewerGroup.id,
changeGroupRequest = ChangeGroupApiRequestADM(status = Some(false)),
requestingUser = imagesUser01,
apiRequestID = UUID.randomUUID
val statusChangeResponse = UnsafeZioRun.runOrThrow(
GroupsResponderADM.updateGroupStatus(
GroupIri.unsafeFrom(imagesReviewerGroup.id),
GroupStatusUpdateRequest(GroupStatus.inactive),
UUID.randomUUID()
)
)

val statusChangeResponse = expectMsgType[GroupGetResponseADM](timeout)
statusChangeResponse.group.status shouldBe false

appActor ! GroupMembersGetRequestADM(
groupIri = imagesReviewerGroup.id,
requestingUser = rootUser
val anotherGroup = UnsafeZioRun.runOrThrow(
GroupsResponderADM.groupMembersGetRequest(
GroupIri.unsafeFrom(imagesReviewerGroup.id),
rootUser
)
)

val noMembers: GroupMembersGetResponseADM = expectMsgType[GroupMembersGetResponseADM](timeout)
noMembers.members.size shouldBe 0
anotherGroup.members.size shouldBe 0
}

"return 'NotFound' when the group IRI is unknown" in {
appActor ! GroupMembersGetRequestADM(
groupIri = "http://rdfh.ch/groups/notexisting",
requestingUser = rootUser
val groupIri = "http://rdfh.ch/groups/0000/notexisting"
val exit = UnsafeZioRun.run(
GroupsResponderADM.groupMembersGetRequest(
GroupIri.unsafeFrom(groupIri),
rootUser
)
)
assertFailsWithA[NotFoundException](
exit,
s"Group <$groupIri> not found"
)

expectMsgPF(timeout) { case msg: Failure =>
msg.cause.isInstanceOf[NotFoundException] should ===(true)
}
}
}
}
Expand Down
Expand Up @@ -37,6 +37,7 @@ import org.knora.webapi.responders.IriService
import org.knora.webapi.responders.Responder
import org.knora.webapi.slice.admin.AdminConstants
import org.knora.webapi.slice.admin.api.GroupsRequests.GroupCreateRequest
import org.knora.webapi.slice.admin.api.GroupsRequests.GroupStatusUpdateRequest
import org.knora.webapi.slice.admin.api.GroupsRequests.GroupUpdateRequest
import org.knora.webapi.slice.admin.domain.model.GroupIri
import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode
Expand Down Expand Up @@ -113,6 +114,20 @@ trait GroupsResponderADM {
apiRequestID: UUID
): Task[GroupGetResponseADM]

/**
* Change group's status.
*
* @param groupIri the IRI of the group we want to change.
* @param request the change request.
* @param apiRequestID the unique request ID.
* @return a [[GroupGetResponseADM]].
*/
def updateGroupStatus(
groupIri: GroupIri,
request: GroupStatusUpdateRequest,
apiRequestID: UUID
): Task[GroupGetResponseADM]

/**
* Change group's basic information.
*
Expand All @@ -128,6 +143,7 @@ trait GroupsResponderADM {
requestingUser: User,
apiRequestID: UUID
): Task[GroupGetResponseADM]

}

final case class GroupsResponderADMLive(
Expand All @@ -151,7 +167,12 @@ final case class GroupsResponderADMLive(
case r: MultipleGroupsGetRequestADM => multipleGroupsGetRequestADM(r.groupIris)
case r: GroupMembersGetRequestADM => groupMembersGetRequestADM(r.groupIri, r.requestingUser)
case r: GroupChangeStatusRequestADM =>
changeGroupStatusRequestADM(r.groupIri, r.changeGroupRequest, r.requestingUser, r.apiRequestID)
changeGroupStatusRequestADM(
r.groupIri,
r.changeGroupRequest,
r.requestingUser,
r.apiRequestID
)
case other => Responder.handleUnexpectedMessage(other, this.getClass.getName)
}

Expand Down Expand Up @@ -362,38 +383,43 @@ final case class GroupsResponderADMLive(
apiRequestID: UUID
): Task[GroupGetResponseADM] = {
val task = for {
// check if necessary information is present
_ <- ZIO
.fail(BadRequestException("Group IRI cannot be empty"))
.when(groupIri.value.isEmpty)

/* Get the project IRI which also verifies that the group exists. */
_ <- ZIO
.fail(NotFoundException(s"Group <${groupIri.value}> not found. Aborting update request."))
.whenZIO(groupGetADM(groupIri.value).map(_.isEmpty))

/* create the update request */
groupUpdatePayload =
GroupUpdateRequest(
name = request.name,
descriptions = request.descriptions,
status = request.status,
selfjoin = request.selfjoin
)
result <- updateGroupHelper(groupIri.value, groupUpdatePayload)
result <- updateGroupHelper(groupIri, request)
} yield result
IriLocker.runWithIriLock(apiRequestID, groupIri.value, task)
}

/**
* Change group's basic information.
* Change group's status.
*
* @param groupIri the IRI of the group we want to change.
* @param changeGroupRequest the change request.
* @param requestingUser the user making the request.
* @param request the change request.
* @param apiRequestID the unique request ID.
* @return a [[GroupGetResponseADM]].
*/
override def updateGroupStatus(
groupIri: GroupIri,
request: GroupStatusUpdateRequest,
apiRequestID: UUID
): Task[GroupGetResponseADM] = {
val task = for {
// update group status
updated <- updateGroupHelper(groupIri, GroupUpdateRequest(None, None, Some(request.status), None))

// remove all members from group if status is false
result <- removeGroupMembersIfNecessary(updated.group)
} yield result
IriLocker.runWithIriLock(apiRequestID, groupIri.value, task)
}

/**
* Change group's basic information.
*
* @param groupIri the IRI of the group we want to change.
* @param changeGroupRequest the change request.
* @param requestingUser the user making the request.
* @param apiRequestID the unique request ID.
* @return a [[GroupGetResponseADM]].
*/
override def changeGroupStatusRequestADM(
groupIri: IRI,
changeGroupRequest: ChangeGroupApiRequestADM,
Expand All @@ -410,16 +436,9 @@ final case class GroupsResponderADMLive(
requestingUser: User
): Task[GroupGetResponseADM] =
for {

// check if necessary information is present
_ <- ZIO
.fail(BadRequestException("Group IRI cannot be empty"))
.when(groupIri.isEmpty)

/* Get the project IRI which also verifies that the group exists. */
groupADM <- groupGetADM(groupIri)
.flatMap(ZIO.fromOption(_))
.orElseFail(NotFoundException(s"Group <$groupIri> not found. Aborting update request."))
.someOrFail(NotFoundException(s"Group <$groupIri> not found. Aborting update request."))

/* check if the requesting user is allowed to perform updates */
_ <- ZIO
Expand All @@ -435,8 +454,10 @@ final case class GroupsResponderADMLive(
/* create the update request */
groupUpdatePayload = GroupUpdateRequest(status = maybeStatus)

iri <- ZIO.fromEither(GroupIri.from(groupIri)).mapError(BadRequestException(_))

// update group status
updateGroupResult <- updateGroupHelper(groupIri, groupUpdatePayload)
updateGroupResult <- updateGroupHelper(iri, groupUpdatePayload)

// remove all members from group if status is false
operationResponse <-
Expand All @@ -455,7 +476,7 @@ final case class GroupsResponderADMLive(
* @param request the payload holding the information which we want to update.
* @return a [[GroupGetResponseADM]]
*/
private def updateGroupHelper(groupIri: IRI, request: GroupUpdateRequest) =
private def updateGroupHelper(groupIri: GroupIri, request: GroupUpdateRequest) =
for {
_ <- ZIO
.fail(BadRequestException("No data would be changed. Aborting update request."))
Expand All @@ -470,8 +491,9 @@ final case class GroupsResponderADMLive(
)

/* Verify that the group exists. */
groupADM <- groupGetADM(groupIri)
.someOrFail(NotFoundException(s"Group <$groupIri> not found. Aborting update request."))
groupADM <-
groupGetADM(groupIri.value)
.someOrFail(NotFoundException(s"Group <${groupIri.value}> not found. Aborting update request."))

/* Verify that the potentially new name is unique */
groupByNameAlreadyExists <-
Expand All @@ -487,7 +509,7 @@ final case class GroupsResponderADMLive(
sparql.admin.txt
.updateGroup(
adminNamedGraphIri = "http://www.knora.org/data/admin",
groupIri,
groupIri.value,
maybeName = request.name.map(_.value),
maybeDescriptions = request.descriptions.map(_.value),
maybeProject = None, // maybe later we want to allow moving of a group to another project
Expand All @@ -498,7 +520,7 @@ final case class GroupsResponderADMLive(

/* Verify that the project was updated. */
updatedGroup <-
groupGetADM(groupIri)
groupGetADM(groupIri.value)
.someOrFail(UpdateNotPerformedException("Group was not updated. Please report this as a possible bug."))
} yield GroupGetResponseADM(updatedGroup)

Expand Down
Expand Up @@ -10,16 +10,13 @@ import org.apache.pekko.http.scaladsl.server.PathMatcher
import org.apache.pekko.http.scaladsl.server.Route
import zio.*

import dsp.errors.BadRequestException
import dsp.valueobjects.Iri
import org.knora.webapi.core.MessageRelay
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.admin.responder.groupsmessages.*
import org.knora.webapi.routing.Authenticator
import org.knora.webapi.routing.KnoraRoute
import org.knora.webapi.routing.KnoraRouteData
import org.knora.webapi.routing.RouteUtilADM.*
import org.knora.webapi.routing.RouteUtilZ

/**
* Provides a routing function for API routes that deal with groups.
Expand All @@ -34,38 +31,7 @@ final case class GroupsRouteADM(
private val groupsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "groups")

override def makeRoute: Route =
changeGroupStatus() ~
deleteGroup()

/**
* Updates the group's status.
*/
private def changeGroupStatus(): Route =
path(groupsBasePath / Segment / "status") { value =>
put {
entity(as[ChangeGroupApiRequestADM]) { apiRequest => requestContext =>
val requestTask = for {
/**
* The api request is already checked at time of creation.
* See case class. Depending on the data sent, we are either
* doing a general update or status change. Since we are in
* the status change route, we are only interested in the
* value of the status property
*/
_ <- ZIO
.fail(BadRequestException("The status property is not allowed to be empty."))
.when(apiRequest.status.isEmpty)
iri <- Iri
.validateAndEscapeIri(value)
.toZIO
.orElseFail(BadRequestException(s"Invalid group IRI $value"))
requestingUser <- Authenticator.getUserADM(requestContext)
uuid <- RouteUtilZ.randomUuid()
} yield GroupChangeStatusRequestADM(iri, apiRequest, requestingUser, uuid)
runJsonRouteZ(requestTask, requestContext)
}
}
}
deleteGroup()

/**
* Deletes a group (sets status to false).
Expand Down
Expand Up @@ -22,6 +22,7 @@ import org.knora.webapi.messages.admin.responder.usersmessages.GroupMembersGetRe
import org.knora.webapi.messages.admin.responder.usersmessages.UsersADMJsonProtocol.*
import org.knora.webapi.slice.admin.api.AdminPathVariables.groupIriPathVar
import org.knora.webapi.slice.admin.api.GroupsRequests.GroupCreateRequest
import org.knora.webapi.slice.admin.api.GroupsRequests.GroupStatusUpdateRequest
import org.knora.webapi.slice.admin.api.GroupsRequests.GroupUpdateRequest
import org.knora.webapi.slice.admin.domain.model.GroupIri
import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri
Expand Down Expand Up @@ -58,6 +59,12 @@ final case class GroupsEndpoints(baseEndpoints: BaseEndpoints) {
.out(sprayJsonBody[GroupGetResponseADM])
.description("Updates a group.")

val putGroupStatus = baseEndpoints.securedEndpoint.put
.in(base / groupIriPathVar / "status")
.in(zioJsonBody[GroupStatusUpdateRequest])
.out(sprayJsonBody[GroupGetResponseADM])
.description("Updates a group's status.")

private val securedEndpoins = Seq(getGroupMembers, postGroup, putGroup).map(_.endpoint)

val endpoints: Seq[AnyEndpoint] = (Seq(getGroups, getGroupByIri) ++ securedEndpoins)
Expand Down Expand Up @@ -87,6 +94,13 @@ object GroupsRequests {
object GroupUpdateRequest {
implicit val jsonCodec: JsonCodec[GroupUpdateRequest] = DeriveJsonCodec.gen[GroupUpdateRequest]
}

final case class GroupStatusUpdateRequest(
status: GroupStatus
)
object GroupStatusUpdateRequest {
implicit val jsonCodec: JsonCodec[GroupStatusUpdateRequest] = DeriveJsonCodec.gen[GroupStatusUpdateRequest]
}
}

object GroupsEndpoints {
Expand Down

0 comments on commit 2ca95ed

Please sign in to comment.