Skip to content

Commit

Permalink
Integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dantb committed Sep 27, 2023
1 parent 23266d9 commit e01c588
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ 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 ch.epfl.bluebrain.nexus.delta.sourcing.{MD5, Transactors}
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.{ConnectionIO, Update}
import doobie.implicits._
import doobie.util.update.Update0
import monix.bio.IO
import monix.bio.UIO

Expand All @@ -29,25 +30,31 @@ object OrganizationDeleter {
for {
orgIsEmpty <- orgIsEmpty(id)
_ <- if (orgIsEmpty) log(s"Deleting empty organization $id") *> deleteAll(id)
else IO.raiseError[OrganizationNonEmpty](OrganizationNonEmpty(id))
else IO.raiseError(OrganizationNonEmpty(id))
} yield ()

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
(for {
_ <- List("scoped_events", "scoped_states").traverse(dropPartition(id, _)).void
_ <- List("global_events", "global_states").traverse(deleteGlobalData(id, _))
} yield ()).transact(xas.write).void.hideErrors

def deleteFromTable(id: Label, table: String): ConnectionIO[Unit] =
def dropPartition(id: Label, table: String): ConnectionIO[Unit] =
Update0(s"DROP TABLE IF EXISTS ${table}_${MD5.hash(id.value)}", None).run.void

def deleteGlobalData(id: Label, table: String): ConnectionIO[Unit] =
for {
_ <- delete(Organizations.encodeId(id), Organizations.entityType, table)
_ <- delete(Acls.encodeId(AclAddress.fromOrg(id)), Acls.entityType, table)
} yield ()

def delete(id: IriOrBNode.Iri, tpe: EntityType, table: String): ConnectionIO[Unit] =
sql"""DELETE FROM $table WHERE type = $tpe AND id = $id""".update.run.void
Update[(EntityType, IriOrBNode.Iri)](s"DELETE FROM $table WHERE" ++ " type = ? AND id = ?").run((tpe, id)).void

def orgIsEmpty(id: Label): UIO[Boolean] =
sql"""SELECT type from scoped_events WHERE org = $id LIMIT 1"""
sql"SELECT type from scoped_events WHERE org = $id LIMIT 1"
.query[Label]
.option
.map(_.isEmpty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,6 @@ trait ConfigFixtures {
1.second,
RetryStrategyConfig.AlwaysGiveUp
)

def logConfig: EventLogConfig = EventLogConfig(queryConfig, 10.seconds)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package ch.epfl.bluebrain.nexus.delta.sdk.projects

import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF
import ch.epfl.bluebrain.nexus.delta.sdk.ConfigFixtures
import ch.epfl.bluebrain.nexus.delta.sdk.generators.ProjectGen.defaultApiMappings
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.{OrganizationDeleter, Organizations}
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.{Organization, OrganizationCommand}
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection.{OrganizationNonEmpty, OrganizationNotFound}
import ch.epfl.bluebrain.nexus.delta.sdk.projects.Projects.FetchOrganization
import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ProjectRejection.WrappedOrganizationRejection
import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.{ApiMappings, ProjectFields}
import ch.epfl.bluebrain.nexus.delta.sourcing.{GlobalEventLog, MD5}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject
import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Identity, Label, ProjectRef}
import ch.epfl.bluebrain.nexus.delta.sourcing.offset.Offset
import ch.epfl.bluebrain.nexus.testkit.IOFixedClock
import ch.epfl.bluebrain.nexus.testkit.bio.BioSuite
import doobie.implicits._
import doobie.util.update.Update0
import monix.bio.{IO, Task, UIO}
import munit.AnyFixture

import java.util.UUID

class OrganizationDeleterSpec extends BioSuite with IOFixedClock with ConfigFixtures {

private val org = Label.unsafe("org")
private val orgUuid = UUID.randomUUID()

private def fetchOrg: FetchOrganization = {
case `org` => UIO.pure(Organization(org, orgUuid, None))
case other => IO.raiseError(WrappedOrganizationRejection(OrganizationNotFound(other)))
}

private val config = ProjectsConfig(eventLogConfig, pagination, cacheConfig, deletionConfig)
private val projectFixture = ProjectsFixture.init(fetchOrg, defaultApiMappings, config)
override def munitFixtures: Seq[AnyFixture[_]] = List(projectFixture)
private lazy val (xas, projects) = projectFixture()
private lazy val orgDeleter = OrganizationDeleter(xas)
private val proj1 = ProjectRef.unsafe("org", "myproj")
private val fields = ProjectFields(None, ApiMappings.empty, None, None)
private lazy val orgLog = GlobalEventLog(Organizations.definition, logConfig, xas)

implicit val subject: Subject = Identity.User("Bob", Label.unsafe("realm"))
implicit val uuidF: UUIDF = UUIDF.fixed(orgUuid)

test("Fail when trying to delete a non-empty organization") {
val arrange: IO[Any, Unit] = for {
_ <- truncateTables
_ <- orgLog.evaluate(org, OrganizationCommand.CreateOrganization(org, None, Identity.Anonymous))
_ <- projects.create(proj1, fields)
} yield ()

val act: UIO[Either[OrganizationNonEmpty, Unit]] =
orgDeleter.deleteIfEmpty(org).attempt

def assert(result: Either[OrganizationNonEmpty, Unit]): IO[Any, Unit] = for {
eventPartitions <- queryPartitions("scoped_events")
statePartitions <- queryPartitions("scoped_states")
fetchedProject <- projects.fetch(proj1)
globalOrgEvent <- orgLog.currentEvents(Offset.Start).compile.to(List).map(_.map(_.value.uuid))
globalOrgState <- orgLog.currentStates(Offset.Start).compile.to(List).map(_.map(_.value.uuid))
} yield {
assertEquals(result, Left(OrganizationNonEmpty(org)))
val orgPartition = MD5.hash(org.value)
assertEquals(eventPartitions.headOption, Some(s"scoped_events_$orgPartition"))
assertEquals(statePartitions.headOption, Some(s"scoped_states_$orgPartition"))
assertEquals(fetchedProject.value.ref, proj1)
assertEquals(globalOrgState, List(orgUuid))
assertEquals(globalOrgEvent, List(orgUuid))
}

arrange >> act >>= assert
}

test("Successfully delete an empty organization") {
val arrange: IO[Any, Unit] = for {
_ <- truncateTables
_ <- orgLog.evaluate(org, OrganizationCommand.CreateOrganization(org, None, Identity.Anonymous))
} yield ()

val act: UIO[Either[OrganizationNonEmpty, Unit]] =
orgDeleter.deleteIfEmpty(org).attempt

def assert(result: Either[OrganizationNonEmpty, Unit]): IO[Any, Unit] = for {
globalEventsDeleted <- orgLog.currentEvents(Offset.Start).compile.to(List).map(_.isEmpty)
globalStateDeleted <- orgLog.currentStates(Offset.Start).compile.to(List).map(_.isEmpty)
eventPartitionDeleted <- queryPartitions("scoped_events").map(_.isEmpty)
statePartitionDeleted <- queryPartitions("scoped_states").map(_.isEmpty)
} yield {
assertEquals(result, Right(()))
assertEquals(eventPartitionDeleted, true)
assertEquals(statePartitionDeleted, true)
assertEquals(globalStateDeleted, true)
assertEquals(globalEventsDeleted, true)
}

arrange >> act >>= assert
}

def truncateTables: UIO[Unit] =
List("global_events", "global_states", "scoped_events", "scoped_states").traverse(t =>
Update0(s"DELETE FROM $t", None).run.transact(xas.write)
).void.hideErrors

def queryPartitions(table: String): Task[List[String]] =
sql"""
SELECT inhrelid::regclass AS child
FROM pg_catalog.pg_inherits
WHERE inhparent = $table::regclass
""".query[String].to[List].transact(xas.read)
}
Original file line number Diff line number Diff line change
Expand Up @@ -292,4 +292,53 @@ class OrgsSpec extends BaseSpec with EitherValuable {
} yield succeed
}
}

"deleting an organization" should {
val id = genId()
val name = genString()

"add orgs/create permissions for user" in {
aclDsl.addPermission(
"/",
Leela,
Organizations.Create
)
}

"create the organization" in {
adminDsl.createOrganization(
id,
name,
Leela
)
}

"fail when wrong revision is provided" in {
deltaClient.delete[Json](s"/orgs/$id?rev=4", Leela) { (json, response) =>
response.status shouldEqual StatusCodes.Conflict
json shouldEqual jsonContentOf("/admin/errors/org-incorrect-revision.json")
}
}

"fail when revision is not provided" in {
deltaClient.delete[Json](s"/orgs/$id", Leela) { (json, response) =>
response.status shouldEqual StatusCodes.BadRequest
json shouldEqual jsonContentOf("/admin/errors/rev-not-provided.json")
}
}

"succeed if organization exists" in {
for {
_ <- adminDsl.deprecateOrganization(id, Leela)
_ <- deltaClient.get[Json](s"/orgs/$id", Leela) { (json, response) =>
response.status shouldEqual StatusCodes.OK
admin.validate(json, "Organization", "orgs", id, name, 2, id, deprecated = true)
}
_ <- deltaClient.get[Json](s"/orgs/$id?rev=1", Leela) { (json, response) =>
response.status shouldEqual StatusCodes.OK
admin.validate(json, "Organization", "orgs", id, name, 1, id)
}
} yield succeed
}
}
}

0 comments on commit e01c588

Please sign in to comment.