diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/RestProjectsService.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/RestProjectsService.scala index 073e636b1b..9d06918125 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/RestProjectsService.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/RestProjectsService.scala @@ -3,7 +3,6 @@ import zio.Task import zio.URLayer import zio.ZLayer -import org.knora.webapi.IRI import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM @@ -11,22 +10,6 @@ import org.knora.webapi.responders.ActorToZioBridge final case class RestProjectsService(bridge: ActorToZioBridge) { - /** - * Finds the project by an [[IRI]] and returns the information as a [[ProjectGetResponseADM]]. - * - * @param projectIri an [[IRI]] identifying the project - * @return - * '''success''': information about the project as a [[ProjectGetResponseADM]] - * - * '''error''': [[dsp.errors.NotFoundException]] when no project for the given IRI can be found - * [[dsp.errors.ValidationException]] if the given `projectIri` is invalid - */ - def getSingleProjectADMRequest(projectIri: IRI): Task[ProjectGetResponseADM] = - ProjectIdentifierADM.IriIdentifier - .fromString(projectIri) - .toZIO - .flatMap(getSingleProjectADMRequest(_)) - /** * Finds the project by its [[ProjectIdentifierADM]] and returns the information as a [[ProjectGetResponseADM]]. * diff --git a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilZ.scala b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilZ.scala index 1fae3dd846..415cea03cb 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilZ.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilZ.scala @@ -16,9 +16,9 @@ object RouteUtilZ { * * @param value The value in utf-8 to be url decoded * @param errorMsg Custom error message for the error type - * @return ```success``` the decoded string + * @return '''success''' the decoded string * - * ```failure``` A [[BadRequestException]] with the `errorMsg` + * '''failure''' A [[BadRequestException]] with the `errorMsg` */ def urlDecode(value: String, errorMsg: String = ""): Task[String] = ZIO 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 41a3661f99..00b6cb77aa 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 @@ -1,13 +1,16 @@ package org.knora.webapi.routing.admin import zhttp.http._ +import zio.Task import zio.URLayer import zio.ZLayer +import dsp.errors.BadRequestException import dsp.errors.InternalServerException import dsp.errors.RequestRejectedException import org.knora.webapi.config.AppConfig import org.knora.webapi.http.handler.ExceptionHandlerZ +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM._ import org.knora.webapi.responders.admin.RestProjectsService import org.knora.webapi.routing.RouteUtilZ @@ -18,21 +21,34 @@ final case class ProjectsRouteZ( val route: HttpApp[Any, Nothing] = Http - .collectZIO[Request] { case Method.GET -> !! / "admin" / "projects" / "iri" / iriUrlEncoded => - getProjectByIriEncoded(iriUrlEncoded) + .collectZIO[Request] { + case Method.GET -> !! / "admin" / "projects" / "iri" / iriUrlEncoded => getProjectByIriEncoded(iriUrlEncoded) + case Method.GET -> !! / "admin" / "projects" / "shortname" / shortname => getProjectByShortname(shortname) + case Method.GET -> !! / "admin" / "projects" / "shortcode" / shortcode => getProjectByShortcode(shortcode) } .catchAll { - case RequestRejectedException(e) => - ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig) - case InternalServerException(e) => - ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig) + case RequestRejectedException(e) => ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig) + case InternalServerException(e) => ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig) } - private def getProjectByIriEncoded(iriUrlEncoded: String) = - RouteUtilZ - .urlDecode(iriUrlEncoded, "Failed to url decode IRI parameter.") - .flatMap(projectsService.getSingleProjectADMRequest(_).map(_.toJsValue.toString())) - .map(Response.json(_)) + private def getProjectByIriEncoded(iriUrlEncoded: String): Task[Response] = + for { + iriDecoded <- RouteUtilZ.urlDecode(iriUrlEncoded, s"Failed to URL decode IRI parameter $iriUrlEncoded.") + iri <- IriIdentifier.fromString(iriDecoded).toZIO + projectGetResponse <- projectsService.getSingleProjectADMRequest(identifier = iri) + } yield Response.json(projectGetResponse.toJsValue.toString()) + + private def getProjectByShortname(shortname: String): Task[Response] = + for { + shortnameIdentifier <- ShortnameIdentifier.fromString(shortname).toZIO.mapError(e => BadRequestException(e.msg)) + projectGetResponse <- projectsService.getSingleProjectADMRequest(identifier = shortnameIdentifier) + } yield Response.json(projectGetResponse.toJsValue.toString()) + + private def getProjectByShortcode(shortcode: String): Task[Response] = + for { + shortcodeIdentifier <- ShortcodeIdentifier.fromString(shortcode).toZIO.mapError(e => BadRequestException(e.msg)) + projectGetResponse <- projectsService.getSingleProjectADMRequest(identifier = shortcodeIdentifier) + } yield Response.json(projectGetResponse.toJsValue.toString()) } object ProjectsRouteZ { diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/RestProjectsServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/RestProjectsServiceSpec.scala index 214f46d624..af3a97dcbc 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/RestProjectsServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/RestProjectsServiceSpec.scala @@ -3,13 +3,11 @@ import zio.Scope import zio.ZIO import zio.mock._ import zio.test.Assertion -import zio.test.SmartAssertionOps import zio.test.Spec import zio.test.TestEnvironment import zio.test.ZIOSpecDefault import zio.test.assertTrue -import dsp.errors.ValidationException import dsp.valueobjects.Project.ShortCode import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM @@ -53,12 +51,6 @@ object RestProjectsServiceSpec extends ZIOSpecDefault { actual <- sut.getSingleProjectADMRequest(id) } yield assertTrue(actual == expectedResponse) } - .provide(RestProjectsService.layer, expectSuccess), - test("given an invalid iri should return with a failure") { - for { - sut <- systemUnderTest - result <- sut.getSingleProjectADMRequest("invalid").exit - } yield assertTrue(result.is(_.failure) == ValidationException("Project IRI is invalid.")) - }.provide(RestProjectsService.layer, expectNoInteraction) + .provide(RestProjectsService.layer, expectSuccess) ) } diff --git a/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectRouteZSpec.scala b/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectRouteZSpec.scala deleted file mode 100644 index b92508324f..0000000000 --- a/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectRouteZSpec.scala +++ /dev/null @@ -1,77 +0,0 @@ -package org.knora.webapi.routing.admin - -import zhttp.http._ -import zio._ -import zio.mock.Expectation -import zio.test.ZIOSpecDefault -import zio.test._ - -import java.net.URLEncoder - -import org.knora.webapi.config.AppConfig -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM -import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 -import org.knora.webapi.responders.ActorToZioBridge -import org.knora.webapi.responders.ActorToZioBridgeMock -import org.knora.webapi.responders.admin.RestProjectsService - -object ProjectRouteZSpec extends ZIOSpecDefault { - - private val systemUnderTest: URIO[ProjectsRouteZ, HttpApp[Any, Nothing]] = ZIO.service[ProjectsRouteZ].map(_.route) - - // Test data - private val projectIri: ProjectIdentifierADM.IriIdentifier = - ProjectIdentifierADM.IriIdentifier - .fromString("http://rdfh.ch/projects/0001") - .getOrElse(throw new IllegalArgumentException()) - - private val basePath: Path = !! / "admin" / "projects" / "iri" - private val validIriEncoded: String = URLEncoder.encode(projectIri.value.value, "utf-8") - private val expectedRequestSuccess: ProjectGetRequestADM = ProjectGetRequestADM(projectIri) - private val expectedResponseSuccess: ProjectGetResponseADM = ProjectGetResponseADM( - ProjectADM( - "id", - "shortname", - "shortcode", - None, - List(StringLiteralV2("description")), - List.empty, - None, - List.empty, - status = false, - selfjoin = false - ) - ) - - // Expectations and layers for ActorToZioBridge mock - private val commonLayers: URLayer[ActorToZioBridge, ProjectsRouteZ] = - ZLayer.makeSome[ActorToZioBridge, ProjectsRouteZ](AppConfig.test, ProjectsRouteZ.layer, RestProjectsService.layer) - - private val expectMessageToProjectResponderADM: ULayer[ActorToZioBridge] = - ActorToZioBridgeMock.AskAppActor - .of[ProjectGetResponseADM] - .apply(Assertion.equalTo(expectedRequestSuccess), Expectation.value(expectedResponseSuccess)) - .toLayer - private val expectNoInteractionWithProjectsResponderADM = ActorToZioBridgeMock.empty - - val spec: Spec[Any, Serializable] = - suite("ProjectsRouteZSpec")( - test("given valid project iri should respond with success") { - val urlWithValidIri = URL.empty.setPath(basePath / validIriEncoded) - for { - route <- systemUnderTest - actual <- route.apply(Request(url = urlWithValidIri)).flatMap(_.body.asString) - } yield assertTrue(actual == expectedResponseSuccess.toJsValue.toString()) - }.provide(commonLayers, expectMessageToProjectResponderADM), - test("given invalid project iri but no authentication should respond with bad request") { - val urlWithInvalidIri = URL.empty.setPath(basePath / "invalid") - for { - route <- systemUnderTest - actual <- route.apply(Request(url = urlWithInvalidIri)).map(_.status) - } yield assertTrue(actual == Status.BadRequest) - }.provide(commonLayers, expectNoInteractionWithProjectsResponderADM) - ) -} 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 new file mode 100644 index 0000000000..3eff481517 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectsRouteZSpec.scala @@ -0,0 +1,141 @@ +package org.knora.webapi.routing.admin + +import zhttp.http._ +import zio._ +import zio.mock.Expectation +import zio.test.ZIOSpecDefault +import zio.test._ + +import java.net.URLEncoder + +import org.knora.webapi.config.AppConfig +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM +import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 +import org.knora.webapi.responders.ActorToZioBridge +import org.knora.webapi.responders.ActorToZioBridgeMock +import org.knora.webapi.responders.admin.RestProjectsService + +object ProjectRouteZSpec extends ZIOSpecDefault { + + private val systemUnderTest: URIO[ProjectsRouteZ, HttpApp[Any, Nothing]] = ZIO.service[ProjectsRouteZ].map(_.route) + + // Test data + private val projectIri: ProjectIdentifierADM.IriIdentifier = + ProjectIdentifierADM.IriIdentifier + .fromString("http://rdfh.ch/projects/0001") + .getOrElse(throw new IllegalArgumentException()) + + private val projectShortname: ProjectIdentifierADM.ShortnameIdentifier = + ProjectIdentifierADM.ShortnameIdentifier + .fromString("shortname") + .getOrElse(throw new IllegalArgumentException()) + + private val projectShortcode: ProjectIdentifierADM.ShortcodeIdentifier = + ProjectIdentifierADM.ShortcodeIdentifier + .fromString("AB12") + .getOrElse(throw new IllegalArgumentException()) + + private val basePathIri: Path = !! / "admin" / "projects" / "iri" + private val basePathShortname: Path = !! / "admin" / "projects" / "shortname" + private val basePathShortcode: Path = !! / "admin" / "projects" / "shortcode" + private val validIriUrlEncoded: String = URLEncoder.encode(projectIri.value.value, "utf-8") + private val validShortname: String = "shortname" + private val validShortcode: String = "AB12" + private val expectedRequestSuccessIri: ProjectGetRequestADM = ProjectGetRequestADM(projectIri) + private val expectedRequestSuccessShortname: ProjectGetRequestADM = ProjectGetRequestADM(projectShortname) + private val expectedRequestSuccessShortcode: ProjectGetRequestADM = ProjectGetRequestADM(projectShortcode) + private val expectedResponseSuccess: ProjectGetResponseADM = + ProjectGetResponseADM( + ProjectADM( + id = "id", + shortname = "shortname", + shortcode = "AB12", + longname = None, + description = List(StringLiteralV2("description")), + keywords = List.empty, + logo = None, + ontologies = List.empty, + status = false, + selfjoin = false + ) + ) + + // Expectations and layers for ActorToZioBridge mock + private val commonLayers: URLayer[ActorToZioBridge, ProjectsRouteZ] = + ZLayer.makeSome[ActorToZioBridge, ProjectsRouteZ](AppConfig.test, ProjectsRouteZ.layer, RestProjectsService.layer) + + private val expectMessageToProjectResponderIri: ULayer[ActorToZioBridge] = + ActorToZioBridgeMock.AskAppActor + .of[ProjectGetResponseADM] + .apply(Assertion.equalTo(expectedRequestSuccessIri), Expectation.value(expectedResponseSuccess)) + .toLayer + + private val expectMessageToProjectResponderShortname: ULayer[ActorToZioBridge] = + ActorToZioBridgeMock.AskAppActor + .of[ProjectGetResponseADM] + .apply(Assertion.equalTo(expectedRequestSuccessShortname), Expectation.value(expectedResponseSuccess)) + .toLayer + + private val expectMessageToProjectResponderShortcode: ULayer[ActorToZioBridge] = + ActorToZioBridgeMock.AskAppActor + .of[ProjectGetResponseADM] + .apply(Assertion.equalTo(expectedRequestSuccessShortcode), Expectation.value(expectedResponseSuccess)) + .toLayer + + private val expectNoInteractionWithProjectsResponderADM = ActorToZioBridgeMock.empty + + val spec: Spec[Any, Serializable] = + suite("ProjectsRouteZSpec")( + suite("get project by IRI")( + test("given valid project iri should respond with success") { + val urlWithValidIri = URL.empty.setPath(basePathIri / validIriUrlEncoded) + for { + route <- systemUnderTest + actual <- route.apply(Request(url = urlWithValidIri)).flatMap(_.body.asString) + } yield assertTrue(actual == expectedResponseSuccess.toJsValue.toString()) + }.provide(commonLayers, expectMessageToProjectResponderIri), + test("given invalid project iri should respond with bad request") { + val urlWithInvalidIri = URL.empty.setPath(basePathIri / "invalid") + for { + route <- systemUnderTest + actual <- route.apply(Request(url = urlWithInvalidIri)).map(_.status) + } yield assertTrue(actual == Status.BadRequest) + }.provide(commonLayers, expectNoInteractionWithProjectsResponderADM) + ), + suite("get project by shortname")( + test("given valid project shortname should respond with success") { + val urlWithValidShortname = URL.empty.setPath(basePathShortname / validShortname) + for { + route <- systemUnderTest + actual <- route.apply(Request(url = urlWithValidShortname)).flatMap(_.body.asString) + } yield assertTrue(actual == expectedResponseSuccess.toJsValue.toString()) + }.provide(commonLayers, expectMessageToProjectResponderShortname), + test("given invalid project shortname should respond with bad request") { + val urlWithInvalidShortname = URL.empty.setPath(basePathShortname / "123") + for { + route <- systemUnderTest + actual <- route.apply(Request(url = urlWithInvalidShortname)).map(_.status) + } yield assertTrue(actual == Status.BadRequest) + }.provide(commonLayers, expectNoInteractionWithProjectsResponderADM) + ), + suite("get project by shortcode")( + test("given valid project shortcode should respond with success") { + val urlWithValidShortcode = URL.empty.setPath(basePathShortcode / validShortcode) + for { + route <- systemUnderTest + actual <- route.apply(Request(url = urlWithValidShortcode)).flatMap(_.body.asString) + } yield assertTrue(actual == expectedResponseSuccess.toJsValue.toString()) + }.provide(commonLayers, expectMessageToProjectResponderShortcode), + test("given invalid project shortcode should respond with bad request") { + val urlWithInvalidShortcode = URL.empty.setPath(basePathShortcode / "invalid") + for { + route <- systemUnderTest + actual <- route.apply(Request(url = urlWithInvalidShortcode)).map(_.status) + } yield assertTrue(actual == Status.BadRequest) + }.provide(commonLayers, expectNoInteractionWithProjectsResponderADM) + ) + ) +}