Skip to content

Commit

Permalink
feat: expose GET /admin/projects/iri/{projectIri}/Keywords as ZIO HTT…
Browse files Browse the repository at this point in the history
…P route (DEV-1587)  (#2425)
  • Loading branch information
irinaschubert committed Feb 6, 2023
1 parent 97eb0fc commit 3b86834
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 26 deletions.
Expand Up @@ -664,27 +664,33 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender {
}

"return all keywords for a single project" in {
val iri = Iri.ProjectIri
.make(SharedTestDataADM.incunabulaProject.id)
.getOrElse(throw new IllegalArgumentException("Invalid project IRI"))
appActor ! ProjectKeywordsGetRequestADM(
projectIri = SharedTestDataADM.incunabulaProject.id,
requestingUser = SharedTestDataADM.rootUser
projectIri = iri
)
val received: ProjectKeywordsGetResponseADM = expectMsgType[ProjectKeywordsGetResponseADM](timeout)
received.keywords should be(SharedTestDataADM.incunabulaProject.keywords)
}

"return empty list for a project without keywords" in {
val iri = Iri.ProjectIri
.make(SharedTestDataADM.dokubibProject.id)
.getOrElse(throw new IllegalArgumentException("Invalid project IRI"))
appActor ! ProjectKeywordsGetRequestADM(
projectIri = SharedTestDataADM.dokubibProject.id,
requestingUser = SharedTestDataADM.rootUser
projectIri = iri
)
val received: ProjectKeywordsGetResponseADM = expectMsgType[ProjectKeywordsGetResponseADM](timeout)
received.keywords should be(Seq.empty[String])
}

"return 'NotFound' when the project IRI is unknown" in {
val iri = Iri.ProjectIri
.make(notExistingProjectButValidProjectIri)
.getOrElse(throw new IllegalArgumentException("Invalid project IRI"))
appActor ! ProjectKeywordsGetRequestADM(
projectIri = notExistingProjectButValidProjectIri,
SharedTestDataADM.rootUser
projectIri = iri
)

expectMsg(Failure(NotFoundException(s"Project '$notExistingProjectButValidProjectIri' not found.")))
Expand Down
Expand Up @@ -215,11 +215,9 @@ case class ProjectsKeywordsGetRequestADM() extends ProjectsResponderRequestADM
* Returns all keywords for a project identified through IRI.
*
* @param projectIri the IRI of the project.
* @param requestingUser the user making the request.
*/
case class ProjectKeywordsGetRequestADM(
projectIri: IRI,
requestingUser: UserADM
projectIri: ProjectIri
) extends ProjectsResponderRequestADM

/**
Expand Down
Expand Up @@ -73,8 +73,8 @@ final case class ProjectsResponderADM(actorDeps: ActorDeps, cacheServiceSettings
case ProjectAdminMembersGetRequestADM(identifier, requestingUser) =>
projectAdminMembersGetRequestADM(identifier, requestingUser)
case ProjectsKeywordsGetRequestADM() => projectsKeywordsGetRequestADM()
case ProjectKeywordsGetRequestADM(projectIri, requestingUser) =>
projectKeywordsGetRequestADM(projectIri, requestingUser)
case ProjectKeywordsGetRequestADM(projectIri) =>
projectKeywordsGetRequestADM(projectIri)
case ProjectRestrictedViewSettingsGetADM(identifier, requestingUser) =>
projectRestrictedViewSettingsGetADM(identifier, requestingUser)
case ProjectRestrictedViewSettingsGetRequestADM(identifier, requestingUser) =>
Expand Down Expand Up @@ -388,23 +388,21 @@ final case class ProjectsResponderADM(actorDeps: ActorDeps, cacheServiceSettings
* Gets all keywords for a single project and returns them. Returns an empty list if none are found.
*
* @param projectIri the IRI of the project.
* @param requestingUser the user making the request.
* @return keywords for a projects as [[ProjectKeywordsGetResponseADM]]
*/
private def projectKeywordsGetRequestADM(
projectIri: IRI,
requestingUser: UserADM
projectIri: Iri.ProjectIri
): Future[ProjectKeywordsGetResponseADM] =
for {
maybeProject <- getSingleProjectADM(
identifier = IriIdentifier
.fromString(projectIri)
.fromString(projectIri.value)
.getOrElseWith(e => throw BadRequestException(e.head.getMessage))
)

keywords: Seq[String] = maybeProject match {
case Some(p) => p.keywords
case None => throw NotFoundException(s"Project '$projectIri' not found.")
case None => throw NotFoundException(s"Project '${projectIri.value}' not found.")
}

} yield ProjectKeywordsGetResponseADM(keywords = keywords)
Expand Down
Expand Up @@ -41,6 +41,9 @@ trait ProjectsService {
requestingUser: UserADM
): Task[ProjectAdminMembersGetResponseADM]
def getKeywords(): Task[ProjectsKeywordsGetResponseADM]
def getKeywordsByProjectIri(
projectIri: ProjectIri
): Task[ProjectKeywordsGetResponseADM]
}

final case class ProjectsServiceLive(bridge: ActorToZioBridge) extends ProjectsService {
Expand Down Expand Up @@ -169,6 +172,10 @@ final case class ProjectsServiceLive(bridge: ActorToZioBridge) extends ProjectsS
def getKeywords(): Task[ProjectsKeywordsGetResponseADM] =
bridge.askAppActor(ProjectsKeywordsGetRequestADM())

def getKeywordsByProjectIri(
projectIri: ProjectIri
): Task[ProjectKeywordsGetResponseADM] =
bridge.askAppActor(ProjectKeywordsGetRequestADM(projectIri))
}

object ProjectsService {
Expand Down
Expand Up @@ -144,17 +144,17 @@ class ProjectsRouteADM(routeData: KnoraRouteData)
private def getProjectKeywords(): Route =
path(projectsBasePath / "iri" / Segment / "Keywords") { value =>
get { requestContext =>
val checkedProjectIri =
stringFormatter.validateAndEscapeProjectIri(value, throw BadRequestException(s"Invalid project IRI $value"))

val projectIri =
ProjectIri
.make(value)
.getOrElse(throw BadRequestException(s"Invalid project IRI $value"))
val requestMessage: Future[ProjectKeywordsGetRequestADM] = for {
requestingUser <- getUserADM(
requestContext = requestContext,
routeData.appConfig
)
} yield ProjectKeywordsGetRequestADM(
projectIri = checkedProjectIri,
requestingUser = requestingUser
projectIri = projectIri
)

RouteUtilADM.runJsonRoute(
Expand Down
Expand Up @@ -79,6 +79,8 @@ final case class ProjectsRouteZ(
) =>
getProjectAdminsByShortcode(shortcode, requestingUser)
case (Method.GET -> !! / "admin" / "projects" / "Keywords", _) => getKeywords()
case (Method.GET -> !! / "admin" / "projects" / "iri" / iriUrlEncoded / "Keywords", _) =>
getKeywordsByProjectIri(iriUrlEncoded)
}
.catchAll {
case RequestRejectedException(e) => ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig)
Expand Down Expand Up @@ -194,6 +196,13 @@ final case class ProjectsRouteZ(
r <- projectsService.getKeywords()
} yield Response.json(r.toJsValue.toString)

private def getKeywordsByProjectIri(iriUrlEncoded: String): Task[Response] =
for {
iriDecoded <- RouteUtilZ.urlDecode(iriUrlEncoded, s"Failed to URL decode IRI parameter $iriUrlEncoded.")
projectIri <- ProjectIri.make(iriDecoded).toZIO.mapError(e => BadRequestException(e.msg))
r <- projectsService.getKeywordsByProjectIri(projectIri)
} yield Response.json(r.toJsValue.toString)

}

object ProjectsRouteZ {
Expand Down
Expand Up @@ -14,6 +14,8 @@ import java.nio.file
import dsp.valueobjects.V2._
import org.knora.webapi.TestDataFactory
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectKeywordsGetRequestADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectKeywordsGetResponseADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectsKeywordsGetRequestADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectsKeywordsGetResponseADM
import org.knora.webapi.messages.admin.responder.projectsmessages._
Expand Down Expand Up @@ -56,7 +58,8 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault {
getAllProjectDataSpec,
getProjectMembers,
getProjectAdmins,
getKeywordsSpec
getKeywordsSpec,
getKeywordsByProjectIri
).provide(StringFormatter.test)

val getAllProjectsSpec = test("get all projects") {
Expand Down Expand Up @@ -381,4 +384,24 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault {
_ <- projectsService.provide(actorToZioBridge)
} yield assertCompletes
}

val getKeywordsByProjectIri = test("get keywords by project IRI") {
val iri = "http://rdfh.ch/projects/0001"
val projectIri = TestDataFactory.projectIri(iri)
val projectsService =
ZIO
.serviceWithZIO[ProjectsService](_.getKeywordsByProjectIri(projectIri))
.provideSome[ActorToZioBridge](layers)
val actorToZioBridge = ActorToZioBridgeMock.AskAppActor
.of[ProjectKeywordsGetResponseADM]
.apply(
assertion = Assertion.equalTo(ProjectKeywordsGetRequestADM(projectIri)),
result = Expectation.value(ProjectKeywordsGetResponseADM(Seq.empty[String]))
)
.toLayer
for {
_ <- projectsService.provide(actorToZioBridge)
} yield assertCompletes
}

}
Expand Up @@ -22,9 +22,10 @@ object ProjectsServiceMock extends Mock[ProjectsService] {
extends Effect[(ProjectIri, ProjectUpdatePayloadADM, UserADM), Throwable, ProjectOperationResponseADM]
object GetAllProjectData
extends Effect[(ProjectIdentifierADM.IriIdentifier, UserADM), Throwable, ProjectDataGetResponseADM]
object GetProjectMembers extends Effect[(ProjectIdentifierADM, UserADM), Throwable, ProjectMembersGetResponseADM]
object GetProjectAdmins extends Effect[(ProjectIdentifierADM, UserADM), Throwable, ProjectAdminMembersGetResponseADM]
object GetKeywords extends Effect[Unit, Throwable, ProjectsKeywordsGetResponseADM]
object GetProjectMembers extends Effect[(ProjectIdentifierADM, UserADM), Throwable, ProjectMembersGetResponseADM]
object GetProjectAdmins extends Effect[(ProjectIdentifierADM, UserADM), Throwable, ProjectAdminMembersGetResponseADM]
object GetKeywords extends Effect[Unit, Throwable, ProjectsKeywordsGetResponseADM]
object GetKeywordsByProjectIri extends Effect[ProjectIri, Throwable, ProjectKeywordsGetResponseADM]

override val compose: URLayer[Proxy, ProjectsService] =
ZLayer {
Expand Down Expand Up @@ -75,6 +76,11 @@ object ProjectsServiceMock extends Mock[ProjectsService] {
def getKeywords(): Task[ProjectsKeywordsGetResponseADM] =
proxy(GetKeywords)

def getKeywordsByProjectIri(
projectIri: ProjectIri
): Task[ProjectKeywordsGetResponseADM] =
proxy(GetKeywordsByProjectIri, projectIri)

}
}
}
Expand Up @@ -18,6 +18,7 @@ import dsp.valueobjects.V2
import org.knora.webapi.TestDataFactory
import org.knora.webapi.config.AppConfig
import org.knora.webapi.http.middleware.AuthenticationMiddleware
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectKeywordsGetResponseADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectsKeywordsGetResponseADM
import org.knora.webapi.messages.admin.responder.projectsmessages._
import org.knora.webapi.messages.admin.responder.usersmessages.UserADM
Expand Down Expand Up @@ -79,7 +80,8 @@ object ProjectsRouteZSpec extends ZIOSpecDefault {
getAllDataSpec,
getProjectMembersSpec,
getProjectAdminsSpec,
getKeywordsSpec
getKeywordsSpec,
getKeywordsByProjectSpec
)

val getProjectsSpec = test("get all projects") {
Expand Down Expand Up @@ -614,4 +616,35 @@ object ProjectsRouteZSpec extends ZIOSpecDefault {
body <- response.body.asString
} yield assertCompletes
}

val getKeywordsByProjectSpec = suite("get all keywords of a specific project")(
test("successfully get keywords") {
val iri = "http://rdfh.ch/projects/0001"
val projectIri = TestDataFactory.projectIri(iri)
val request = Request.get(url = URL(basePathProjectsIri / encode(iri) / "Keywords"))
val mockService: ULayer[ProjectsService] = ProjectsServiceMock
.GetKeywordsByProjectIri(
assertion = Assertion.equalTo(projectIri),
result = Expectation.value[ProjectKeywordsGetResponseADM](
ProjectKeywordsGetResponseADM(Seq.empty[String])
)
)
.toLayer
for {
response <- applyRoutes(request).provide(mockService)
body <- response.body.asString
} yield assertTrue(body == """{"keywords":[]}""")
},
test("return a BadRequest Exception if project IRI is invalid") {
val iri = "http://rdfh.ch/project/0001"
val request = Request.get(url = URL(basePathProjectsIri / encode(iri) / "Keywords"))

for {
response <- applyRoutes(request).provide(ProjectsServiceMock.empty)
bodyAsString <- response.body.asString
} yield assertTrue(response.status == Status.BadRequest) &&
assertTrue(bodyAsString == """{"error":"dsp.errors.BadRequestException: Project IRI is invalid."}""")
}
)

}

0 comments on commit 3b86834

Please sign in to comment.