diff --git a/webapi/src/it/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala b/webapi/src/it/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala index 54cf92adf6..345a8199c8 100644 --- a/webapi/src/it/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala +++ b/webapi/src/it/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala @@ -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."))) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala index 28a692d3b9..cda401651e 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala @@ -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 /** diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala index e08122287d..32b5f27ff7 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala @@ -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) => @@ -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) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsService.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsService.scala index 5043a3cd87..edcb2bce80 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsService.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsService.scala @@ -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 { @@ -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 { diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala index 87cd373c01..0d5664f2f5 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala @@ -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( diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala index 1361d9fef8..d5e722dc58 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala @@ -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) @@ -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 { diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceLiveSpec.scala index 7130bcdde1..cbdc7c7c72 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceLiveSpec.scala @@ -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._ @@ -56,7 +58,8 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { getAllProjectDataSpec, getProjectMembers, getProjectAdmins, - getKeywordsSpec + getKeywordsSpec, + getKeywordsByProjectIri ).provide(StringFormatter.test) val getAllProjectsSpec = test("get all projects") { @@ -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 + } + } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceMock.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceMock.scala index b676c74079..1ca3890a5d 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceMock.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceMock.scala @@ -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 { @@ -75,6 +76,11 @@ object ProjectsServiceMock extends Mock[ProjectsService] { def getKeywords(): Task[ProjectsKeywordsGetResponseADM] = proxy(GetKeywords) + def getKeywordsByProjectIri( + projectIri: ProjectIri + ): Task[ProjectKeywordsGetResponseADM] = + proxy(GetKeywordsByProjectIri, projectIri) + } } } diff --git a/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectsRouteZSpec.scala b/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectsRouteZSpec.scala index b26b6ec3a8..f0c58c28d7 100644 --- a/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectsRouteZSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectsRouteZSpec.scala @@ -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 @@ -79,7 +80,8 @@ object ProjectsRouteZSpec extends ZIOSpecDefault { getAllDataSpec, getProjectMembersSpec, getProjectAdminsSpec, - getKeywordsSpec + getKeywordsSpec, + getKeywordsByProjectSpec ) val getProjectsSpec = test("get all projects") { @@ -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."}""") + } + ) + }