From 23266d9ba4849455bf2579dc4b903a7c4c1cbb23 Mon Sep 17 00:00:00 2001 From: dantb Date: Wed, 27 Sep 2023 11:50:43 +0200 Subject: [PATCH] Unit tests for route, refactor errors --- .../delta/routes/OrganizationsRoutes.scala | 38 +++++++++---------- .../routes/OrganizationsRoutesSpec.scala | 38 +++++++++++++++++-- .../organizations/OrganizationDeleter.scala | 36 ++++++++++-------- .../model/OrganizationRejection.scala | 1 + 4 files changed, 76 insertions(+), 37 deletions(-) diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutes.scala index 2277c2d9f3..fedb07ef4e 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutes.scala @@ -1,8 +1,9 @@ package ch.epfl.bluebrain.nexus.delta.routes import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.server.Directive1 import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.{Directive1, Route} +import akka.http.scaladsl.server.Route import cats.implicits._ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder @@ -11,18 +12,22 @@ import ch.epfl.bluebrain.nexus.delta.routes.OrganizationsRoutes.OrganizationInpu import ch.epfl.bluebrain.nexus.delta.sdk.OrganizationResource import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck import ch.epfl.bluebrain.nexus.delta.sdk.circe.CirceUnmarshalling +import ch.epfl.bluebrain.nexus.delta.sdk.directives.AuthDirectives import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._ -import ch.epfl.bluebrain.nexus.delta.sdk.directives.{AuthDirectives, DeltaSchemeDirectives} +import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaSchemeDirectives import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri +import ch.epfl.bluebrain.nexus.delta.sdk.model.search.PaginationConfig import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams.OrganizationSearchParams +import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults._ -import ch.epfl.bluebrain.nexus.delta.sdk.model.search.{PaginationConfig, SearchResults} +import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationDeleter import ch.epfl.bluebrain.nexus.delta.sdk.organizations.Organizations +import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.Organization +import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection._ -import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.{Organization, OrganizationRejection} import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions._ import io.circe.Decoder import io.circe.generic.extras.Configuration @@ -31,7 +36,6 @@ import kamon.instrumentation.akka.http.TracingDirectives.operationName import monix.execution.Scheduler import scala.annotation.nowarn -import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationDeleter /** * The organization routes. @@ -117,21 +121,17 @@ final class OrganizationsRoutes( }, // Deprecate organization delete { - authorizeFor(id, orgs.write).apply { - parameter("rev".as[Int].?, "prune".?(false)) { - case (_, true) => - authorizeFor(id, orgs.delete).apply { - emit( - orgDeleter - .deleteIfEmpty(id) - .leftMap[OrganizationRejection](_ => OrganizationRejection.OrganizationNonEmpty(id)) - ) - } - case (Some(rev), false) => + parameter("rev".as[Int].?, "prune".?(false)) { + case (_, true) => + authorizeFor(id, orgs.delete).apply { + emit(orgDeleter.deleteIfEmpty(id).leftWiden[OrganizationRejection]) + } + case (Some(rev), false) => + authorizeFor(id, orgs.write).apply { emit(organizations.deprecate(id, rev).mapValue(_.metadata)) - case (None, false) => - complete(StatusCodes.BadRequest) - } + } + case (None, false) => + complete(StatusCodes.BadRequest) } } ) diff --git a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutesSpec.scala b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutesSpec.scala index 887e2162fc..75b655f381 100644 --- a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutesSpec.scala +++ b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutesSpec.scala @@ -14,7 +14,9 @@ import ch.epfl.bluebrain.nexus.delta.sdk.identities.IdentitiesDummy import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationsConfig +import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationDeleter import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationsImpl +import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection.OrganizationNonEmpty import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.events import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.{orgs => orgsPermissions} import ch.epfl.bluebrain.nexus.delta.sdk.projects.OwnerPermissionsScopeInitialization @@ -26,9 +28,10 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label import ch.epfl.bluebrain.nexus.testkit.bio.IOFromMap import io.circe.Json +import monix.bio.IO import java.util.UUID -import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationDeleter + class OrganizationsRoutesSpec extends BaseRouteSpec with IOFromMap { @@ -46,8 +49,10 @@ class OrganizationsRoutesSpec extends BaseRouteSpec with IOFromMap { Set(orgsPermissions.write, orgsPermissions.read) ) - private lazy val orgs = OrganizationsImpl(Set(aopd), config, xas) - private lazy val orgDeleter = OrganizationDeleter(xas) + private lazy val orgs = OrganizationsImpl(Set(aopd), config, xas) + private lazy val orgDeleter: OrganizationDeleter = id => + if (id == org1.label) IO.raiseError(OrganizationNonEmpty(id)) + else IO.unit private val caller = Caller(alice, Set(alice, Anonymous, Authenticated(realm), Group("group", realm))) @@ -222,6 +227,33 @@ class OrganizationsRoutesSpec extends BaseRouteSpec with IOFromMap { } } + "fail to deprecate an organization if the revision is omitted" in { + Delete("/v1/orgs/org2") ~> addCredentials(OAuth2BearerToken("alice")) ~> routes ~> check { + status shouldEqual StatusCodes.BadRequest + } + } + + "delete an organization" in { + aclChecker.append(AclAddress.fromOrg(org2.label), caller.subject -> Set(orgsPermissions.delete)).accepted + Delete("/v1/orgs/org2?prune=true") ~> addCredentials(OAuth2BearerToken("alice")) ~> routes ~> check { + status shouldEqual StatusCodes.OK + } + } + + "fail when trying to delete a non-empty organization" in { + aclChecker.append(AclAddress.fromOrg(org1.label), caller.subject -> Set(orgsPermissions.delete)).accepted + Delete("/v1/orgs/org1?prune=true") ~> addCredentials(OAuth2BearerToken("alice")) ~> routes ~> check { + status shouldEqual StatusCodes.Conflict + } + } + + "fail to delete an organization without organizations/delete permission" in { + aclChecker.subtract(AclAddress.fromOrg(org2.label), caller.subject -> Set(orgsPermissions.delete)).accepted + Delete("/v1/orgs/org2?prune=true") ~> addCredentials(OAuth2BearerToken("alice")) ~> routes ~> check { + status shouldEqual StatusCodes.Forbidden + } + } + "fail fetch an organization without organizations/read permission" in { aclChecker.delete(Label.unsafe("org1")).accepted forAll( diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationDeleter.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationDeleter.scala index 0f7aabf431..07b1664910 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationDeleter.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/OrganizationDeleter.scala @@ -1,36 +1,41 @@ package ch.epfl.bluebrain.nexus.delta.sdk.organizations -import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclAddress -import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label - -import ch.epfl.bluebrain.nexus.delta.sdk.acls.Acls -import ch.epfl.bluebrain.nexus.delta.sourcing.model.EntityType +import cats.syntax.all._ import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode +import ch.epfl.bluebrain.nexus.delta.sdk.acls.Acls +import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclAddress +import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection.OrganizationNonEmpty import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors - -import cats.syntax.all._ import ch.epfl.bluebrain.nexus.delta.sourcing.implicits._ +import ch.epfl.bluebrain.nexus.delta.sourcing.model.EntityType +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label +import com.typesafe.scalalogging.Logger import doobie.ConnectionIO import doobie.implicits._ -import monix.bio.Task +import monix.bio.IO +import monix.bio.UIO trait OrganizationDeleter { - def deleteIfEmpty(id: Label): Task[Unit] + def deleteIfEmpty(id: Label): IO[OrganizationNonEmpty, Unit] } object OrganizationDeleter { def apply(xas: Transactors): OrganizationDeleter = new OrganizationDeleter { - def deleteIfEmpty(id: Label): Task[Unit] = + private val logger: Logger = Logger[OrganizationDeleter] + + def deleteIfEmpty(id: Label): IO[OrganizationNonEmpty, Unit] = for { orgIsEmpty <- orgIsEmpty(id) - _ <- if (orgIsEmpty) deleteAll(id) - else Task.raiseError(new Exception(s"Cannot delete non-empty organization $id")) + _ <- if (orgIsEmpty) log(s"Deleting empty organization $id") *> deleteAll(id) + else IO.raiseError[OrganizationNonEmpty](OrganizationNonEmpty(id)) } yield () - def deleteAll(id: Label): Task[Unit] = - List("global_events", "global_states").traverse(deleteFromTable(id, _)).transact(xas.write).void + def log(msg: String): UIO[Unit] = UIO.delay(logger.info(msg)) + + def deleteAll(id: Label): UIO[Unit] = + List("global_events", "global_states").traverse(deleteFromTable(id, _)).transact(xas.write).void.hideErrors def deleteFromTable(id: Label, table: String): ConnectionIO[Unit] = for { @@ -41,12 +46,13 @@ object OrganizationDeleter { def delete(id: IriOrBNode.Iri, tpe: EntityType, table: String): ConnectionIO[Unit] = sql"""DELETE FROM $table WHERE type = $tpe AND id = $id""".update.run.void - def orgIsEmpty(id: Label): Task[Boolean] = + def orgIsEmpty(id: Label): UIO[Boolean] = sql"""SELECT type from scoped_events WHERE org = $id LIMIT 1""" .query[Label] .option .map(_.isEmpty) .transact(xas.read) + .hideErrors } } diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/model/OrganizationRejection.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/model/OrganizationRejection.scala index af81c8fae6..76ba67d9d3 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/model/OrganizationRejection.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/organizations/model/OrganizationRejection.scala @@ -117,6 +117,7 @@ object OrganizationRejection { case OrganizationRejection.OrganizationNotFound(_) => StatusCodes.NotFound case OrganizationRejection.OrganizationAlreadyExists(_) => StatusCodes.Conflict case OrganizationRejection.IncorrectRev(_, _) => StatusCodes.Conflict + case OrganizationRejection.OrganizationNonEmpty(_) => StatusCodes.Conflict case OrganizationRejection.RevisionNotFound(_, _) => StatusCodes.NotFound case _ => StatusCodes.BadRequest }