diff --git a/delta/app/src/main/resources/app.conf b/delta/app/src/main/resources/app.conf index 4bbb20f559..1417769624 100644 --- a/delta/app/src/main/resources/app.conf +++ b/delta/app/src/main/resources/app.conf @@ -239,8 +239,6 @@ app { resolvers { # the resolvers event-log configuration event-log = ${app.defaults.event-log} - # the resolvers pagination config - pagination = ${app.defaults.pagination} defaults = { # the name of the default resolver diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala index 7fecca3bab..95826f8a29 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala @@ -1,17 +1,20 @@ package ch.epfl.bluebrain.nexus.delta.routes import akka.http.scaladsl.model.StatusCodes.Created +import akka.http.scaladsl.model.{StatusCode, StatusCodes} import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ +import cats.effect.IO import cats.implicits._ +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, schemas} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering import ch.epfl.bluebrain.nexus.delta.sdk._ import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck +import ch.epfl.bluebrain.nexus.delta.sdk.ce.DeltaDirectives._ import ch.epfl.bluebrain.nexus.delta.sdk.circe.CirceUnmarshalling -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.fusion.FusionConfig import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities @@ -19,9 +22,6 @@ 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.marshalling.RdfMarshalling import ch.epfl.bluebrain.nexus.delta.sdk.model.routes.Tag -import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams.ResolverSearchParams -import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults.searchResultsJsonLdEncoder -import ch.epfl.bluebrain.nexus.delta.sdk.model.search.{PaginationConfig, SearchResults} import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegment, IdSegmentRef, ResourceF} import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.resolvers.{read => Read, write => Write} @@ -29,10 +29,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.Resol import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{MultiResolutionResult, Resolver, ResolverRejection} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.{MultiResolution, Resolvers} import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef -import io.circe.Json -import kamon.instrumentation.akka.http.TracingDirectives.operationName -import monix.bio.IO -import monix.execution.Scheduler +import io.circe.{Json, Printer} /** * The resolver routes @@ -45,7 +42,7 @@ import monix.execution.Scheduler * the resolvers module * @param schemeDirectives * directives related to orgs and projects - * @param index + * @param indexAction * the indexing action on write operations */ final class ResolversRoutes( @@ -54,11 +51,9 @@ final class ResolversRoutes( resolvers: Resolvers, multiResolution: MultiResolution, schemeDirectives: DeltaSchemeDirectives, - index: IndexingAction.Execute[Resolver] + indexAction: IndexingAction.Execute[Resolver] )(implicit baseUri: BaseUri, - paginationConfig: PaginationConfig, - s: Scheduler, cr: RemoteContextResolution, ordering: JsonKeyOrdering, fusionConfig: FusionConfig @@ -66,138 +61,104 @@ final class ResolversRoutes( with CirceUnmarshalling with RdfMarshalling { - import baseUri.prefixSegment import schemeDirectives._ implicit private val resourceFUnitJsonLdEncoder: JsonLdEncoder[ResourceF[Unit]] = ResourceF.resourceFAJsonLdEncoder(ContextValue(contexts.resolversMetadata)) - private def resolverSearchParams(implicit projectRef: ProjectRef, caller: Caller): Directive1[ResolverSearchParams] = - (searchParams & types).tmap { case (deprecated, rev, createdBy, updatedBy, types) => - val fetchAllCached = aclCheck.fetchAll.memoizeOnSuccess - ResolverSearchParams( - Some(projectRef), - deprecated, - rev, - createdBy, - updatedBy, - types, - resolver => aclCheck.authorizeFor(resolver.project, Read, fetchAllCached) - ) - } + private def emitFetch(io: IO[ResolverResource]): Route = + emit(io.attemptNarrow[ResolverRejection].rejectOn[ResolverNotFound]) + private def emitMetadata(statusCode: StatusCode, io: IO[ResolverResource]): Route = + emit(statusCode, io.map(_.void).attemptNarrow[ResolverRejection]) + + private def emitMetadata(io: IO[ResolverResource]): Route = emitMetadata(StatusCodes.OK, io) + + private def emitMetadataOrReject(io: IO[ResolverResource]): Route = + emit(io.map(_.void).attemptNarrow[ResolverRejection].rejectOn[ResolverNotFound]) + + private def emitSource(io: IO[ResolverResource]): Route = { + implicit val source: Printer = sourcePrinter + emit(io.map(_.value.source).attemptNarrow[ResolverRejection].rejectOn[ResolverNotFound]) + } + + private def emitTags(io: IO[ResolverResource]): Route = + emit(io.map(_.value.tags).attemptNarrow[ResolverRejection].rejectOn[ResolverNotFound]) def routes: Route = (baseUriPrefix(baseUri.prefix) & replaceUri("resolvers", schemas.resolvers)) { pathPrefix("resolvers") { extractCaller { implicit caller => - resolveProjectRef.apply { implicit ref => - val projectAddress = ref - val authorizeRead = authorizeFor(projectAddress, Read) - val authorizeWrite = authorizeFor(projectAddress, Write) + (resolveProjectRef & indexingMode) { (ref, mode) => + def index(resolver: ResolverResource): IO[Unit] = indexAction(resolver.value.project, resolver, mode) + val authorizeRead = authorizeFor(ref, Read) + val authorizeWrite = authorizeFor(ref, Write) concat( - (pathEndOrSingleSlash & operationName(s"$prefixSegment/resolvers/{org}/{project}")) { + pathEndOrSingleSlash { // Create a resolver without an id segment - (post & noParameter("rev") & entity(as[Json]) & indexingMode) { (payload, mode) => + (post & noParameter("rev") & entity(as[Json])) { payload => authorizeWrite { - emit(Created, resolvers.create(ref, payload).tapEval(index(ref, _, mode)).map(_.void)) - } - } - }, - (pathPrefix("caches") & pathEndOrSingleSlash) { - operationName(s"$prefixSegment/resolvers/{org}/{project}/caches") { - // List resolvers in cache - (get & extractUri & fromPaginated & resolverSearchParams & sort[Resolver]) { - (uri, pagination, params, order) => - authorizeRead { - implicit val searchJsonLdEncoder: JsonLdEncoder[SearchResults[ResolverResource]] = - searchResultsJsonLdEncoder(Resolver.context, pagination, uri) - - emit(resolvers.list(pagination, params, order).widen[SearchResults[ResolverResource]]) - } + emitMetadata(Created, resolvers.create(ref, payload).flatTap(index)) } } }, - (idSegment & indexingMode) { (id, mode) => + idSegment { id => concat( pathEndOrSingleSlash { - operationName(s"$prefixSegment/resolvers/{org}/{project}/{id}") { - concat( - put { - authorizeWrite { - (parameter("rev".as[Int].?) & pathEndOrSingleSlash & entity(as[Json])) { - case (None, payload) => - // Create a resolver with an id segment - emit( - Created, - resolvers.create(id, ref, payload).tapEval(index(ref, _, mode)).map(_.void) - ) - case (Some(rev), payload) => - // Update a resolver - emit(resolvers.update(id, ref, rev, payload).tapEval(index(ref, _, mode)).map(_.void)) - } + concat( + put { + authorizeWrite { + (parameter("rev".as[Int].?) & pathEndOrSingleSlash & entity(as[Json])) { + case (None, payload) => + // Create a resolver with an id segment + emitMetadata(Created, resolvers.create(id, ref, payload).flatTap(index)) + case (Some(rev), payload) => + // Update a resolver + emitMetadata(resolvers.update(id, ref, rev, payload).flatTap(index)) } - }, - (delete & parameter("rev".as[Int])) { rev => - authorizeWrite { - // Deprecate a resolver - emit( - resolvers - .deprecate(id, ref, rev) - .tapEval(index(ref, _, mode)) - .map(_.void) - .rejectOn[ResolverNotFound] - ) - } - }, - // Fetches a resolver - (get & idSegmentRef(id)) { id => - emitOrFusionRedirect( - ref, - id, - authorizeRead { - emit(resolvers.fetch(id, ref).rejectOn[ResolverNotFound]) - } - ) } - ) - } + }, + (delete & parameter("rev".as[Int])) { rev => + authorizeWrite { + // Deprecate a resolver + emitMetadataOrReject(resolvers.deprecate(id, ref, rev).flatTap(index)) + } + }, + // Fetches a resolver + (get & idSegmentRef(id)) { id => + emitOrFusionRedirect( + ref, + id, + authorizeRead { + emitFetch(resolvers.fetch(id, ref)) + } + ) + } + ) }, // Fetches a resolver original source (pathPrefix("source") & get & pathEndOrSingleSlash & idSegmentRef(id) & authorizeRead) { id => - operationName(s"$prefixSegment/resolvers/{org}/{project}/{id}/source") { - emit(resolvers.fetch(id, ref).map(_.value.source).rejectOn[ResolverNotFound]) - } + emitSource(resolvers.fetch(id, ref)) }, // Tags (pathPrefix("tags") & pathEndOrSingleSlash) { - operationName(s"$prefixSegment/resolvers/{org}/{project}/{id}/tags") { - concat( - // Fetch a resolver tags - (get & idSegmentRef(id) & authorizeRead) { id => - emit(resolvers.fetch(id, ref).map(_.value.tags).rejectOn[ResolverNotFound]) - }, - // Tag a resolver - (post & parameter("rev".as[Int])) { rev => - authorizeWrite { - entity(as[Tag]) { case Tag(tagRev, tag) => - emit( - Created, - resolvers - .tag(id, ref, tag, tagRev, rev) - .tapEval(index(ref, _, mode)) - .map(_.void) - ) - } + concat( + // Fetch a resolver tags + (get & idSegmentRef(id) & authorizeRead) { id => + emitTags(resolvers.fetch(id, ref)) + }, + // Tag a resolver + (post & parameter("rev".as[Int])) { rev => + authorizeWrite { + entity(as[Tag]) { case Tag(tagRev, tag) => + emitMetadata(Created, resolvers.tag(id, ref, tag, tagRev, rev).flatTap(index)) } } - ) - } + } + ) }, // Fetch a resource using a resolver (idSegmentRef & pathEndOrSingleSlash) { resourceIdRef => - operationName(s"$prefixSegment/resolvers/{org}/{project}/{id}/{resourceId}") { - resolve(resourceIdRef, ref, underscoreToOption(id)) - } + resolve(resourceIdRef, ref, underscoreToOption(id)) } ) } @@ -212,11 +173,11 @@ final class ResolversRoutes( ): Route = authorizeFor(projectRef, Permissions.resources.read).apply { parameter("showReport".as[Boolean].withDefault(default = false)) { showReport => - def emitResult[R: JsonLdEncoder](io: IO[ResolverRejection, MultiResolutionResult[R]]) = + def emitResult[R: JsonLdEncoder](io: IO[MultiResolutionResult[R]]) = if (showReport) - emit(io.map(_.report)) + emit(io.map(_.report).attemptNarrow[ResolverRejection]) else - emit(io.map(_.value.jsonLdValue)) + emit(io.map(_.value.jsonLdValue).attemptNarrow[ResolverRejection]) resolverId.fold(emitResult(multiResolution(resourceSegment, projectRef))) { resolverId => emitResult(multiResolution(resourceSegment, projectRef, resolverId)) @@ -241,8 +202,6 @@ object ResolversRoutes { index: IndexingAction.Execute[Resolver] )(implicit baseUri: BaseUri, - s: Scheduler, - paginationConfig: PaginationConfig, cr: RemoteContextResolution, ordering: JsonKeyOrdering, fusionConfig: FusionConfig diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/DeltaModule.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/DeltaModule.scala index ce4b3414cb..a22575c37e 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/DeltaModule.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/DeltaModule.scala @@ -8,7 +8,7 @@ import akka.http.scaladsl.model.headers.Location import akka.http.scaladsl.server.{ExceptionHandler, RejectionHandler, Route} import akka.stream.{Materializer, SystemMaterializer} import cats.data.NonEmptyList -import cats.effect.{Clock, IO, Resource, Sync, Timer} +import cats.effect.{Clock, ContextShift, IO, Resource, Sync, Timer} import ch.epfl.bluebrain.nexus.delta.Main.pluginsMaxPriority import ch.epfl.bluebrain.nexus.delta.config.AppConfig import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF @@ -105,6 +105,7 @@ class DeltaModule(appCfg: AppConfig, config: Config)(implicit classLoader: Class make[Clock[UIO]].from(Clock[UIO]) make[Clock[IO]].from(Clock.create[IO]) make[Timer[IO]].from(IO.timer(ExecutionContext.global)) + make[ContextShift[IO]].from(IO.contextShift(ExecutionContext.global)) make[UUIDF].from(UUIDF.random) make[Scheduler].from(scheduler) make[JsonKeyOrdering].from( diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResolversModule.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResolversModule.scala index c927a46523..a9f0cf6a18 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResolversModule.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResolversModule.scala @@ -1,6 +1,6 @@ package ch.epfl.bluebrain.nexus.delta.wiring -import cats.effect.Clock +import cats.effect.{Clock, IO} import ch.epfl.bluebrain.nexus.delta.Main.pluginsMaxPriority import ch.epfl.bluebrain.nexus.delta.config.AppConfig import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF @@ -26,8 +26,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{Resolver, ResolverEven import ch.epfl.bluebrain.nexus.delta.sdk.sse.SseEncoder import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors import izumi.distage.model.definition.{Id, ModuleDef} -import monix.bio.UIO -import monix.execution.Scheduler /** * Resolvers wiring @@ -42,7 +40,7 @@ object ResolversModule extends ModuleDef { config: AppConfig, xas: Transactors, api: JsonLdApi, - clock: Clock[UIO], + clock: Clock[IO], uuidF: UUIDF ) => ResolversImpl( @@ -68,7 +66,6 @@ object ResolversModule extends ModuleDef { make[ResolversRoutes].from { ( - config: AppConfig, identities: Identities, aclCheck: AclCheck, resolvers: Resolvers, @@ -77,7 +74,6 @@ object ResolversModule extends ModuleDef { shift: Resolver.Shift, multiResolution: MultiResolution, baseUri: BaseUri, - s: Scheduler, cr: RemoteContextResolution @Id("aggregate"), ordering: JsonKeyOrdering, fusionConfig: FusionConfig @@ -91,8 +87,6 @@ object ResolversModule extends ModuleDef { indexingAction(_, _, _)(shift, cr) )( baseUri, - config.resolvers.pagination, - s, cr, ordering, fusionConfig @@ -104,7 +98,7 @@ object ResolversModule extends ModuleDef { many[ScopedEventMetricEncoder[_]].add { ResolverEvent.resolverEventMetricEncoder } make[ResolverScopeInitialization].from { (resolvers: Resolvers, serviceAccount: ServiceAccount, config: AppConfig) => - new ResolverScopeInitialization(resolvers, serviceAccount, config.resolvers.defaults) + ResolverScopeInitialization(resolvers, serviceAccount, config.resolvers.defaults) } many[ScopeInitialization].ref[ResolverScopeInitialization] diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/SchemasModule.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/SchemasModule.scala index a9fcf0a567..550cc08b2a 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/SchemasModule.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/SchemasModule.scala @@ -1,6 +1,6 @@ package ch.epfl.bluebrain.nexus.delta.wiring -import cats.effect.{Clock, IO} +import cats.effect.{Clock, ContextShift, IO} import ch.epfl.bluebrain.nexus.delta.Main.pluginsMaxPriority import ch.epfl.bluebrain.nexus.delta.config.AppConfig import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF @@ -59,9 +59,10 @@ object SchemasModule extends ModuleDef { aclCheck: AclCheck, resolvers: Resolvers, resources: Resources, - schemas: Schemas + schemas: Schemas, + contextShift: ContextShift[IO] ) => - SchemaImports(aclCheck, resolvers, schemas, resources) + SchemaImports(aclCheck, resolvers, schemas, resources)(contextShift) } make[SchemasRoutes].from { diff --git a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutesSpec.scala b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutesSpec.scala index aa0d40da91..3862673d7d 100644 --- a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutesSpec.scala +++ b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutesSpec.scala @@ -4,6 +4,7 @@ import akka.http.scaladsl.model.MediaTypes.`text/html` import akka.http.scaladsl.model.headers.{Accept, Location, OAuth2BearerToken} import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.server.Route +import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.kernel.utils.{UUIDF, UrlUtils} import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, nxv, schema, schemas} @@ -22,7 +23,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings import ch.epfl.bluebrain.nexus.delta.sdk.resolvers._ import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.ProjectContextRejection import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverType.{CrossProject, InProject} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{ResolverRejection, ResolverType, ResourceResolutionReport} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{ResolverRejection, ResolverType} import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.Resource import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec @@ -30,14 +31,14 @@ import ch.epfl.bluebrain.nexus.delta.sdk.{Defaults, IndexingAction} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject} import ch.epfl.bluebrain.nexus.delta.sourcing.model.ResourceRef.{Latest, Revision} import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef} +import ch.epfl.bluebrain.nexus.testkit.ce.IOFixedClock import io.circe.Json import io.circe.syntax._ -import monix.bio.{IO, UIO} import java.util.UUID import java.util.concurrent.atomic.AtomicInteger -class ResolversRoutesSpec extends BaseRouteSpec { +class ResolversRoutesSpec extends BaseRouteSpec with IOFixedClock { private val uuid = UUID.randomUUID() implicit private val uuidF: UUIDF = UUIDF.fixed(uuid) @@ -58,10 +59,7 @@ class ResolversRoutesSpec extends BaseRouteSpec { Caller(bob, Set(bob)) ) - val resolverContextResolution: ResolverContextResolution = new ResolverContextResolution( - rcr, - (_, _, _) => IO.raiseError(ResourceResolutionReport()) - ) + val resolverContextResolution: ResolverContextResolution = ResolverContextResolution(rcr) private val resourceId = nxv + "resource" private val resource = @@ -77,18 +75,18 @@ class ResolversRoutesSpec extends BaseRouteSpec { ) private val resourceFS = SchemaGen.resourceFor(schemaResource) - def fetchResource: (ResourceRef, ProjectRef) => UIO[Option[JsonLdContent[Resource, Nothing]]] = + def fetchResource: (ResourceRef, ProjectRef) => IO[Option[JsonLdContent[Resource, Nothing]]] = (ref: ResourceRef, _: ProjectRef) => ref match { - case Latest(`resourceId`) => UIO.some(JsonLdContent(resourceFR, resourceFR.value.source, None)) - case _ => UIO.none + case Latest(`resourceId`) => IO.pure(Some(JsonLdContent(resourceFR, resourceFR.value.source, None))) + case _ => IO.none } - def fetchSchema: (ResourceRef, ProjectRef) => UIO[Option[JsonLdContent[Schema, Nothing]]] = + def fetchSchema: (ResourceRef, ProjectRef) => IO[Option[JsonLdContent[Schema, Nothing]]] = (ref: ResourceRef, _: ProjectRef) => ref match { - case Revision(_, `schemaId`, 5) => UIO.some(JsonLdContent(resourceFS, resourceFS.value.source, None)) - case _ => UIO.none + case Revision(_, `schemaId`, 5) => IO.pure(Some(JsonLdContent(resourceFS, resourceFS.value.source, None))) + case _ => IO.none } private val defaults = Defaults("resolverName", "resolverDescription") @@ -96,7 +94,7 @@ class ResolversRoutesSpec extends BaseRouteSpec { private lazy val resolvers = ResolversImpl( fetchContext, resolverContextResolution, - ResolversConfig(eventLogConfig, pagination, defaults), + ResolversConfig(eventLogConfig, defaults), xas ) @@ -111,7 +109,7 @@ class ResolversRoutesSpec extends BaseRouteSpec { resolvers, (ref: ResourceRef, project: ProjectRef) => fetchResource(ref, project).flatMap { - case Some(c) => UIO.some(c) + case Some(c) => IO.pure(Some(c)) case None => fetchSchema(ref, project) } ) @@ -447,10 +445,10 @@ class ResolversRoutesSpec extends BaseRouteSpec { def inProject( id: Iri, priority: Int, - rev: Int = 1, - deprecated: Boolean = false, + rev: Int, + deprecated: Boolean, createdBy: Subject = bob, - updatedBy: Subject = bob + updatedBy: Subject ) = resolverMetadata( id, @@ -659,66 +657,6 @@ class ResolversRoutesSpec extends BaseRouteSpec { } } - "listing the resolvers" should { - - def expectedResults(results: Json*): Json = { - val ctx = json"""{"@context": ["${contexts.metadata}", "${contexts.search}", "${contexts.resolvers}"]}""" - Json.obj("_total" -> Json.fromInt(results.size), "_results" -> Json.arr(results: _*)) deepMerge ctx - } - - "return the deprecated resolvers the user has access to" in { - Get(s"/v1/resolvers/${project.ref}/caches?deprecated=true") ~> asBob ~> routes ~> check { - status shouldEqual StatusCodes.OK - response.asJson shouldEqual expectedResults(inProjectLast) - } - } - - "return the in project resolvers" in { - val encodedResolver = UrlUtils.encode(nxv.Resolver.toString) - val encodedInProjectResolver = UrlUtils.encode(nxv.InProject.toString) - Get( - s"/v1/resolvers/${project.ref}/caches?type=$encodedResolver&type=$encodedInProjectResolver" - ) ~> asBob ~> routes ~> check { - status shouldEqual StatusCodes.OK - response.asJson should equalIgnoreArrayOrder( - expectedResults( - inProjectLast, - inProject(nxv + "in-project-put2", 3), - inProject(nxv + "in-project-post", 1) - ) - ) - } - } - - "return the resolvers with revision 2" in { - Get(s"/v1/resolvers/${project2.ref}/caches?rev=2") ~> asAlice ~> routes ~> check { - status shouldEqual StatusCodes.OK - response.asJson should equalIgnoreArrayOrder( - expectedResults( - crossProjectUseCurrentLast, - crossProjectProvidedIdentitiesLast.replace( - Json.arr("nxv:Schema".asJson, "nxv:Custom".asJson), - Json.arr(nxv.Schema.asJson, (nxv + "Custom").asJson) - ) - ) - ) - } - } - - "fail to list resolvers if the user has not access resolvers/read on the project" in { - forAll( - List( - Get(s"/v1/resolvers/${project.ref}/caches?deprecated=true") ~> routes, - Get(s"/v1/resolvers/${project2.ref}/caches") ~> asBob ~> routes - ) - ) { request => - request ~> check { - status shouldEqual StatusCodes.Forbidden - } - } - } - } - val idResourceEncoded = UrlUtils.encode(resourceId.toString) val idSchemaEncoded = UrlUtils.encode(schemaId.toString) val unknownResourceEncoded = UrlUtils.encode((nxv + "xxx").toString) diff --git a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala index 3a4d0b3492..da88aaa92c 100644 --- a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala +++ b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala @@ -4,6 +4,7 @@ import akka.http.scaladsl.model.MediaTypes.`text/html` import akka.http.scaladsl.model.headers.{Accept, Location, OAuth2BearerToken} import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.server.Route +import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.kernel.utils.{UUIDF, UrlUtils} import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, nxv, schema, schemas} @@ -22,7 +23,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.FetchResource -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport import ch.epfl.bluebrain.nexus.delta.sdk.resources.NexusSource.DecodingOption import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.ProjectContextRejection import ch.epfl.bluebrain.nexus.delta.sdk.resources.{Resources, ResourcesConfig, ResourcesImpl, ValidateResource} @@ -32,7 +32,6 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authent import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} import ch.epfl.bluebrain.nexus.testkit.bio.IOFromMap import io.circe.{Json, Printer} -import monix.bio.{IO, UIO} import java.util.UUID @@ -84,19 +83,16 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { private val aclCheck = AclSimpleCheck().accepted private val fetchSchema: (ResourceRef, ProjectRef) => FetchResource[Schema] = { - case (ref, _) if ref.iri == schema2.id => UIO.some(SchemaGen.resourceFor(schema2, deprecated = true)) - case (ref, _) if ref.iri == schema1.id => UIO.some(SchemaGen.resourceFor(schema1)) - case _ => UIO.none + case (ref, _) if ref.iri == schema2.id => IO.pure(Some(SchemaGen.resourceFor(schema2, deprecated = true))) + case (ref, _) if ref.iri == schema1.id => IO.pure(Some(SchemaGen.resourceFor(schema1))) + case _ => IO.none } private val validator: ValidateResource = ValidateResource( ResourceResolutionGen.singleInProject(projectRef, fetchSchema) ) private val fetchContext = FetchContextDummy(List(project.value), ProjectContextRejection) - private val resolverContextResolution: ResolverContextResolution = new ResolverContextResolution( - rcr, - (_, _, _) => IO.raiseError(ResourceResolutionReport()) - ) + private val resolverContextResolution: ResolverContextResolution = ResolverContextResolution(rcr) private def routesWithDecodingOption(implicit decodingOption: DecodingOption) = { val resources = ResourcesImpl( diff --git a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/SchemasRoutesSpec.scala b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/SchemasRoutesSpec.scala index e09ec8684e..6cc50ff1e5 100644 --- a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/SchemasRoutesSpec.scala +++ b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/SchemasRoutesSpec.scala @@ -4,6 +4,7 @@ import akka.http.scaladsl.model.MediaTypes.`text/html` import akka.http.scaladsl.model.headers.{Accept, Location, OAuth2BearerToken} import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.server.Route +import cats.effect.{ContextShift, IO} import ch.epfl.bluebrain.nexus.delta.kernel.utils.{UUIDF, UrlUtils} import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary @@ -22,7 +23,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.{events, resour import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.SchemaRejection.ProjectContextRejection import ch.epfl.bluebrain.nexus.delta.sdk.schemas.{SchemaImports, SchemasConfig, SchemasImpl} import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec @@ -31,15 +31,17 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef import ch.epfl.bluebrain.nexus.testkit.bio.IOFromMap import ch.epfl.bluebrain.nexus.testkit.ce.IOFixedClock import io.circe.Json -import monix.bio.IO import java.util.UUID +import scala.concurrent.ExecutionContext class SchemasRoutesSpec extends BaseRouteSpec with IOFixedClock with IOFromMap { private val uuid = UUID.randomUUID() implicit private val uuidF: UUIDF = UUIDF.fixed(uuid) + implicit private val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global) + private val caller = Caller(alice, Set(alice, Anonymous, Authenticated(realm), Group("group", realm))) private val identities = IdentitiesDummy(caller) @@ -61,15 +63,9 @@ class SchemasRoutesSpec extends BaseRouteSpec with IOFixedClock with IOFromMap { private val payloadNoId = payload.removeKeys(keywords.id) private val payloadUpdated = payloadNoId.replace("datatype" -> "xsd:integer", "xsd:double") - private val schemaImports = new SchemaImports( - (_, _, _) => IO.raiseError(ResourceResolutionReport()), - (_, _, _) => IO.raiseError(ResourceResolutionReport()) - ) + private val schemaImports = SchemaImports.alwaysFail - private val resolverContextResolution: ResolverContextResolution = new ResolverContextResolution( - rcr, - (_, _, _) => IO.raiseError(ResourceResolutionReport()) - ) + private val resolverContextResolution: ResolverContextResolution = ResolverContextResolution(rcr) private lazy val aclCheck = AclSimpleCheck().accepted diff --git a/delta/plugins/composite-views/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViewDecodingSpec.scala b/delta/plugins/composite-views/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViewDecodingSpec.scala index ff2f89174b..16d3ce4f48 100644 --- a/delta/plugins/composite-views/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViewDecodingSpec.scala +++ b/delta/plugins/composite-views/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViewDecodingSpec.scala @@ -17,11 +17,9 @@ import ch.epfl.bluebrain.nexus.delta.sdk.generators.ProjectGen import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.{ApiMappings, ProjectContext} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Group, User} import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef} import ch.epfl.bluebrain.nexus.testkit.{CirceLiteral, EitherValuable, TestHelpers} -import monix.bio.IO import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.{Inspectors, OptionValues} @@ -52,8 +50,7 @@ class CompositeViewDecodingSpec val uuid = UUID.randomUUID() implicit private val uuidF: UUIDF = UUIDF.fixed(uuid) - val resolverContext: ResolverContextResolution = - new ResolverContextResolution(rcr, (_, _, _) => IO.raiseError(ResourceResolutionReport())) + val resolverContext: ResolverContextResolution = ResolverContextResolution(rcr) private val decoder = CompositeViewFieldsJsonLdSourceDecoder(uuidF, resolverContext, 1.minute) val query1 = diff --git a/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViewDecodingSpec.scala b/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViewDecodingSpec.scala index 956bff2d40..9c3babdd08 100644 --- a/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViewDecodingSpec.scala +++ b/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViewDecodingSpec.scala @@ -11,14 +11,12 @@ import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.{ApiMappings, ProjectContext} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport import ch.epfl.bluebrain.nexus.delta.sdk.views.{PipeStep, ViewRef} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef} import ch.epfl.bluebrain.nexus.delta.sourcing.stream.pipes._ import ch.epfl.bluebrain.nexus.testkit.{IOValues, TestHelpers} import io.circe.literal._ -import monix.bio.IO import monix.execution.Scheduler.Implicits.global import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike @@ -44,8 +42,7 @@ class ElasticSearchViewDecodingSpec implicit private val uuidF: UUIDF = UUIDF.fixed(UUID.randomUUID()) - implicit private val resolverContext: ResolverContextResolution = - new ResolverContextResolution(rcr, (_, _, _) => IO.raiseError(ResourceResolutionReport())) + implicit private val resolverContext: ResolverContextResolution = ResolverContextResolution(rcr) implicit private val caller: Caller = Caller.Anonymous private val decoder = diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/StorageScopeInitializationSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/StorageScopeInitializationSpec.scala index 472b04d204..50a5069c9e 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/StorageScopeInitializationSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/StorageScopeInitializationSpec.scala @@ -11,7 +11,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.ServiceAccount import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport import ch.epfl.bluebrain.nexus.delta.sdk.{ConfigFixtures, Defaults} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Subject, User} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label @@ -56,7 +55,7 @@ class StorageScopeInitializationSpec "A StorageScopeInitialization" should { lazy val storages = Storages( fetchContext, - new ResolverContextResolution(rcr, (_, _, _) => IO.raiseError(ResourceResolutionReport())), + ResolverContextResolution(rcr), IO.pure(allowedPerms.toSet), (_, _) => IO.unit, xas, diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala index 39174a9c6e..2acfa30993 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala @@ -31,7 +31,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, User} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef} @@ -108,7 +107,7 @@ class FilesSpec(docker: RemoteStorageDocker) lazy val storages: Storages = Storages( fetchContext.mapRejection(StorageRejection.ProjectContextRejection), - new ResolverContextResolution(rcr, (_, _, _) => IO.raiseError(ResourceResolutionReport())), + ResolverContextResolution(rcr), IO.pure(allowedPerms), (_, _) => IO.unit, xas, diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala index 638e7ba20f..ea5e957d8f 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala @@ -34,7 +34,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.events import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport import ch.epfl.bluebrain.nexus.delta.sdk.utils.{BaseRouteSpec, RouteFixtures} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject, User} import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef} @@ -101,7 +100,7 @@ class FilesRoutesSpec private val aclCheck = AclSimpleCheck().accepted lazy val storages: Storages = Storages( fetchContext.mapRejection(StorageRejection.ProjectContextRejection), - new ResolverContextResolution(rcr, (_, _, _) => IO.raiseError(ResourceResolutionReport())), + ResolverContextResolution(rcr), IO.pure(allowedPerms.toSet), (_, _) => IO.unit, xas, diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/StoragesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/StoragesSpec.scala index 7356901899..361686cfcf 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/StoragesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/StoragesSpec.scala @@ -13,7 +13,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ import ch.epfl.bluebrain.nexus.delta.sdk.model.{IdSegmentRef, Tags} import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Authenticated, Group, User} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef} @@ -65,7 +64,7 @@ class StoragesSpec lazy val storages = Storages( fetchContext, - new ResolverContextResolution(rcr, (_, _, _) => IO.raiseError(ResourceResolutionReport())), + ResolverContextResolution(rcr), IO.pure(allowedPerms.toSet), (_, _) => IO.unit, xas, diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/routes/StoragesRoutesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/routes/StoragesRoutesSpec.scala index e8a30c5af5..35219a1f08 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/routes/StoragesRoutesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/routes/StoragesRoutesSpec.scala @@ -28,7 +28,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject, User} import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef} @@ -102,7 +101,7 @@ class StoragesRoutesSpec extends BaseRouteSpec with TryValues with StorageFixtur private lazy val storages = Storages( fetchContext, - new ResolverContextResolution(rcr, (_, _, _) => IO.raiseError(ResourceResolutionReport())), + ResolverContextResolution(rcr), IO.pure(perms), (_, _) => IO.unit, xas, diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/package.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/package.scala index 2a7f173817..6fe942e104 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/package.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/package.scala @@ -2,6 +2,7 @@ package ch.epfl.bluebrain.nexus.delta import akka.stream.scaladsl.Source import akka.util.ByteString +import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.Acl import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceF @@ -14,7 +15,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.Resource import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema import ch.epfl.bluebrain.nexus.delta.sourcing.model.ResourceRef import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef -import monix.bio.IO package object sdk { @@ -61,7 +61,7 @@ package object sdk { /** * Type alias for resolver resolution */ - type Resolve[A] = (ResourceRef, ProjectRef, Caller) => IO[ResourceResolutionReport, A] + type Resolve[A] = (ResourceRef, ProjectRef, Caller) => IO[Either[ResourceResolutionReport, A]] type AkkaSource = Source[ByteString, Any] diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/MultiResolution.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/MultiResolution.scala index dd6dc01f13..5ddbd3f597 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/MultiResolution.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/MultiResolution.scala @@ -1,15 +1,16 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resolvers +import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.{ExpandIri, JsonLdContent} import ch.epfl.bluebrain.nexus.delta.sdk.model.{IdSegment, IdSegmentRef} +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContext import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ProjectContext import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.{InvalidResolution, InvalidResolvedResourceId, InvalidResolverResolution} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport.ResolverReport import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{MultiResolutionResult, ResolverRejection, ResourceResolutionReport} import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef -import monix.bio.IO /** * Allow to attempt resolutions for the different resource types available @@ -19,7 +20,7 @@ import monix.bio.IO * the resource resolution */ final class MultiResolution( - fetchProject: ProjectRef => IO[ResolverRejection, ProjectContext], + fetchProject: ProjectRef => IO[ProjectContext], resourceResolution: ResolverResolution[JsonLdContent[_, _]] ) { @@ -36,10 +37,10 @@ final class MultiResolution( def apply( resourceSegment: IdSegmentRef, projectRef: ProjectRef - )(implicit caller: Caller): IO[ResolverRejection, MultiResolutionResult[ResourceResolutionReport]] = + )(implicit caller: Caller): IO[MultiResolutionResult[ResourceResolutionReport]] = for { project <- fetchProject(projectRef) - resourceRef <- expandResourceIri(resourceSegment, project) + resourceRef <- toCatsIO(expandResourceIri(resourceSegment, project)) result <- resourceResolution.resolveReport(resourceRef, projectRef).flatMap { case (resourceReport, Some(resourceResult)) => IO.pure(MultiResolutionResult(resourceReport, resourceResult)) @@ -61,12 +62,12 @@ final class MultiResolution( resourceSegment: IdSegmentRef, projectRef: ProjectRef, resolverSegment: IdSegment - )(implicit caller: Caller): IO[ResolverRejection, MultiResolutionResult[ResolverReport]] = { + )(implicit caller: Caller): IO[MultiResolutionResult[ResolverReport]] = { for { project <- fetchProject(projectRef) - resourceRef <- expandResourceIri(resourceSegment, project) - resolverId <- Resolvers.expandIri(resolverSegment, project) + resourceRef <- toCatsIO(expandResourceIri(resourceSegment, project)) + resolverId <- toCatsIO(Resolvers.expandIri(resolverSegment, project)) result <- resourceResolution.resolveReport(resourceRef, projectRef, resolverId).flatMap { case (resourceReport, Some(resourceResult)) => IO.pure(MultiResolutionResult(resourceReport, resourceResult)) diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverContextResolution.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverContextResolution.scala index b5e98c4ee2..068ae7d24d 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverContextResolution.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverContextResolution.scala @@ -1,10 +1,13 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resolvers +import cats.effect.IO +import cats.syntax.all._ import ch.epfl.bluebrain.nexus.delta.kernel.Logger +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution.Result import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolutionError.RemoteContextNotAccessible -import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContext, RemoteContextResolution} +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContext, RemoteContextResolution, RemoteContextResolutionError} import ch.epfl.bluebrain.nexus.delta.sdk._ import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller @@ -16,7 +19,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.Resource import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} import io.circe.syntax._ -import monix.bio.IO import scala.collection.concurrent @@ -40,32 +42,32 @@ final class ResolverContextResolution(val rcr: RemoteContextResolution, resolveR IO.pure(cache.get(iri)).flatMap { case Some(s) => IO.pure(s) case None => - rcr - .resolve(iri) - .onErrorFallbackTo( - resolveResource(ResourceRef(iri), projectRef, caller) - .bimap( - report => + toCatsIO(rcr.resolve(iri)) + .handleErrorWith(_ => + resolveResource(ResourceRef(iri), projectRef, caller).flatMap { + case Left(report) => + IO.raiseError( RemoteContextNotAccessible( iri, s"Resolution via static resolution and via resolvers failed in '$projectRef'", Some(report.asJson) - ), - ProjectRemoteContext.fromResource - ) + ) + ) + case Right(resource) => IO.pure(ProjectRemoteContext.fromResource(resource)) + } ) - .tapEval { context => + .flatTap { context => IO.pure(cache.put(iri, context)) *> logger.debug(s"Iri $iri has been resolved for project $projectRef and caller $caller.subject") } } - } + }.toBIO[RemoteContextResolutionError] } } object ResolverContextResolution { - private val logger: Logger = Logger[ResolverContextResolution] + private val logger = Logger.cats[ResolverContextResolution] /** * A remote context defined in Nexus as a resource @@ -89,7 +91,7 @@ object ResolverContextResolution { * a previously defined 'RemoteContextResolution' */ def apply(rcr: RemoteContextResolution): ResolverContextResolution = - new ResolverContextResolution(rcr, (_, _, _) => IO.raiseError(ResourceResolutionReport())) + new ResolverContextResolution(rcr, (_, _, _) => IO.pure(Left(ResourceResolutionReport()))) /** * Constructs a [[ResolverContextResolution]] diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverResolution.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverResolution.scala index b8997226d4..d35985f668 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverResolution.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverResolution.scala @@ -1,6 +1,8 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resolvers +import cats.effect.IO import cats.implicits._ +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck @@ -16,10 +18,10 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.IdentityResolution.{Pro import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.Resolver.{CrossProjectResolver, InProjectResolver} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverResolutionRejection.{ProjectAccessDenied, ResolutionFetchRejection, ResourceTypesDenied, WrappedResolverRejection} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport.{ResolverFailedReport, ResolverReport, ResolverSuccessReport} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{Resolver, ResolverRejection, ResourceResolutionReport} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{Resolver, ResolverRejection, ResolverResolutionRejection, ResourceResolutionReport} import ch.epfl.bluebrain.nexus.delta.sdk.{ResolverResource, ResourceShifts} import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Identity, ProjectRef, ResourceRef} -import monix.bio.{IO, UIO} +import monix.bio.{IO => BIO} import java.time.Instant import scala.collection.immutable.VectorMap @@ -36,9 +38,9 @@ import scala.collection.immutable.VectorMap * how we can get a resource from a [[ResourceRef]] */ final class ResolverResolution[R]( - checkAcls: (ProjectRef, Set[Identity]) => UIO[Boolean], - listResolvers: ProjectRef => UIO[List[Resolver]], - fetchResolver: (Iri, ProjectRef) => IO[ResolverRejection, Resolver], + checkAcls: (ProjectRef, Set[Identity]) => IO[Boolean], + listResolvers: ProjectRef => IO[List[Resolver]], + fetchResolver: (Iri, ProjectRef) => IO[Resolver], fetch: (ResourceRef, ProjectRef) => Fetch[R], extractTypes: R => Set[Iri] ) { @@ -52,9 +54,11 @@ final class ResolverResolution[R]( * @param projectRef * the project reference */ - def resolve(ref: ResourceRef, projectRef: ProjectRef)(implicit caller: Caller): IO[ResourceResolutionReport, R] = - resolveReport(ref, projectRef).flatMap { case (report, resource) => - IO.fromOption(resource, report) + def resolve(ref: ResourceRef, projectRef: ProjectRef)(implicit + caller: Caller + ): IO[Either[ResourceResolutionReport, R]] = + resolveReport(ref, projectRef).map { case (report, resource) => + resource.toRight(report) } /** @@ -68,7 +72,7 @@ final class ResolverResolution[R]( */ def resolveReport(ref: ResourceRef, projectRef: ProjectRef)(implicit caller: Caller - ): UIO[(ResourceResolutionReport, Option[R])] = { + ): IO[(ResourceResolutionReport, Option[R])] = { val initial: (ResourceResolutionReport, Option[R]) = ResourceResolutionReport() -> None @@ -101,11 +105,9 @@ final class ResolverResolution[R]( */ def resolve(ref: ResourceRef, projectRef: ProjectRef, resolverId: Iri)(implicit caller: Caller - ): IO[ResolverReport, R] = + ): IO[Either[ResolverReport, R]] = resolveReport(ref, projectRef, resolverId) - .flatMap { case (report, resource) => - IO.fromOption(resource, report) - } + .map { case (report, resource) => resource.toRight(report) } /** * Attempts to resolve the resource against the given resolver and return the resource if found and a report of how @@ -119,10 +121,10 @@ final class ResolverResolution[R]( */ def resolveReport(ref: ResourceRef, projectRef: ProjectRef, resolverId: Iri)(implicit caller: Caller - ): UIO[(ResolverReport, Option[R])] = + ): IO[(ResolverReport, Option[R])] = fetchResolver(resolverId, projectRef) .flatMap { r => resolveReport(ref, projectRef, r) } - .onErrorHandle { r => + .recover { case r: ResolverRejection => ResolverReport.failed(resolverId, projectRef -> WrappedResolverRejection(r)) -> None } @@ -130,7 +132,7 @@ final class ResolverResolution[R]( ref: ResourceRef, projectRef: ProjectRef, resolver: Resolver - )(implicit caller: Caller): UIO[ResolverResolutionResult[R]] = + )(implicit caller: Caller): IO[ResolverResolutionResult[R]] = resolver match { case i: InProjectResolver => inProjectResolve(ref, projectRef, i) case c: CrossProjectResolver => crossProjectResolve(ref, c) @@ -140,7 +142,7 @@ final class ResolverResolution[R]( ref: ResourceRef, projectRef: ProjectRef, resolver: InProjectResolver - ): UIO[ResolverResolutionResult[R]] = + ): IO[ResolverResolutionResult[R]] = fetch(ref, projectRef).map { case None => ResolverReport.failed(resolver.id, projectRef -> ResolutionFetchRejection(ref, projectRef)) -> None case s => ResolverReport.success(resolver.id, projectRef) -> s @@ -149,10 +151,10 @@ final class ResolverResolution[R]( private def crossProjectResolve( ref: ResourceRef, resolver: CrossProjectResolver - )(implicit caller: Caller): UIO[ResolverResolutionResult[R]] = { + )(implicit caller: Caller): IO[ResolverResolutionResult[R]] = { import resolver.value._ - def validateIdentities(p: ProjectRef): IO[ProjectAccessDenied, Unit] = { + def validateIdentities(p: ProjectRef): IO[Unit] = { val identities = identityResolution match { case UseCurrentCaller => caller.identities case ProvidedIdentities(identities) => identities @@ -164,10 +166,8 @@ final class ResolverResolution[R]( } } - def validateResourceTypes(types: Set[Iri], p: ProjectRef): IO[ResourceTypesDenied, Unit] = - IO.unless(resourceTypes.isEmpty || resourceTypes.exists(types.contains))( - IO.raiseError(ResourceTypesDenied(p, types)) - ) + def validateResourceTypes(types: Set[Iri], p: ProjectRef): IO[Unit] = + IO.raiseUnless(resourceTypes.isEmpty || resourceTypes.exists(types.contains))(ResourceTypesDenied(p, types)) val initial: ResolverResolutionResult[R] = ResolverFailedReport(resolver.id, VectorMap.empty) -> None projects.foldLeftM(initial) { (previous, projectRef) => @@ -179,12 +179,13 @@ final class ResolverResolution[R]( val resolve = for { _ <- validateIdentities(projectRef) resource <- fetch(ref, projectRef).flatMap { res => - IO.fromOption(res, ResolutionFetchRejection(ref, projectRef)) + IO.fromOption(res)(ResolutionFetchRejection(ref, projectRef)) } _ <- validateResourceTypes(extractTypes(resource), projectRef) } yield ResolverSuccessReport(resolver.id, projectRef, f.rejections) -> Option(resource) - resolve.onErrorHandle { e => - f.copy(rejections = f.rejections + (projectRef -> e)) -> None + resolve.attemptNarrow[ResolverResolutionRejection].map { + case Left(r) => f.copy(rejections = f.rejections + (projectRef -> r)) -> None + case Right(s) => s } } } @@ -198,13 +199,13 @@ object ResolverResolution { */ type ResourceResolution[R] = ResolverResolution[ResourceF[R]] - type Fetch[R] = UIO[Option[R]] + type Fetch[R] = IO[Option[R]] - type FetchResource[R] = UIO[Option[ResourceF[R]]] + type FetchResource[R] = IO[Option[ResourceF[R]]] type ResolverResolutionResult[R] = (ResolverReport, Option[R]) - private val resolverSearchParams = ResolverSearchParams(deprecated = Some(false), filter = _ => UIO.pure(true)) + private val resolverSearchParams = ResolverSearchParams(deprecated = Some(false), filter = _ => BIO.pure(true)) private val resolverOrdering: Ordering[ResolverResource] = Ordering[Instant] on (r => r.createdAt) @@ -257,7 +258,7 @@ object ResolverResolution { def apply( aclCheck: AclCheck, resolvers: Resolvers, - fetch: (ResourceRef, ProjectRef) => UIO[Option[JsonLdContent[_, _]]] + fetch: (ResourceRef, ProjectRef) => IO[Option[JsonLdContent[_, _]]] ): ResolverResolution[JsonLdContent[_, _]] = apply(aclCheck, resolvers, fetch, _.resource.types, Permissions.resources.read) diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverScopeInitialization.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverScopeInitialization.scala index 296a9fa7bf..8eceb187d0 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverScopeInitialization.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverScopeInitialization.scala @@ -3,13 +3,13 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resolvers import cats.effect.IO import cats.implicits._ import ch.epfl.bluebrain.nexus.delta.kernel.Logger -import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.kernel.kamon.KamonMetricComponent import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv import ch.epfl.bluebrain.nexus.delta.sdk.error.ServiceError.ScopeInitializationFailed import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.{Caller, ServiceAccount} import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.Organization import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.Project +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverScopeInitialization.{logger, CreateResolver} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.Resolvers.entityType import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.{ProjectContextRejection, ResourceAlreadyExists} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverValue.InProjectValue @@ -17,6 +17,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{Priority, ResolverValu import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ import ch.epfl.bluebrain.nexus.delta.sdk.{Defaults, ScopeInitialization} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject +import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef /** * The default creation of the InProject resolver as part of the project initialization. @@ -26,22 +27,14 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject * @param serviceAccount * the subject that will be recorded when performing the initialization */ -class ResolverScopeInitialization( - resolvers: Resolvers, - serviceAccount: ServiceAccount, - defaults: Defaults -) extends ScopeInitialization { +class ResolverScopeInitialization(createResolver: CreateResolver, defaults: Defaults) extends ScopeInitialization { - private val logger = Logger.cats[ResolverScopeInitialization] private val defaultInProjectResolverValue: ResolverValue = InProjectValue(Some(defaults.name), Some(defaults.description), Priority.unsafe(1)) - implicit private val caller: Caller = serviceAccount.caller implicit private val kamonComponent: KamonMetricComponent = KamonMetricComponent(entityType.value) override def onProjectCreation(project: Project, subject: Subject): IO[Unit] = - resolvers - .create(nxv.defaultResolver, project.ref, defaultInProjectResolverValue) - .void + createResolver(project.ref, defaultInProjectResolverValue) .handleErrorWith { case _: ResourceAlreadyExists => IO.unit // nothing to do, resolver already exits case _: ProjectContextRejection => IO.unit // project or org is likely deprecated @@ -52,8 +45,22 @@ class ResolverScopeInitialization( } .span("createDefaultResolver") - override def onOrganizationCreation( - organization: Organization, - subject: Subject - ): IO[Unit] = IO.unit + override def onOrganizationCreation(organization: Organization, subject: Subject): IO[Unit] = IO.unit +} + +object ResolverScopeInitialization { + + type CreateResolver = (ProjectRef, ResolverValue) => IO[Unit] + + private val logger = Logger.cats[ResolverScopeInitialization] + + def apply(resolvers: Resolvers, serviceAccount: ServiceAccount, defaults: Defaults) = { + implicit val caller: Caller = serviceAccount.caller + def createResolver: CreateResolver = resolvers.create(nxv.defaultResolver, _, _).void + new ResolverScopeInitialization( + createResolver, + defaults + ) + } + } diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/Resolvers.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/Resolvers.scala index 791b13f7ad..6b08e8a59a 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/Resolvers.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/Resolvers.scala @@ -1,6 +1,9 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resolvers -import cats.effect.Clock +import cats.effect.{Clock, IO} +import cats.syntax.all._ +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ +import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination.FromPagination import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, nxv, schemas} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.ContextValue @@ -8,7 +11,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.ResolverResource import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.instances._ import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.ExpandIri -import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination.FromPagination import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams.ResolverSearchParams import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults.UnscoredSearchResults import ch.epfl.bluebrain.nexus.delta.sdk.model.{IdSegment, IdSegmentRef, ResourceToSchemaMappings, Tags} @@ -17,7 +19,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.IdentityResolution.{ProvidedIdentities, UseCurrentCaller} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverCommand.{CreateResolver, DeprecateResolver, TagResolver, UpdateResolver} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverEvent.{ResolverCreated, ResolverDeprecated, ResolverTagAdded, ResolverUpdated} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.{DifferentResolverType, IncorrectRev, InvalidIdentities, InvalidResolverId, NoIdentities, PriorityAlreadyExists, ResolverIsDeprecated, ResolverNotFound, ResourceAlreadyExists, RevisionNotFound} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.{DifferentResolverType, IncorrectRev, InvalidIdentities, InvalidResolverId, NoIdentities, ResolverIsDeprecated, ResolverNotFound, ResourceAlreadyExists, RevisionNotFound} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverValue.{CrossProjectValue, InProjectValue} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model._ import ch.epfl.bluebrain.nexus.delta.sourcing.ScopedEntityDefinition.Tagger @@ -27,7 +29,6 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{EntityType, Label, ProjectRef} import ch.epfl.bluebrain.nexus.delta.sourcing.{ScopedEntityDefinition, StateMachine} import io.circe.Json -import monix.bio.{IO, UIO} /** * Operations for handling resolvers @@ -42,9 +43,7 @@ trait Resolvers { * @param source * the payload to create the resolver */ - def create(projectRef: ProjectRef, source: Json)(implicit - caller: Caller - ): IO[ResolverRejection, ResolverResource] + def create(projectRef: ProjectRef, source: Json)(implicit caller: Caller): IO[ResolverResource] /** * Create a new resolver with the provided id @@ -56,9 +55,7 @@ trait Resolvers { * @param source * the payload to create the resolver */ - def create(id: IdSegment, projectRef: ProjectRef, source: Json)(implicit - caller: Caller - ): IO[ResolverRejection, ResolverResource] + def create(id: IdSegment, projectRef: ProjectRef, source: Json)(implicit caller: Caller): IO[ResolverResource] /** * Create a new resolver with the provided id @@ -71,7 +68,7 @@ trait Resolvers { */ def create(id: IdSegment, projectRef: ProjectRef, resolverValue: ResolverValue)(implicit caller: Caller - ): IO[ResolverRejection, ResolverResource] + ): IO[ResolverResource] /** * Update an existing resolver @@ -86,7 +83,7 @@ trait Resolvers { */ def update(id: IdSegment, projectRef: ProjectRef, rev: Int, source: Json)(implicit caller: Caller - ): IO[ResolverRejection, ResolverResource] + ): IO[ResolverResource] /** * Update an existing resolver @@ -101,7 +98,7 @@ trait Resolvers { */ def update(id: IdSegment, projectRef: ProjectRef, rev: Int, resolverValue: ResolverValue)(implicit caller: Caller - ): IO[ResolverRejection, ResolverResource] + ): IO[ResolverResource] /** * Add a tag to an existing resolver @@ -119,7 +116,7 @@ trait Resolvers { */ def tag(id: IdSegment, projectRef: ProjectRef, tag: UserTag, tagRev: Int, rev: Int)(implicit subject: Subject - ): IO[ResolverRejection, ResolverResource] + ): IO[ResolverResource] /** * Deprecate an existing resolver @@ -130,9 +127,7 @@ trait Resolvers { * @param rev * the ResolverState revision of the resolver */ - def deprecate(id: IdSegment, projectRef: ProjectRef, rev: Int)(implicit - subject: Subject - ): IO[ResolverRejection, ResolverResource] + def deprecate(id: IdSegment, projectRef: ProjectRef, rev: Int)(implicit subject: Subject): IO[ResolverResource] /** * Fetch the resolver at the requested version @@ -141,7 +136,7 @@ trait Resolvers { * @param projectRef * the project where the resolver belongs */ - def fetch(id: IdSegmentRef, projectRef: ProjectRef): IO[ResolverRejection, ResolverResource] + def fetch(id: IdSegmentRef, projectRef: ProjectRef): IO[ResolverResource] /** * Fetches and validate the resolver, rejecting if the project does not exists or if it is deprecated @@ -150,7 +145,7 @@ trait Resolvers { * @param projectRef * the project reference */ - def fetchActiveResolver(id: Iri, projectRef: ProjectRef): IO[ResolverRejection, Resolver] = + def fetchActiveResolver(id: Iri, projectRef: ProjectRef): IO[Resolver] = fetch(id, projectRef).flatMap(res => IO.raiseWhen(res.deprecated)(ResolverIsDeprecated(id)).as(res.value)) /** @@ -169,7 +164,7 @@ trait Resolvers { pagination: FromPagination, params: ResolverSearchParams, ordering: Ordering[ResolverResource] - ): UIO[UnscoredSearchResults[ResolverResource]] + ): IO[UnscoredSearchResults[ResolverResource]] /** * List resolvers within a project @@ -188,13 +183,13 @@ trait Resolvers { pagination: FromPagination, params: ResolverSearchParams, ordering: Ordering[ResolverResource] - ): UIO[UnscoredSearchResults[ResolverResource]] = + ): IO[UnscoredSearchResults[ResolverResource]] = list(pagination, params.copy(project = Some(projectRef)), ordering) } object Resolvers { - type ValidatePriority = (ProjectRef, Iri, Priority) => IO[PriorityAlreadyExists, Unit] + type ValidatePriority = (ProjectRef, Iri, Priority) => IO[Unit] /** * The resolver entity type. @@ -217,7 +212,7 @@ object Resolvers { Label.unsafe("resolvers") -> schemas.resolvers ) - import ch.epfl.bluebrain.nexus.delta.kernel.utils.IOUtils.instant + import ch.epfl.bluebrain.nexus.delta.kernel.utils.IOInstant.now private[delta] def next(state: Option[ResolverState], event: ResolverEvent): Option[ResolverState] = { @@ -267,15 +262,15 @@ object Resolvers { private[delta] def evaluate( validatePriority: ValidatePriority )(state: Option[ResolverState], command: ResolverCommand)(implicit - clock: Clock[UIO] - ): IO[ResolverRejection, ResolverEvent] = { + clock: Clock[IO] + ): IO[ResolverEvent] = { def validateResolverValue( project: ProjectRef, id: Iri, value: ResolverValue, caller: Caller - ): IO[ResolverRejection, Unit] = + ): IO[Unit] = (value match { case CrossProjectValue(_, _, _, _, _, identityResolution) => identityResolution match { @@ -283,18 +278,17 @@ object Resolvers { case ProvidedIdentities(value) if value.isEmpty => IO.raiseError(NoIdentities) case ProvidedIdentities(value) => val missing = value.diff(caller.identities) - IO.when(missing.nonEmpty)(IO.raiseError(InvalidIdentities(missing))) + IO.raiseWhen(missing.nonEmpty)(InvalidIdentities(missing)) } - - case _ => IO.unit + case _ => IO.unit }) >> validatePriority(project, id, value.priority) - def create(c: CreateResolver): IO[ResolverRejection, ResolverCreated] = state match { + def create(c: CreateResolver): IO[ResolverCreated] = state match { // Create a resolver case None => for { _ <- validateResolverValue(c.project, c.id, c.value, c.caller) - now <- instant + now <- now } yield ResolverCreated( id = c.id, project = c.project, @@ -309,7 +303,7 @@ object Resolvers { IO.raiseError(ResourceAlreadyExists(c.id, c.project)) } - def update(c: UpdateResolver): IO[ResolverRejection, ResolverUpdated] = state match { + def update(c: UpdateResolver): IO[ResolverUpdated] = state match { // Update a non existing resolver case None => IO.raiseError(ResolverNotFound(c.id, c.project)) @@ -323,9 +317,9 @@ object Resolvers { // Update a resolver case Some(s) => for { - _ <- IO.when(s.value.tpe != c.value.tpe)(IO.raiseError(DifferentResolverType(c.id, c.value.tpe, s.value.tpe))) + _ <- IO.raiseWhen(s.value.tpe != c.value.tpe)(DifferentResolverType(c.id, c.value.tpe, s.value.tpe)) _ <- validateResolverValue(c.project, c.id, c.value, c.caller) - now <- instant + now <- now } yield ResolverUpdated( id = c.id, project = c.project, @@ -337,7 +331,7 @@ object Resolvers { ) } - def addTag(c: TagResolver): IO[ResolverRejection, ResolverTagAdded] = state match { + def addTag(c: TagResolver): IO[ResolverTagAdded] = state match { // Resolver can't be found case None => IO.raiseError(ResolverNotFound(c.id, c.project)) @@ -348,7 +342,7 @@ object Resolvers { case Some(s) if c.targetRev <= 0 || c.targetRev > s.rev => IO.raiseError(RevisionNotFound(c.targetRev, s.rev)) case Some(s) => - instant.map { now => + now.map { now => ResolverTagAdded( id = c.id, project = c.project, @@ -362,7 +356,7 @@ object Resolvers { } } - def deprecate(c: DeprecateResolver): IO[ResolverRejection, ResolverDeprecated] = state match { + def deprecate(c: DeprecateResolver): IO[ResolverDeprecated] = state match { // Resolver can't be found case None => IO.raiseError(ResolverNotFound(c.id, c.project)) @@ -372,7 +366,7 @@ object Resolvers { case Some(s) if s.deprecated => IO.raiseError(ResolverIsDeprecated(s.id)) case Some(s) => - instant.map { now => + now.map { now => ResolverDeprecated( id = c.id, project = c.project, @@ -392,15 +386,16 @@ object Resolvers { } } + private type ResolverDefinition = + ScopedEntityDefinition[Iri, ResolverState, ResolverCommand, ResolverEvent, ResolverRejection] + /** * Entity definition for [[Resolvers]] */ - def definition(validatePriority: ValidatePriority)(implicit - clock: Clock[UIO] - ): ScopedEntityDefinition[Iri, ResolverState, ResolverCommand, ResolverEvent, ResolverRejection] = + def definition(validatePriority: ValidatePriority)(implicit clock: Clock[IO]): ResolverDefinition = ScopedEntityDefinition( entityType, - StateMachine(None, evaluate(validatePriority), next), + StateMachine(None, evaluate(validatePriority)(_, _).toBIO[ResolverRejection], next), ResolverEvent.serializer, ResolverState.serializer, Tagger[ResolverEvent]( diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversConfig.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversConfig.scala index a43870ede3..2883f51a90 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversConfig.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversConfig.scala @@ -1,7 +1,6 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resolvers import ch.epfl.bluebrain.nexus.delta.sdk.Defaults -import ch.epfl.bluebrain.nexus.delta.sdk.model.search.PaginationConfig import ch.epfl.bluebrain.nexus.delta.sourcing.config.EventLogConfig import pureconfig.ConfigReader import pureconfig.generic.semiauto.deriveReader @@ -11,12 +10,9 @@ import pureconfig.generic.semiauto.deriveReader * * @param eventLog * configuration of the event log - * @param pagination - * configuration for how pagination should behave in listing operations */ final case class ResolversConfig( eventLog: EventLogConfig, - pagination: PaginationConfig, defaults: Defaults ) diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversImpl.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversImpl.scala index 71f4b51567..a34ce39e56 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversImpl.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversImpl.scala @@ -1,6 +1,7 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resolvers -import cats.effect.Clock +import cats.effect.{Clock, IO} +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.kernel.kamon.KamonMetricComponent import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination.FromPagination import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF @@ -27,7 +28,6 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Identity, ProjectRef} import doobie.implicits._ import io.circe.Json -import monix.bio.{IO, UIO} final class ResolversImpl private ( log: ResolversLog, @@ -40,7 +40,7 @@ final class ResolversImpl private ( override def create( projectRef: ProjectRef, source: Json - )(implicit caller: Caller): IO[ResolverRejection, ResolverResource] = { + )(implicit caller: Caller): IO[ResolverResource] = { for { pc <- fetchContext.onCreate(projectRef) (iri, resolverValue) <- sourceDecoder(projectRef, pc, source) @@ -52,7 +52,7 @@ final class ResolversImpl private ( id: IdSegment, projectRef: ProjectRef, source: Json - )(implicit caller: Caller): IO[ResolverRejection, ResolverResource] = { + )(implicit caller: Caller): IO[ResolverResource] = { for { pc <- fetchContext.onCreate(projectRef) iri <- expandIri(id, pc) @@ -65,7 +65,7 @@ final class ResolversImpl private ( id: IdSegment, projectRef: ProjectRef, resolverValue: ResolverValue - )(implicit caller: Caller): IO[ResolverRejection, ResolverResource] = { + )(implicit caller: Caller): IO[ResolverResource] = { for { pc <- fetchContext.onCreate(projectRef) iri <- expandIri(id, pc) @@ -79,7 +79,7 @@ final class ResolversImpl private ( projectRef: ProjectRef, rev: Int, source: Json - )(implicit caller: Caller): IO[ResolverRejection, ResolverResource] = { + )(implicit caller: Caller): IO[ResolverResource] = { for { pc <- fetchContext.onModify(projectRef) iri <- expandIri(id, pc) @@ -95,7 +95,7 @@ final class ResolversImpl private ( resolverValue: ResolverValue )(implicit caller: Caller - ): IO[ResolverRejection, ResolverResource] = { + ): IO[ResolverResource] = { for { pc <- fetchContext.onModify(projectRef) iri <- expandIri(id, pc) @@ -112,7 +112,7 @@ final class ResolversImpl private ( rev: Int )(implicit subject: Identity.Subject - ): IO[ResolverRejection, ResolverResource] = { + ): IO[ResolverResource] = { for { pc <- fetchContext.onModify(projectRef) iri <- expandIri(id, pc) @@ -124,7 +124,7 @@ final class ResolversImpl private ( id: IdSegment, projectRef: ProjectRef, rev: Int - )(implicit subject: Identity.Subject): IO[ResolverRejection, ResolverResource] = { + )(implicit subject: Identity.Subject): IO[ResolverResource] = { for { pc <- fetchContext.onModify(projectRef) iri <- expandIri(id, pc) @@ -132,7 +132,7 @@ final class ResolversImpl private ( } yield res }.span("deprecateResolver") - override def fetch(id: IdSegmentRef, projectRef: ProjectRef): IO[ResolverRejection, ResolverResource] = { + override def fetch(id: IdSegmentRef, projectRef: ProjectRef): IO[ResolverResource] = { for { pc <- fetchContext.onRead(projectRef) iri <- expandIri(id.value, pc) @@ -151,7 +151,7 @@ final class ResolversImpl private ( pagination: FromPagination, params: ResolverSearchParams, ordering: Ordering[ResolverResource] - ): UIO[UnscoredSearchResults[ResolverResource]] = { + ): IO[UnscoredSearchResults[ResolverResource]] = { val scope = params.project.fold[Scope](Scope.Root)(ref => Scope.Project(ref)) SearchResults( log.currentStates(scope, _.toResource).evalFilter(params.matches), @@ -160,7 +160,7 @@ final class ResolversImpl private ( ).span("listResolvers") } - private def eval(cmd: ResolverCommand): IO[ResolverRejection, ResolverResource] = + private def eval(cmd: ResolverCommand) = log.evaluate(cmd.project, cmd.id, cmd).map(_._2.toResource) } @@ -176,13 +176,12 @@ object ResolversImpl { contextResolution: ResolverContextResolution, config: ResolversConfig, xas: Transactors - )(implicit api: JsonLdApi, clock: Clock[UIO], uuidF: UUIDF): Resolvers = { - def priorityAlreadyExists(ref: ProjectRef, self: Iri, priority: Priority): IO[PriorityAlreadyExists, Unit] = { + )(implicit api: JsonLdApi, clock: Clock[IO], uuidF: UUIDF): Resolvers = { + def priorityAlreadyExists(ref: ProjectRef, self: Iri, priority: Priority): IO[Unit] = { sql"SELECT id FROM scoped_states WHERE type = ${Resolvers.entityType} AND org = ${ref.organization} AND project = ${ref.project} AND id != $self AND (value->'value'->'priority')::int = ${priority.value} " .query[Iri] .option - .transact(xas.read) - .hideErrors + .transact(xas.readCE) .flatMap { case Some(other) => IO.raiseError(PriorityAlreadyExists(ref, other, priority)) case None => IO.unit diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResourceResolution.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResourceResolution.scala index 72686a4dcb..24ddb3303c 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResourceResolution.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResourceResolution.scala @@ -1,19 +1,19 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resolvers +import cats.effect.IO +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceF import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.{FetchResource, ResourceResolution} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{Resolver, ResolverRejection} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.Resolver import ch.epfl.bluebrain.nexus.delta.sdk.resources.Resources import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.Resource import ch.epfl.bluebrain.nexus.delta.sdk.schemas.Schemas -import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.{Schema, SchemaRejection} +import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Identity, ProjectRef, ResourceRef} -import monix.bio.{IO, UIO} -import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ object ResourceResolution { @@ -29,9 +29,9 @@ object ResourceResolution { * how to fetch the resource */ def apply[R]( - checkAcls: (ProjectRef, Set[Identity]) => UIO[Boolean], - listResolvers: ProjectRef => UIO[List[Resolver]], - fetchResolver: (Iri, ProjectRef) => IO[ResolverRejection, Resolver], + checkAcls: (ProjectRef, Set[Identity]) => IO[Boolean], + listResolvers: ProjectRef => IO[List[Resolver]], + fetchResolver: (Iri, ProjectRef) => IO[Resolver], fetch: (ResourceRef, ProjectRef) => FetchResource[R] ): ResourceResolution[R] = new ResolverResolution(checkAcls, listResolvers, fetchResolver, fetch, (r: ResourceF[R]) => r.types) @@ -67,7 +67,7 @@ object ResourceResolution { apply( aclCheck, resolvers, - (ref: ResourceRef, project: ProjectRef) => resources.fetch(ref, project).redeem(_ => None, Some(_)), + (ref: ResourceRef, project: ProjectRef) => toCatsIO(resources.fetch(ref, project).redeem(_ => None, Some(_))), Permissions.resources.read ) @@ -84,8 +84,7 @@ object ResourceResolution { apply( aclCheck, resolvers, - (ref: ResourceRef, project: ProjectRef) => - schemas.fetch(ref, project).toBIO[SchemaRejection].redeem(_ => None, Some(_)), + (ref: ResourceRef, project: ProjectRef) => schemas.fetch(ref, project).redeem(_ => None, Some(_)), Permissions.schemas.read ) diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/model/MultiResolutionResult.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/model/MultiResolutionResult.scala index ce5116a1c1..49fadb1496 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/model/MultiResolutionResult.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/model/MultiResolutionResult.scala @@ -5,4 +5,4 @@ import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdContent /** * Result of a MultiResolution */ -final case class MultiResolutionResult[R](report: R, value: JsonLdContent[_, _]) +final case class MultiResolutionResult[+R](report: R, value: JsonLdContent[_, _]) diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/model/Resolver.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/model/Resolver.scala index a8f39b1446..c8d76a4c96 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/model/Resolver.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/model/Resolver.scala @@ -1,5 +1,6 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.contexts import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.ContextValue @@ -104,7 +105,7 @@ object Resolver { def shift(resolvers: Resolvers)(implicit baseUri: BaseUri): Shift = ResourceShift.apply[ResolverState, Resolver]( Resolvers.entityType, - (ref, project) => resolvers.fetch(IdSegmentRef(ref), project), + (ref, project) => resolvers.fetch(IdSegmentRef(ref), project).toBIO[ResolverRejection], state => state.toResource, value => JsonLdContent(value, value.value.source, None) ) diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/model/ResolverResolutionRejection.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/model/ResolverResolutionRejection.scala index 55391b7d7c..39c3c5b837 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/model/ResolverResolutionRejection.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/model/ResolverResolutionRejection.scala @@ -8,6 +8,7 @@ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag +import ch.epfl.bluebrain.nexus.delta.sourcing.rejection.Rejection import io.circe.syntax._ import io.circe.{Encoder, JsonObject} @@ -17,7 +18,7 @@ import io.circe.{Encoder, JsonObject} * @param reason * a descriptive message as to why the rejection occurred */ -sealed abstract class ResolverResolutionRejection(val reason: String) extends Product with Serializable +sealed abstract class ResolverResolutionRejection(val reason: String) extends Rejection object ResolverResolutionRejection { diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResource.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResource.scala index 580614c781..5de2089d00 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResource.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResource.scala @@ -1,6 +1,8 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resources import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri +import cats.syntax.all._ +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, schemas} import ch.epfl.bluebrain.nexus.delta.rdf.graph.Graph import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.ExpandedJsonLd @@ -123,8 +125,11 @@ object ValidateResource { ) = { resourceResolution .resolve(schemaRef, projectRef)(caller) - .mapError(InvalidSchemaRejection(schemaRef, projectRef, _)) - .tapEval(schema => assertNotDeprecated(schema)) - } + .flatMap { result => + val invalidSchema = result.leftMap(InvalidSchemaRejection(schemaRef, projectRef, _)) + IO.fromEither(invalidSchema) + } + .flatTap(schema => assertNotDeprecated(schema)) + }.toBIO[ResourceRejection] } } diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemaImports.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemaImports.scala index bca5065df2..0be3b097d1 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemaImports.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemaImports.scala @@ -1,26 +1,28 @@ package ch.epfl.bluebrain.nexus.delta.sdk.schemas import cats.data.NonEmptyList +import cats.effect.{ContextShift, IO} import cats.implicits._ import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.owl import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.ExpandedJsonLd +import ch.epfl.bluebrain.nexus.delta.sdk.Resolve import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.{Resolvers, ResourceResolution} import ch.epfl.bluebrain.nexus.delta.sdk.resources.Resources import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.Resource +import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.SchemaRejection.InvalidSchemaResolution -import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.{Schema, SchemaRejection} -import ch.epfl.bluebrain.nexus.delta.sdk.Resolve -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.{Resolvers, ResourceResolution} import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} -import monix.bio.IO /** * Resolves the OWL imports from a Schema */ -final class SchemaImports(resolveSchema: Resolve[Schema], resolveResource: Resolve[Resource]) { self => +final class SchemaImports(resolveSchema: Resolve[Schema], resolveResource: Resolve[Resource])(implicit + contextShift: ContextShift[IO] +) { self => /** * Resolve the ''imports'' from the passed ''expanded'' document and recursively from the resolved documents. @@ -36,7 +38,7 @@ final class SchemaImports(resolveSchema: Resolve[Schema], resolveResource: Resol */ def resolve(id: Iri, projectRef: ProjectRef, expanded: ExpandedJsonLd)(implicit caller: Caller - ): IO[SchemaRejection, NonEmptyList[ExpandedJsonLd]] = { + ): IO[NonEmptyList[ExpandedJsonLd]] = { def detectNonOntology(resourceSuccess: Map[ResourceRef, Resource]): Set[ResourceRef] = resourceSuccess.collect { @@ -47,14 +49,14 @@ final class SchemaImports(resolveSchema: Resolve[Schema], resolveResource: Resol schemaRejections: Map[ResourceRef, ResourceResolutionReport], resourceRejections: Map[ResourceRef, ResourceResolutionReport], nonOntologies: Set[ResourceRef] - ): IO[InvalidSchemaResolution, Unit] = - IO.when(resourceRejections.nonEmpty || nonOntologies.nonEmpty)( - IO.raiseError(InvalidSchemaResolution(id, schemaRejections, resourceRejections, nonOntologies)) + ): IO[Unit] = + IO.raiseWhen(resourceRejections.nonEmpty || nonOntologies.nonEmpty)( + InvalidSchemaResolution(id, schemaRejections, resourceRejections, nonOntologies) ) def lookupFromSchemasAndResources( toResolve: Set[ResourceRef] - ): IO[InvalidSchemaResolution, Iterable[ExpandedJsonLd]] = + ): IO[Iterable[ExpandedJsonLd]] = for { (schemaRejections, schemaSuccess) <- lookupInBatch(toResolve, resolveSchema(_, projectRef, caller)) resourcesToResolve = toResolve -- schemaSuccess.keySet @@ -74,15 +76,23 @@ final class SchemaImports(resolveSchema: Resolve[Schema], resolveResource: Resol } } - private def lookupInBatch[A](toResolve: Set[ResourceRef], fetch: ResourceRef => IO[ResourceResolutionReport, A]) = + private def lookupInBatch[A]( + toResolve: Set[ResourceRef], + fetch: ResourceRef => IO[Either[ResourceResolutionReport, A]] + ): IO[(Map[ResourceRef, ResourceResolutionReport], Map[ResourceRef, A])] = toResolve.toList - .parTraverse(ref => fetch(ref).bimap(ref -> _, ref -> _).attempt) + .parTraverse { ref => fetch(ref).map(_.bimap(ref -> _, ref -> _)) } .map(_.partitionMap(identity)) .map { case (rejections, successes) => rejections.toMap -> successes.toMap } } object SchemaImports { + final def alwaysFail(implicit contextShift: ContextShift[IO]) = new SchemaImports( + (_, _, _) => IO.pure(Left(ResourceResolutionReport())), + (_, _, _) => IO.pure(Left(ResourceResolutionReport())) + ) + /** * Construct a [[SchemaImports]]. */ @@ -91,17 +101,17 @@ object SchemaImports { resolvers: Resolvers, schemas: Schemas, resources: Resources - ): SchemaImports = { + )(implicit contextShift: ContextShift[IO]): SchemaImports = { def resolveSchema(ref: ResourceRef, projectRef: ProjectRef, caller: Caller) = ResourceResolution .schemaResource(aclCheck, resolvers, schemas) .resolve(ref, projectRef)(caller) - .map(_.value) + .map(_.map(_.value)) def resolveResource(ref: ResourceRef, projectRef: ProjectRef, caller: Caller) = ResourceResolution .dataResource(aclCheck, resolvers, resources) .resolve(ref, projectRef)(caller) - .map(_.value) + .map(_.map(_.value)) new SchemaImports(resolveSchema, resolveResource) } diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemasImpl.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemasImpl.scala index daa7ba1bb6..bd3b2d8445 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemasImpl.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemasImpl.scala @@ -7,6 +7,7 @@ import ch.epfl.bluebrain.nexus.delta.kernel.kamon.KamonMetricComponent import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, nxv} +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.ExpandedJsonLd import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.JsonLdApi import ch.epfl.bluebrain.nexus.delta.sdk._ import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller @@ -59,7 +60,7 @@ final class SchemasImpl private ( pc <- fetchContext.onCreate(projectRef) iri <- id.traverse(expandIri(_, pc)) jsonLd <- sourceParser(projectRef, pc, iri, source) - expandedResolved <- schemaImports.resolve(jsonLd.iri, projectRef, jsonLd.expanded.addType(nxv.Schema)) + expandedResolved <- resolveImports(jsonLd.iri, projectRef, jsonLd.expanded) } yield CreateSchema(jsonLd.iri, projectRef, source, jsonLd.compacted, expandedResolved, caller.subject) override def update( @@ -72,7 +73,7 @@ final class SchemasImpl private ( pc <- fetchContext.onModify(projectRef) iri <- expandIri(id, pc) (compacted, expanded) <- sourceParser(projectRef, pc, iri, source).map { j => (j.compacted, j.expanded) } - expandedResolved <- schemaImports.resolve(iri, projectRef, expanded.addType(nxv.Schema)) + expandedResolved <- resolveImports(iri, projectRef, expanded) res <- eval(UpdateSchema(iri, projectRef, source, compacted, expandedResolved, rev, caller.subject)) } yield res @@ -87,7 +88,7 @@ final class SchemasImpl private ( iri <- expandIri(id, pc) schema <- log.stateOr(projectRef, iri, SchemaNotFound(iri, projectRef)) (compacted, expanded) <- sourceParser(projectRef, pc, iri, schema.source).map { j => (j.compacted, j.expanded) } - expandedResolved <- schemaImports.resolve(iri, projectRef, expanded.addType(nxv.Schema)) + expandedResolved <- resolveImports(iri, projectRef, expanded) res <- eval(RefreshSchema(iri, projectRef, compacted, expandedResolved, schema.rev, caller.subject)) } yield res @@ -149,6 +150,9 @@ final class SchemasImpl private ( private def dryRun(cmd: SchemaCommand) = log.dryRun(cmd.project, cmd.id, cmd).map(_._2.toResource) + + private def resolveImports(id: Iri, projectRef: ProjectRef, expanded: ExpandedJsonLd)(implicit caller: Caller) = + schemaImports.resolve(id, projectRef, expanded.addType(nxv.Schema)).toBIO[SchemaRejection] } object SchemasImpl { diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/generators/ResolverResolutionGen.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/generators/ResolverResolutionGen.scala index 33bc5c69a1..e697818bb7 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/generators/ResolverResolutionGen.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/generators/ResolverResolutionGen.scala @@ -1,12 +1,12 @@ package ch.epfl.bluebrain.nexus.delta.sdk.generators +import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.Fetch import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.ResolverNotFound import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Identity, ProjectRef, ResourceRef} -import monix.bio.{IO, UIO} object ResolverResolutionGen { @@ -24,7 +24,7 @@ object ResolverResolutionGen { val resolver = ResolverGen.inProject(nxv + "in-project", projectRef) new ResolverResolution( - (_: ProjectRef, _: Set[Identity]) => UIO.pure(false), + (_: ProjectRef, _: Set[Identity]) => IO.pure(false), (_: ProjectRef) => IO.pure(List(resolver)), (resolverId: Iri, p: ProjectRef) => if (resolverId == resolver.id && p == resolver.project) diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/generators/ResourceResolutionGen.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/generators/ResourceResolutionGen.scala index a2c25e0fee..fa3c28c00b 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/generators/ResourceResolutionGen.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/generators/ResourceResolutionGen.scala @@ -1,12 +1,12 @@ package ch.epfl.bluebrain.nexus.delta.sdk.generators +import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv import ch.epfl.bluebrain.nexus.delta.sdk.resolvers import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.{FetchResource, ResourceResolution} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.ResolverNotFound import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Identity, ProjectRef, ResourceRef} -import monix.bio.{IO, UIO} object ResourceResolutionGen { @@ -24,7 +24,7 @@ object ResourceResolutionGen { val resolver = ResolverGen.inProject(nxv + "in-project", projectRef) resolvers.ResourceResolution( - (_: ProjectRef, _: Set[Identity]) => UIO.pure(false), + (_: ProjectRef, _: Set[Identity]) => IO.pure(false), (_: ProjectRef) => IO.pure(List(resolver)), (resolverId: Iri, p: ProjectRef) => if (resolverId == resolver.id && p == resolver.project) diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/MultiResolutionSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/MultiResolutionSuite.scala similarity index 51% rename from delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/MultiResolutionSpec.scala rename to delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/MultiResolutionSuite.scala index f89d386675..3a345ae347 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/MultiResolutionSpec.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/MultiResolutionSuite.scala @@ -1,38 +1,32 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resolvers +import cats.effect.IO +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder import ch.epfl.bluebrain.nexus.delta.sdk.generators.{ResolverResolutionGen, ResourceGen, SchemaGen} import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdContent import ch.epfl.bluebrain.nexus.delta.sdk.model.{IdSegmentRef, ResourceF} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverResolutionRejection.ResourceNotFound import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.{ApiMappings, ProjectContext} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.Fetch -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.{InvalidResolution, InvalidResolverId, InvalidResolverResolution, ProjectContextRejection} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverResolutionRejection.ResourceNotFound +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{MultiResolutionResult, ResourceResolutionReport} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.{InvalidResolution, InvalidResolverResolution, ProjectContextRejection} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport.ResolverReport -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{MultiResolutionResult, ResolverRejection, ResourceResolutionReport} import ch.epfl.bluebrain.nexus.delta.sdk.utils.Fixtures import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.User import ch.epfl.bluebrain.nexus.delta.sourcing.model.ResourceRef.{Latest, Revision} import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef} -import ch.epfl.bluebrain.nexus.testkit.{CirceLiteral, IOValues, TestHelpers} +import ch.epfl.bluebrain.nexus.testkit.ce.{CatsEffectSuite, IOFromMap} +import ch.epfl.bluebrain.nexus.testkit.{CirceLiteral, TestHelpers} import io.circe.Json -import monix.bio.IO -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpecLike - -class MultiResolutionSpec - extends AnyWordSpecLike - with Matchers - with TestHelpers - with IOValues - with CirceLiteral - with Fixtures { + +class MultiResolutionSuite extends CatsEffectSuite with TestHelpers with CirceLiteral with IOFromMap with Fixtures { private val alice = User("alice", Label.unsafe("wonderland")) - implicit val aliceCaller: Caller = Caller(User("alice", Label.unsafe("wonderland")), Set(alice)) + implicit val aliceCaller: Caller = Caller(alice, Set(alice)) private val projectRef = ProjectRef.unsafe("org", "project") @@ -61,12 +55,12 @@ class MultiResolutionSpec def fetch: (ResourceRef, ProjectRef) => Fetch[JsonLdContent[_, _]] = (ref: ResourceRef, _: ProjectRef) => ref match { - case Latest(`resourceId`) => IO.some(resourceValue) - case Revision(_, `schemaId`, _) => IO.some(schemaValue) + case Latest(`resourceId`) => IO.pure(Some(resourceValue)) + case Revision(_, `schemaId`, _) => IO.pure(Some(schemaValue)) case _ => IO.none } - def fetchProject: ProjectRef => IO[ResolverRejection, ProjectContext] = + def fetchProject: ProjectRef => IO[ProjectContext] = FetchContextDummy(Map(projectRef -> ProjectContext.unsafe(ApiMappings.empty, nxv.base, nxv.base))) .mapRejection(ProjectContextRejection) .onRead @@ -77,53 +71,46 @@ class MultiResolutionSpec private val multiResolution = new MultiResolution(fetchProject, resourceResolution) - "A multi-resolution" should { - - "resolve the id as a resource" in { - multiResolution(resourceId, projectRef).accepted shouldEqual - MultiResolutionResult(ResourceResolutionReport(ResolverReport.success(resolverId, projectRef)), resourceValue) - } - - "resolve the id as a resource with a specific resolver" in { - multiResolution(resourceId, projectRef, resolverId).accepted shouldEqual - MultiResolutionResult(ResolverReport.success(resolverId, projectRef), resourceValue) - } - - "resolve the id as a schema" in { - multiResolution(IdSegmentRef(schemaId, 5), projectRef).accepted shouldEqual - MultiResolutionResult(ResourceResolutionReport(ResolverReport.success(resolverId, projectRef)), schemaValue) - } - - "resolve the id as a schema with a specific resolver" in { - multiResolution(IdSegmentRef(schemaId, 5), projectRef, resolverId).accepted shouldEqual - MultiResolutionResult(ResolverReport.success(resolverId, projectRef), schemaValue) - } - - "fail when it can't be resolved neither as a resource or a schema" in { - multiResolution(unknownResourceId, projectRef).rejected shouldEqual - InvalidResolution( - unknownResourceRef, - projectRef, - ResourceResolutionReport( - ResolverReport.failed(resolverId, projectRef -> ResourceNotFound(unknownResourceId, projectRef)) - ) - ) - } - - "fail with a specific resolver when it can't be resolved neither as a resource or a schema" in { - multiResolution(unknownResourceId, projectRef, resolverId).rejected shouldEqual - InvalidResolverResolution( - unknownResourceRef, - resolverId, - projectRef, - ResolverReport.failed(resolverId, projectRef -> ResourceNotFound(unknownResourceId, projectRef)) - ) - } - - "fail with an invalid resolver id" in { - val invalid = "qa$%" - multiResolution(resourceId, projectRef, invalid).rejected shouldEqual InvalidResolverId(invalid) - } + test("Resolve the id as a resource") { + val expected = + MultiResolutionResult(ResourceResolutionReport(ResolverReport.success(resolverId, projectRef)), resourceValue) + multiResolution(resourceId, projectRef).assertEquals(expected) + } + + test("Resolve the id as a resource with a specific resolver") { + val expected = MultiResolutionResult(ResolverReport.success(resolverId, projectRef), resourceValue) + multiResolution(resourceId, projectRef, resolverId).assertEquals(expected) + } + + test("Resolve the id as a schema") { + val expected = + MultiResolutionResult(ResourceResolutionReport(ResolverReport.success(resolverId, projectRef)), schemaValue) + multiResolution(IdSegmentRef(schemaId, 5), projectRef).assertEquals(expected) + } + + test("Resolve the id as a schema with a specific resolver") { + val expected = MultiResolutionResult(ResolverReport.success(resolverId, projectRef), schemaValue) + multiResolution(IdSegmentRef(schemaId, 5), projectRef, resolverId).assertEquals(expected) } + test("Fail when it can't be resolved neither as a resource or a schema") { + val expectedError = InvalidResolution( + unknownResourceRef, + projectRef, + ResourceResolutionReport( + ResolverReport.failed(resolverId, projectRef -> ResourceNotFound(unknownResourceId, projectRef)) + ) + ) + multiResolution(unknownResourceId, projectRef).intercept(expectedError) + } + + test("Fail with a specific resolver when it can't be resolved neither as a resource or a schema") { + val expectedError = InvalidResolverResolution( + unknownResourceRef, + resolverId, + projectRef, + ResolverReport.failed(resolverId, projectRef -> ResourceNotFound(unknownResourceId, projectRef)) + ) + multiResolution(unknownResourceId, projectRef, resolverId).intercept(expectedError) + } } diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverContextResolutionSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverContextResolutionSuite.scala similarity index 72% rename from delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverContextResolutionSpec.scala rename to delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverContextResolutionSuite.scala index 837c265b17..ee99041de8 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverContextResolutionSpec.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverContextResolutionSuite.scala @@ -1,32 +1,33 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resolvers import akka.http.scaladsl.model.Uri +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ +import cats.effect.IO +import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, nxv, schemas} +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.{CompactedJsonLd, ExpandedJsonLd} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContext.StaticContext import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolutionError.RemoteContextNotAccessible import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution} -import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.{CompactedJsonLd, ExpandedJsonLd} import ch.epfl.bluebrain.nexus.delta.sdk.generators.ResourceResolutionGen import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller +import ch.epfl.bluebrain.nexus.testkit.TestHelpers import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ -import ch.epfl.bluebrain.nexus.delta.sdk.model._ +import ch.epfl.bluebrain.nexus.delta.sdk.model.{ResourceF, ResourceUris, Tags} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution.ProjectRemoteContext import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.FetchResource import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.Resource import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.User import ch.epfl.bluebrain.nexus.delta.sourcing.model.ResourceRef.Latest import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef} -import ch.epfl.bluebrain.nexus.testkit.{IOValues, TestHelpers} +import ch.epfl.bluebrain.nexus.testkit.ce.CatsEffectSuite import io.circe.Json import io.circe.syntax._ -import monix.bio.IO -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpecLike import java.time.Instant -class ResolverContextResolutionSpec extends AnyWordSpecLike with IOValues with TestHelpers with Matchers { +class ResolverContextResolutionSuite extends CatsEffectSuite with TestHelpers { private val metadataContext = jsonContentOf("/contexts/metadata.json").topContextValueOrEmpty @@ -65,7 +66,7 @@ class ResolverContextResolutionSpec extends AnyWordSpecLike with IOValues with T def fetchResource: (ResourceRef, ProjectRef) => FetchResource[Resource] = { (r: ResourceRef, p: ProjectRef) => (r, p) match { - case (Latest(id), `project`) if resourceId == id => IO.some(resource) + case (Latest(id), `project`) if resourceId == id => IO.pure(Some(resource)) case _ => IO.none } } @@ -74,23 +75,20 @@ class ResolverContextResolutionSpec extends AnyWordSpecLike with IOValues with T private val resolverContextResolution = ResolverContextResolution(rcr, resourceResolution) - "Resolving contexts" should { - - "resolve correctly static contexts" in { - val expected = StaticContext(contexts.metadata, metadataContext) - resolverContextResolution(project).resolve(contexts.metadata).accepted shouldEqual expected - } + private def resolve(iri: Iri) = + toCatsIO(resolverContextResolution(project).resolve(iri)) - "resolve correctly a resource context" in { - val expected = ProjectRemoteContext(resourceId, project, 5, ContextValue(context)) - resolverContextResolution(project).resolve(resourceId).accepted shouldEqual expected - } + test("Resolve correctly static contexts") { + val expected = StaticContext(contexts.metadata, metadataContext) + resolve(contexts.metadata).assertEquals(expected) + } - "fail is applying for an unknown resource" in { - resolverContextResolution(project) - .resolve(nxv + "xxx") - .rejectedWith[RemoteContextNotAccessible] - } + test("Resolve correctly a resource context") { + val expected = ProjectRemoteContext(resourceId, project, 5, ContextValue(context)) + resolve(resourceId).assertEquals(expected) } + test("Fail is applying for an unknown resource") { + resolve(nxv + "xxx").intercept[RemoteContextNotAccessible] + } } diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverResolutionSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverResolutionSpec.scala deleted file mode 100644 index 9192275c99..0000000000 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverResolutionSpec.scala +++ /dev/null @@ -1,372 +0,0 @@ -package ch.epfl.bluebrain.nexus.delta.sdk.resolvers - -import akka.http.scaladsl.model.Uri -import cats.data.NonEmptyList -import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri -import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{nxv, schemas} -import ch.epfl.bluebrain.nexus.delta.sdk -import ch.epfl.bluebrain.nexus.delta.sdk.generators.ResolverGen -import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller -import ch.epfl.bluebrain.nexus.delta.sdk.model._ -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.FetchResource -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolutionSpec.ResourceExample -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.IdentityResolution.{ProvidedIdentities, UseCurrentCaller} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.Resolver.CrossProjectResolver -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.ResolverNotFound -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverResolutionRejection.{ProjectAccessDenied, ResourceNotFound, ResourceTypesDenied, WrappedResolverRejection} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverValue.CrossProjectValue -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport.ResolverReport -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{IdentityResolution, Priority, Resolver, ResolverRejection, ResourceResolutionReport} -import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.User -import ch.epfl.bluebrain.nexus.delta.sourcing.model.ResourceRef.Latest -import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Identity, Label, ProjectRef, ResourceRef} -import ch.epfl.bluebrain.nexus.testkit.IOValues -import io.circe.Json -import monix.bio.{IO, UIO} -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpecLike -import org.scalatest.{Inspectors, OptionValues} - -import java.time.Instant - -class ResolverResolutionSpec extends AnyWordSpecLike with Matchers with IOValues with OptionValues with Inspectors { - - private val alice = User("alice", Label.unsafe("wonderland")) - private val bob = User("bob", Label.unsafe("wonderland")) - - implicit val aliceCaller: Caller = Caller(alice, Set(alice)) - - private val project1 = ProjectRef.unsafe("org", "project1") - private val project2 = ProjectRef.unsafe("org", "project2") - private val project3 = ProjectRef.unsafe("org", "project3") - - val checkAcls: (ProjectRef, Set[Identity]) => UIO[Boolean] = - (p: ProjectRef, identities: Set[Identity]) => - p match { - case `project1` if identities == Set(alice) || identities == Set(bob) => UIO.pure(true) - case `project2` if identities == Set(bob) => UIO.pure(true) - case `project3` if identities == Set(alice) => UIO.pure(true) - case _ => UIO.pure(false) - } - - private val resource = ResourceF( - id = nxv + "example1", - uris = ResourceUris(Uri("/example1")), - rev = 5, - types = Set(nxv + "ResourceExample", nxv + "ResourceExample2"), - deprecated = false, - createdAt = Instant.now(), - createdBy = alice, - updatedAt = Instant.now(), - updatedBy = alice, - schema = Latest(schemas + "ResourceExample"), - value = ResourceExample("myResource") - ) - - private val inProjectResolver = ResolverGen.inProject(nxv + "in-project-proj-1", project1) - - def crossProjectResolver( - id: String, - priority: Int, - resourceTypes: Set[Iri] = Set.empty, - projects: NonEmptyList[ProjectRef] = NonEmptyList.of(project1, project2, project3), - identityResolution: IdentityResolution = UseCurrentCaller - ): CrossProjectResolver = - CrossProjectResolver( - nxv + id, - project1, - CrossProjectValue( - Priority.unsafe(priority), - resourceTypes, - projects, - identityResolution - ), - Json.obj(), - Tags.empty - ) - - def listResolvers(resolvers: List[Resolver]): ProjectRef => UIO[List[Resolver]] = (_: ProjectRef) => - IO.pure(resolvers) - private val emptyResolverListQuery = listResolvers(List.empty[Resolver]) - - val noResolverFetch: (Iri, ProjectRef) => IO[ResolverNotFound, Nothing] = - (_: Iri, projectRef: ProjectRef) => IO.raiseError(ResolverNotFound(nxv + "not-found", projectRef)) - def fetchResolver(resolver: Resolver): (Iri, ProjectRef) => IO[ResolverRejection, Resolver] = - (id: Iri, projectRef: ProjectRef) => - if (id == resolver.id) IO.pure(resolver) - else IO.raiseError(ResolverNotFound(id, projectRef)) - - def fetchResource( - projectRef: ProjectRef - ): (ResourceRef, ProjectRef) => FetchResource[ResourceExample] = - (_: ResourceRef, p: ProjectRef) => - p match { - case `projectRef` => UIO.some(resource) - case _ => UIO.none - } - - "The Resource resolution" when { - - def singleResolverResolution(resourceProject: ProjectRef, resolver: Resolver) = - ResourceResolution( - checkAcls, - emptyResolverListQuery, - fetchResolver(resolver), - fetchResource(resourceProject) - ) - - def multipleResolverResolution(resourceProject: ProjectRef, resolvers: Resolver*) = - sdk.resolvers.ResourceResolution( - checkAcls, - listResolvers(resolvers.toList), - noResolverFetch, - fetchResource(resourceProject) - ) - - "resolving with an in-project resolver" should { - val resourceResolution = singleResolverResolution(project1, inProjectResolver) - - "fail if the resolver can't be found" in { - val unknown = nxv + "xxx" - resourceResolution - .resolve(Latest(resource.id), project1, unknown) - .rejected shouldEqual ResolverReport.failed( - unknown, - project1 -> WrappedResolverRejection(ResolverNotFound(unknown, project1)) - ) - } - - "fail if the resource can't be found in the project" in { - val (report, result) = resourceResolution - .resolveReport( - Latest(resource.id), - project2, - inProjectResolver.id - ) - .accepted - - report shouldEqual ResolverReport.failed( - inProjectResolver.id, - project2 -> ResourceNotFound(resource.id, project2) - ) - result shouldEqual None - } - - "be successful if the resource can be fetched" in { - val (report, result) = - resourceResolution.resolveReport(Latest(resource.id), project1, inProjectResolver.id).accepted - - report shouldEqual ResolverReport.success(inProjectResolver.id, project1) - result.value shouldEqual resource - } - } - - "resolving with a cross-project resolver with using current caller resolution" should { - "succeed at 3rd project" in { - forAll( - List( - crossProjectResolver("use-current", 40, identityResolution = UseCurrentCaller), - crossProjectResolver( - "use-current", - 40, - resourceTypes = resource.types + nxv.Schema, - identityResolution = UseCurrentCaller - ) - ) - ) { resolver => - val (report, result) = singleResolverResolution(project3, resolver) - .resolveReport(Latest(resource.id), project1, resolver.id) - .accepted - - report shouldEqual ResolverReport.success( - resolver.id, - project3, - project1 -> ResourceNotFound(resource.id, project1), - project2 -> ProjectAccessDenied(project2, UseCurrentCaller) - ) - result.value shouldEqual resource - } - } - - "fail if the caller has no access to the resource project" in { - val resolver = crossProjectResolver( - "use-current", - 40, - identityResolution = UseCurrentCaller - ) - val (report, result) = singleResolverResolution(project2, resolver) - .resolveReport(Latest(resource.id), project1, resolver.id) - .accepted - - report shouldEqual ResolverReport.failed( - resolver.id, - project1 -> ResourceNotFound(resource.id, project1), - project2 -> ProjectAccessDenied(project2, UseCurrentCaller), - project3 -> ResourceNotFound(resource.id, project3) - ) - result shouldEqual None - } - - "fail if the resource type is not defined in the cross project resolver" in { - val resolver = crossProjectResolver( - "use-current", - 40, - resourceTypes = Set(nxv.Schema), - identityResolution = UseCurrentCaller - ) - - val resourceResolution = singleResolverResolution(project3, resolver) - - val (report, result) = resourceResolution - .resolveReport(Latest(resource.id), project1, resolver.id) - .accepted - - report shouldEqual ResolverReport.failed( - resolver.id, - project1 -> ResourceNotFound(resource.id, project1), - project2 -> ProjectAccessDenied(project2, UseCurrentCaller), - project3 -> ResourceTypesDenied(project3, resource.types) - ) - result shouldEqual None - } - - } - - "resolving with a cross-project resolver with using provided entities resolution" should { - "succeed at 2nd project" in { - forAll( - List( - crossProjectResolver("provided-entities", 40, identityResolution = ProvidedIdentities(Set(bob))), - crossProjectResolver( - "provided-entities", - 40, - resourceTypes = resource.types + nxv.Schema, - identityResolution = ProvidedIdentities(Set(bob)) - ) - ) - ) { resolver => - val (report, result) = singleResolverResolution(project2, resolver) - .resolveReport(Latest(resource.id), project1, resolver.id) - .accepted - - report shouldEqual ResolverReport.success( - resolver.id, - project2, - project1 -> ResourceNotFound(resource.id, project1) - ) - result.value shouldEqual resource - } - } - - "fail if the provided entity has no access to the resource project" in { - val resolver = crossProjectResolver( - "provided-entities", - 40, - identityResolution = ProvidedIdentities(Set(bob)) - ) - val (report, result) = singleResolverResolution(project3, resolver) - .resolveReport(Latest(resource.id), project1, resolver.id) - .accepted - - report shouldEqual ResolverReport.failed( - resolver.id, - project1 -> ResourceNotFound(resource.id, project1), - project2 -> ResourceNotFound(resource.id, project2), - project3 -> ProjectAccessDenied(project3, ProvidedIdentities(Set(bob))) - ) - result shouldEqual None - } - } - - "resolving with multiple resolvers" should { - - "be successful with the in-project resolver after failing a first time" in { - val resolution = multipleResolverResolution( - project1, - crossProjectResolver("cross-project-1", priority = 10, resourceTypes = Set(nxv.Schema)), - crossProjectResolver("cross-project-2", priority = 40), - inProjectResolver - ) - - val (report, result) = resolution.resolveReport(Latest(resource.id), project1).accepted - - report shouldEqual ResourceResolutionReport( - ResolverReport.failed( - nxv + "cross-project-1", - project1 -> ResourceTypesDenied(project1, resource.types), - project2 -> ProjectAccessDenied(project2, UseCurrentCaller), - project3 -> ResourceNotFound(resource.id, project3) - ), - ResolverReport.success(inProjectResolver.id, project1) - ) - - result.value shouldEqual resource - } - - "be successful with the last resolver" in { - val resolution = multipleResolverResolution( - project3, - crossProjectResolver("cross-project-1", priority = 10, resourceTypes = Set(nxv.Schema)), - crossProjectResolver("cross-project-2", priority = 40, projects = NonEmptyList.of(project3)), - inProjectResolver - ) - - val (report, result) = resolution.resolveReport(Latest(resource.id), project1).accepted - - report shouldEqual ResourceResolutionReport( - ResolverReport.failed( - nxv + "cross-project-1", - project1 -> ResourceNotFound(resource.id, project1), - project2 -> ProjectAccessDenied(project2, UseCurrentCaller), - project3 -> ResourceTypesDenied(project3, resource.types) - ), - ResolverReport.failed( - inProjectResolver.id, - project1 -> ResourceNotFound(resource.id, project1) - ), - ResolverReport.success(nxv + "cross-project-2", project3) - ) - - result.value shouldEqual resource - } - - "fail if no resolver matches" in { - val resolution = multipleResolverResolution( - project2, - crossProjectResolver("cross-project-1", priority = 10, resourceTypes = Set(nxv.Schema)), - crossProjectResolver("cross-project-2", priority = 40, projects = NonEmptyList.of(project3)), - inProjectResolver - ) - - val (report, result) = resolution.resolveReport(Latest(resource.id), project1).accepted - - report shouldEqual ResourceResolutionReport( - ResolverReport.failed( - nxv + "cross-project-1", - project1 -> ResourceNotFound(resource.id, project1), - project2 -> ProjectAccessDenied(project2, UseCurrentCaller), - project3 -> ResourceNotFound(resource.id, project3) - ), - ResolverReport.failed( - inProjectResolver.id, - project1 -> ResourceNotFound(resource.id, project1) - ), - ResolverReport.failed( - nxv + "cross-project-2", - project3 -> ResourceNotFound(resource.id, project3) - ) - ) - result shouldEqual None - } - - } - - } - -} - -object ResolverResolutionSpec { - - final case class ResourceExample(value: String) - -} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverResolutionSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverResolutionSuite.scala new file mode 100644 index 0000000000..9da87bc9e1 --- /dev/null +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverResolutionSuite.scala @@ -0,0 +1,360 @@ +package ch.epfl.bluebrain.nexus.delta.sdk.resolvers + +import akka.http.scaladsl.model.Uri +import cats.data.NonEmptyList +import cats.effect.IO +import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri +import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{nxv, schemas} +import ch.epfl.bluebrain.nexus.delta.sdk.generators.ResolverGen +import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller +import ch.epfl.bluebrain.nexus.delta.sdk.model.{ResourceF, ResourceUris, Tags} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.FetchResource +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolutionSuite.ResourceExample +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.IdentityResolution.{ProvidedIdentities, UseCurrentCaller} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.Resolver.CrossProjectResolver +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.ResolverNotFound +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverResolutionRejection.{ProjectAccessDenied, ResourceNotFound, ResourceTypesDenied, WrappedResolverRejection} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverValue.CrossProjectValue +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport.ResolverReport +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{IdentityResolution, Priority, Resolver, ResourceResolutionReport} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.User +import ch.epfl.bluebrain.nexus.delta.sourcing.model.ResourceRef.Latest +import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Identity, Label, ProjectRef, ResourceRef} +import ch.epfl.bluebrain.nexus.testkit.ce.CatsEffectSuite +import io.circe.Json + +import java.time.Instant + +class ResolverResolutionSuite extends CatsEffectSuite { + + private val realm = Label.unsafe("wonderland") + private val alice = User("alice", realm) + private val bob = User("bob", realm) + + implicit val aliceCaller: Caller = Caller(alice, Set(alice)) + + private val project1 = ProjectRef.unsafe("org", "project1") + private val project2 = ProjectRef.unsafe("org", "project2") + private val project3 = ProjectRef.unsafe("org", "project3") + + private val checkAcls: (ProjectRef, Set[Identity]) => IO[Boolean] = + (p: ProjectRef, identities: Set[Identity]) => + p match { + case `project1` if identities == Set(alice) || identities == Set(bob) => IO.pure(true) + case `project2` if identities == Set(bob) => IO.pure(true) + case `project3` if identities == Set(alice) => IO.pure(true) + case _ => IO.pure(false) + } + + private val resource = ResourceF( + id = nxv + "example1", + uris = ResourceUris(Uri("/example1")), + rev = 5, + types = Set(nxv + "ResourceExample", nxv + "ResourceExample2"), + deprecated = false, + createdAt = Instant.now(), + createdBy = alice, + updatedAt = Instant.now(), + updatedBy = alice, + schema = Latest(schemas + "ResourceExample"), + value = ResourceExample("myResource") + ) + + private val inProjectResolver = ResolverGen.inProject(nxv + "in-project-proj-1", project1) + + private def crossProjectResolver( + id: String, + priority: Int, + resourceTypes: Set[Iri] = Set.empty, + projects: NonEmptyList[ProjectRef] = NonEmptyList.of(project1, project2, project3), + identityResolution: IdentityResolution = UseCurrentCaller + ): CrossProjectResolver = + CrossProjectResolver( + nxv + id, + project1, + CrossProjectValue( + Priority.unsafe(priority), + resourceTypes, + projects, + identityResolution + ), + Json.obj(), + Tags.empty + ) + + def listResolvers(resolvers: List[Resolver]): ProjectRef => IO[List[Resolver]] = (_: ProjectRef) => IO.pure(resolvers) + + private val emptyResolverListQuery = listResolvers(List.empty[Resolver]) + + val noResolverFetch: (Iri, ProjectRef) => IO[Nothing] = + (_: Iri, projectRef: ProjectRef) => IO.raiseError(ResolverNotFound(nxv + "not-found", projectRef)) + + def fetchResolver(resolver: Resolver): (Iri, ProjectRef) => IO[Resolver] = + (id: Iri, projectRef: ProjectRef) => + if (id == resolver.id) IO.pure(resolver) + else IO.raiseError(ResolverNotFound(id, projectRef)) + + def fetchResource( + projectRef: ProjectRef + ): (ResourceRef, ProjectRef) => FetchResource[ResourceExample] = + (_: ResourceRef, p: ProjectRef) => + p match { + case `projectRef` => IO.pure(Some(resource)) + case _ => IO.none + } + + private def singleResolverResolution(resourceProject: ProjectRef, resolver: Resolver) = + ResourceResolution( + checkAcls, + emptyResolverListQuery, + fetchResolver(resolver), + fetchResource(resourceProject) + ) + + private def multipleResolverResolution(resourceProject: ProjectRef, resolvers: Resolver*) = + ResourceResolution( + checkAcls, + listResolvers(resolvers.toList), + noResolverFetch, + fetchResource(resourceProject) + ) + + private val inProjectResolution = singleResolverResolution(project1, inProjectResolver) + + private val resource1NotFound = None + private val resource1Found = Some(resource) + + test("Using an in-project resolver fails if the resolver can't be found") { + + val unknown = nxv + "xxx" + val expectedError = ResolverReport.failed( + unknown, + project1 -> WrappedResolverRejection(ResolverNotFound(unknown, project1)) + ) + inProjectResolution + .resolve(Latest(resource.id), project1, unknown) + .assertEquals(Left(expectedError)) + } + + test("Using an in-project resolver fails if the resource can't be found in the project") { + val expectedReport = ResolverReport.failed( + inProjectResolver.id, + project2 -> ResourceNotFound(resource.id, project2) + ) + inProjectResolution + .resolveReport( + Latest(resource.id), + project2, + inProjectResolver.id + ) + .assertEquals((expectedReport, resource1NotFound)) + } + + test("Using an in-project resolver succeeds if the resource can be fetched") { + val expectedReport = ResolverReport.success(inProjectResolver.id, project1) + val expectedResult = Some(resource) + inProjectResolution + .resolveReport(Latest(resource.id), project1, inProjectResolver.id) + .assertEquals((expectedReport, expectedResult)) + } + + test("Using a cross-project resolver with current caller succeeds") { + val resolver = crossProjectResolver("use-current", 40, identityResolution = UseCurrentCaller) + val resolverResolution = singleResolverResolution(project3, resolver) + + val successAtProject3 = ResolverReport.success( + resolver.id, + project3, + project1 -> ResourceNotFound(resource.id, project1), + project2 -> ProjectAccessDenied(project2, UseCurrentCaller) + ) + + resolverResolution + .resolveReport(Latest(resource.id), project1, resolver.id) + .assertEquals((successAtProject3, resource1Found)) + } + + test("Using a cross-project resolver with current caller and limiting on types succeeds") { + val acceptedTypes = resource.types + nxv.Schema + val resolver = + crossProjectResolver("use-current", 40, resourceTypes = acceptedTypes, identityResolution = UseCurrentCaller) + val resolverResolution = singleResolverResolution(project3, resolver) + + val successAtProject3 = ResolverReport.success( + resolver.id, + project3, + project1 -> ResourceNotFound(resource.id, project1), + project2 -> ProjectAccessDenied(project2, UseCurrentCaller) + ) + + resolverResolution + .resolveReport(Latest(resource.id), project1, resolver.id) + .assertEquals((successAtProject3, resource1Found)) + } + + test("Using a cross-project resolver with current caller fails if the caller has no access to the project") { + val resolver = crossProjectResolver("use-current", 40, identityResolution = UseCurrentCaller) + val resolverResolution = singleResolverResolution(project2, resolver) + + val failedReport = ResolverReport.failed( + resolver.id, + project1 -> ResourceNotFound(resource.id, project1), + project2 -> ProjectAccessDenied(project2, UseCurrentCaller), + project3 -> ResourceNotFound(resource.id, project3) + ) + + resolverResolution + .resolveReport(Latest(resource.id), project1, resolver.id) + .assertEquals((failedReport, resource1NotFound)) + } + + test("Using a cross-project resolver with current caller fails if the resource type is not defined") { + val acceptedTypes = Set(nxv.Schema) + val resolver = + crossProjectResolver("use-current", 40, resourceTypes = acceptedTypes, identityResolution = UseCurrentCaller) + val resolverResolution = singleResolverResolution(project3, resolver) + + val failedReport = ResolverReport.failed( + resolver.id, + project1 -> ResourceNotFound(resource.id, project1), + project2 -> ProjectAccessDenied(project2, UseCurrentCaller), + project3 -> ResourceTypesDenied(project3, resource.types) + ) + + resolverResolution + .resolveReport(Latest(resource.id), project1, resolver.id) + .assertEquals((failedReport, resource1NotFound)) + } + + test("Using a cross-project resolver with provided identities succeeds") { + val resolver = crossProjectResolver("provided-identities", 40, identityResolution = ProvidedIdentities(Set(bob))) + val resolverResolution = singleResolverResolution(project2, resolver) + + val successAtProject2 = ResolverReport.success( + resolver.id, + project2, + project1 -> ResourceNotFound(resource.id, project1) + ) + + resolverResolution + .resolveReport(Latest(resource.id), project1, resolver.id) + .assertEquals((successAtProject2, resource1Found)) + } + + test("Using a cross-project resolver with provided identities and limiting on types succeeds") { + val acceptedTypes = resource.types + nxv.Schema + val resolver = crossProjectResolver( + "provided-identities", + 40, + resourceTypes = acceptedTypes, + identityResolution = ProvidedIdentities(Set(bob)) + ) + val resolverResolution = singleResolverResolution(project2, resolver) + + val successAtProject2 = ResolverReport.success( + resolver.id, + project2, + project1 -> ResourceNotFound(resource.id, project1) + ) + + resolverResolution + .resolveReport(Latest(resource.id), project1, resolver.id) + .assertEquals((successAtProject2, resource1Found)) + } + + test("Using a cross-project resolver with provided identities fail if the identity has no access") { + val resolver = crossProjectResolver("provided-identities", 40, identityResolution = ProvidedIdentities(Set(bob))) + val resolverResolution = singleResolverResolution(project3, resolver) + + val failedReport = ResolverReport.failed( + resolver.id, + project1 -> ResourceNotFound(resource.id, project1), + project2 -> ResourceNotFound(resource.id, project2), + project3 -> ProjectAccessDenied(project3, ProvidedIdentities(Set(bob))) + ) + + resolverResolution + .resolveReport(Latest(resource.id), project1, resolver.id) + .assertEquals((failedReport, resource1NotFound)) + } + + test("Using multiple resolvers succeeds after a first failure") { + val resolution = multipleResolverResolution( + project1, + crossProjectResolver("cross-project-1", priority = 10, resourceTypes = Set(nxv.Schema)), + crossProjectResolver("cross-project-2", priority = 40), + inProjectResolver + ) + + val expectedReport = ResourceResolutionReport( + ResolverReport.failed( + nxv + "cross-project-1", + project1 -> ResourceTypesDenied(project1, resource.types), + project2 -> ProjectAccessDenied(project2, UseCurrentCaller), + project3 -> ResourceNotFound(resource.id, project3) + ), + ResolverReport.success(inProjectResolver.id, project1) + ) + + resolution.resolveReport(Latest(resource.id), project1).assertEquals((expectedReport, resource1Found)) + } + + test("Using multiple resolvers succeeds with the last resolver") { + val resolution = multipleResolverResolution( + project3, + crossProjectResolver("cross-project-1", priority = 10, resourceTypes = Set(nxv.Schema)), + crossProjectResolver("cross-project-2", priority = 40, projects = NonEmptyList.of(project3)), + inProjectResolver + ) + + val expectedReport = ResourceResolutionReport( + ResolverReport.failed( + nxv + "cross-project-1", + project1 -> ResourceNotFound(resource.id, project1), + project2 -> ProjectAccessDenied(project2, UseCurrentCaller), + project3 -> ResourceTypesDenied(project3, resource.types) + ), + ResolverReport.failed( + inProjectResolver.id, + project1 -> ResourceNotFound(resource.id, project1) + ), + ResolverReport.success(nxv + "cross-project-2", project3) + ) + + resolution.resolveReport(Latest(resource.id), project1).assertEquals((expectedReport, resource1Found)) + } + + test("Using multiple resolvers fails if no resolver matches") { + val resolution = multipleResolverResolution( + project2, + crossProjectResolver("cross-project-1", priority = 10, resourceTypes = Set(nxv.Schema)), + crossProjectResolver("cross-project-2", priority = 40, projects = NonEmptyList.of(project3)), + inProjectResolver + ) + + val expectedReport = ResourceResolutionReport( + ResolverReport.failed( + nxv + "cross-project-1", + project1 -> ResourceNotFound(resource.id, project1), + project2 -> ProjectAccessDenied(project2, UseCurrentCaller), + project3 -> ResourceNotFound(resource.id, project3) + ), + ResolverReport.failed( + inProjectResolver.id, + project1 -> ResourceNotFound(resource.id, project1) + ), + ResolverReport.failed( + nxv + "cross-project-2", + project3 -> ResourceNotFound(resource.id, project3) + ) + ) + + resolution.resolveReport(Latest(resource.id), project1).assertEquals((expectedReport, resource1NotFound)) + } + +} + +object ResolverResolutionSuite { + final case class ResourceExample(value: String) + +} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverScopeInitializationSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverScopeInitializationSpec.scala deleted file mode 100644 index fb23ce6d28..0000000000 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverScopeInitializationSpec.scala +++ /dev/null @@ -1,85 +0,0 @@ -package ch.epfl.bluebrain.nexus.delta.sdk.resolvers - -import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF -import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, nxv, schema} -import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi} -import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution -import ch.epfl.bluebrain.nexus.delta.sdk.generators.ProjectGen -import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.ServiceAccount -import ch.epfl.bluebrain.nexus.delta.sdk.model.IdSegment -import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy -import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.{ProjectContextRejection, ResolverNotFound} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverValue.InProjectValue -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{Priority, ResourceResolutionReport} -import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ -import ch.epfl.bluebrain.nexus.delta.sdk.{ConfigFixtures, Defaults} -import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Subject, User} -import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label -import ch.epfl.bluebrain.nexus.delta.sourcing.postgres.DoobieScalaTestFixture -import ch.epfl.bluebrain.nexus.testkit.ce.CatsIOValues -import ch.epfl.bluebrain.nexus.testkit.{IOFixedClock, TestHelpers} -import monix.bio.IO -import org.scalatest.matchers.should.Matchers - -import java.util.UUID - -class ResolverScopeInitializationSpec - extends DoobieScalaTestFixture - with Matchers - with CatsIOValues - with IOFixedClock - with TestHelpers - with ConfigFixtures { - - private val defaultInProjectResolverId: IdSegment = nxv.defaultResolver - - private val uuid = UUID.randomUUID() - implicit private val uuidF: UUIDF = UUIDF.fixed(uuid) - private val saRealm: Label = Label.unsafe("service-accounts") - private val usersRealm: Label = Label.unsafe("users") - implicit private val sa: ServiceAccount = ServiceAccount(User("nexus-sa", saRealm)) - implicit private val bob: Subject = User("bob", usersRealm) - - private val am = ApiMappings("nxv" -> nxv.base, "Person" -> schema.Person) - private val projBase = nxv.base - private val project = - ProjectGen.project("org", "project", uuid = uuid, orgUuid = uuid, base = projBase, mappings = am) - - private val defaults = Defaults("resolverName", "resolverDescription") - - private lazy val resolvers: Resolvers = { - implicit val api: JsonLdApi = JsonLdJavaApi.strict - val resolution = RemoteContextResolution.fixed( - contexts.resolvers -> jsonContentOf("/contexts/resolvers.json").topContextValueOrEmpty - ) - val rcr = new ResolverContextResolution(resolution, (_, _, _) => IO.raiseError(ResourceResolutionReport())) - ResolversImpl( - FetchContextDummy(List(project), ProjectContextRejection), - rcr, - ResolversConfig(eventLogConfig, pagination, defaults), - xas - ) - } - "A ResolverScopeInitialization" should { - lazy val init = new ResolverScopeInitialization(resolvers, sa, defaults) - - "create a default resolver on newly created project" in { - resolvers.fetch(defaultInProjectResolverId, project.ref).rejectedWith[ResolverNotFound] - init.onProjectCreation(project, bob).accepted - val resource = resolvers.fetch(defaultInProjectResolverId, project.ref).accepted - resource.value.value shouldEqual - InProjectValue(Some(defaults.name), Some(defaults.description), Priority.unsafe(1)) - resource.rev shouldEqual 1L - resource.createdBy shouldEqual sa.caller.subject - } - - "not create a new resolver if one already exists" in { - resolvers.fetch(defaultInProjectResolverId, project.ref).accepted.rev shouldEqual 1L - init.onProjectCreation(project, bob).accepted - val resource = resolvers.fetch(defaultInProjectResolverId, project.ref).accepted - resource.rev shouldEqual 1L - } - } - -} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverScopeInitializationSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverScopeInitializationSuite.scala new file mode 100644 index 0000000000..1a4904fc6c --- /dev/null +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverScopeInitializationSuite.scala @@ -0,0 +1,53 @@ +package ch.epfl.bluebrain.nexus.delta.sdk.resolvers + +import cats.effect.IO +import cats.effect.concurrent.Ref +import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv +import ch.epfl.bluebrain.nexus.delta.sdk.Defaults +import ch.epfl.bluebrain.nexus.delta.sdk.error.ServiceError.ScopeInitializationFailed +import ch.epfl.bluebrain.nexus.delta.sdk.generators.ProjectGen +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.ResourceAlreadyExists +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverValue.InProjectValue +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{Priority, ResolverValue} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Subject, User} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label +import ch.epfl.bluebrain.nexus.testkit.ce.CatsEffectSuite + +class ResolverScopeInitializationSuite extends CatsEffectSuite { + + private val defaults = Defaults("resolverName", "resolverDescription") + + private val project = ProjectGen.project("org", "project") + + private val usersRealm: Label = Label.unsafe("users") + private val bob: Subject = User("bob", usersRealm) + + test("Succeeds") { + for { + ref <- Ref.of[IO, Option[ResolverValue]](None) + scopeInit = new ResolverScopeInitialization( + (_, resolver) => ref.set(Some(resolver)), + defaults + ) + _ <- scopeInit.onProjectCreation(project, bob) + expected = InProjectValue(Some(defaults.name), Some(defaults.description), Priority.unsafe(1)) + _ <- ref.get.assertEquals(Some(expected)) + } yield () + } + + test("Recovers if the resolver already exists") { + val scopeInit = new ResolverScopeInitialization( + (project, _) => IO.raiseError(ResourceAlreadyExists(nxv.defaultResolver, project)), + defaults + ) + scopeInit.onProjectCreation(project, bob).assert + } + + test("Raises a failure otherwise") { + val scopeInit = new ResolverScopeInitialization( + (_, _) => IO.raiseError(new IllegalStateException("Something got wrong !")), + defaults + ) + scopeInit.onProjectCreation(project, bob).intercept[ScopeInitializationFailed] + } +} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverStateMachineFixture.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverStateMachineFixture.scala new file mode 100644 index 0000000000..31ccb78703 --- /dev/null +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverStateMachineFixture.scala @@ -0,0 +1,67 @@ +package ch.epfl.bluebrain.nexus.delta.sdk.resolvers + +import cats.data.NonEmptyList +import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv +import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller +import ch.epfl.bluebrain.nexus.delta.sdk.model.Tags +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.IdentityResolution.ProvidedIdentities +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{Priority, ResolverState} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverValue.{CrossProjectValue, InProjectValue} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, User} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag +import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef} +import io.circe.Json + +import java.time.Instant +trait ResolverStateMachineFixture { + + val epoch = Instant.EPOCH + val instant = Instant.ofEpochMilli(1000L) + val realm = Label.unsafe("myrealm") + val bob = Caller(User("Bob", realm), Set(User("Bob", realm), Group("mygroup", realm), Authenticated(realm))) + val alice = Caller(User("Alice", realm), Set(User("Alice", realm), Group("mygroup2", realm))) + + val project = ProjectRef.unsafe("org", "proj") + val priority = Priority.unsafe(42) + + val ipId = nxv + "in-project" + val cpId = nxv + "cross-project" + + val inProjectCurrent = ResolverState( + ipId, + project, + InProjectValue(priority), + Json.obj(), + Tags.empty, + 2, + deprecated = false, + epoch, + bob.subject, + instant, + Anonymous + ) + + val crossProjectCurrent = ResolverState( + cpId, + project, + CrossProjectValue( + priority, + Set.empty, + NonEmptyList.of( + ProjectRef.unsafe("org2", "proj") + ), + ProvidedIdentities(bob.identities) + ), + Json.obj(), + Tags(UserTag.unsafe("tag1") -> 5), + 2, + deprecated = false, + epoch, + alice.subject, + instant, + bob.subject + ) + + val bothStates = List(inProjectCurrent, crossProjectCurrent) + +} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversEvaluateSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversEvaluateSuite.scala new file mode 100644 index 0000000000..e81733183b --- /dev/null +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversEvaluateSuite.scala @@ -0,0 +1,317 @@ +package ch.epfl.bluebrain.nexus.delta.sdk.resolvers + +import cats.data.NonEmptyList +import cats.effect.IO +import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.Resolvers.{evaluate, ValidatePriority} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.IdentityResolution.{ProvidedIdentities, UseCurrentCaller} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.Priority +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverCommand.{CreateResolver, DeprecateResolver, TagResolver, UpdateResolver} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverEvent.{ResolverCreated, ResolverDeprecated, ResolverTagAdded, ResolverUpdated} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.{IncorrectRev, _} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverType.{CrossProject, InProject} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverValue.{CrossProjectValue, InProjectValue} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag +import ch.epfl.bluebrain.nexus.testkit.ce.{CatsEffectSuite, IOFixedClock} +import io.circe.Json + +class ResolversEvaluateSuite extends CatsEffectSuite with IOFixedClock with ResolverStateMachineFixture { + + private val validatePriority: ValidatePriority = (_, _, _) => IO.unit + + private def eval = evaluate(validatePriority)(_, _) + + private val createInProject = CreateResolver( + ipId, + project, + InProjectValue(priority), + Json.obj("inProject" -> Json.fromString("created")), + bob + ) + + test("Creation fails if the in-project resolver already exists") { + eval(Some(inProjectCurrent), createInProject).intercept( + ResourceAlreadyExists(createInProject.id, createInProject.project) + ) + } + + test("Creation fails if the priority already exists") { + val validatePriority: ValidatePriority = + (ref, _, priority) => IO.raiseError(PriorityAlreadyExists(ref, nxv + "same-prio", priority)) + evaluate(validatePriority)(None, createInProject).intercept[PriorityAlreadyExists] + } + + test("Creation succeeds for a in-project resolver") { + val expected = ResolverCreated( + ipId, + project, + createInProject.value, + createInProject.source, + 1, + epoch, + bob.subject + ) + eval(None, createInProject).assertEquals(expected) + } + + private val crossProjectValue = CrossProjectValue( + priority, + Set(nxv + "resource"), + NonEmptyList.of( + ProjectRef.unsafe("org2", "proj"), + ProjectRef.unsafe("org2", "proj2") + ), + ProvidedIdentities(bob.identities) + ) + + private val createCrossProject = CreateResolver( + cpId, + project, + crossProjectValue, + Json.obj("crossProject" -> Json.fromString("created")), + bob + ) + + test("Creation fails if the cross-project resolver already exists") { + eval(Some(crossProjectCurrent), createCrossProject).intercept( + ResourceAlreadyExists(createCrossProject.id, createCrossProject.project) + ) + } + + test("Creation fails if no identities are provided for a cross-project resolver") { + val invalidValue = crossProjectValue.copy(identityResolution = ProvidedIdentities(Set.empty)) + val invalidCommand = createCrossProject.copy(value = invalidValue) + eval(None, invalidCommand).intercept(NoIdentities) + } + + test("Creation fails if no identities are provided for a cross-project resolver") { + val invalidValue = + crossProjectValue.copy(identityResolution = ProvidedIdentities(Set(bob.subject, alice.subject))) + val invalidCommand = createCrossProject.copy(value = invalidValue) + eval(None, invalidCommand).intercept(InvalidIdentities(Set(alice.subject))) + } + + test("Creation succeeds for a cross-project resolver with provided identities") { + val expected = ResolverCreated( + cpId, + project, + createCrossProject.value, + createCrossProject.source, + 1, + epoch, + bob.subject + ) + eval(None, createCrossProject).assertEquals(expected) + } + + test("Creation succeeds for a cross-project resolver with provided identities") { + val useCaller = crossProjectValue.copy(identityResolution = UseCurrentCaller) + val command = createCrossProject.copy(value = useCaller) + val expected = ResolverCreated( + cpId, + project, + command.value, + command.source, + 1, + epoch, + bob.subject + ) + eval(None, command).assertEquals(expected) + } + + private val updateInProject = UpdateResolver( + ipId, + project, + InProjectValue(Priority.unsafe(99)), + Json.obj("inProject" -> Json.fromString("updated")), + 2, + alice + ) + + test("Update fails if the in-project resolver does not exist") { + eval(None, updateInProject).intercept(ResolverNotFound(updateInProject.id, updateInProject.project)) + } + + test("Update fails if the provided revision for the in-project resolver is incorrect") { + val invalidCommand = updateInProject.copy(rev = 4) + eval(Some(inProjectCurrent), invalidCommand).intercept(IncorrectRev(invalidCommand.rev, inProjectCurrent.rev)) + } + + test("Update fails if the in-project resolver is deprecated") { + val deprecated = inProjectCurrent.copy(deprecated = true) + eval(Some(deprecated), updateInProject).intercept(ResolverIsDeprecated(deprecated.id)) + } + + test("Update fails if we try to change from in-project to cross-project type") { + val expectedError = DifferentResolverType(updateCrossProject.id, CrossProject, InProject) + eval(Some(inProjectCurrent), updateCrossProject).intercept(expectedError) + } + + test("Update fails if the priority already exists") { + val validatePriority: ValidatePriority = + (ref, _, priority) => IO.raiseError(PriorityAlreadyExists(ref, nxv + "same-priority", priority)) + evaluate(validatePriority)(Some(inProjectCurrent), updateInProject).intercept[PriorityAlreadyExists] + } + + test("Update succeeds for a in-project resolver") { + val expected = ResolverUpdated( + ipId, + project, + updateInProject.value, + updateInProject.source, + 3, + epoch, + alice.subject + ) + eval(Some(inProjectCurrent), updateInProject).assertEquals(expected) + } + + private val updateCrossProject = UpdateResolver( + cpId, + project, + CrossProjectValue( + Priority.unsafe(99), + Set(nxv + "resource"), + NonEmptyList.of( + ProjectRef.unsafe("org2", "proj"), + ProjectRef.unsafe("org2", "proj2") + ), + ProvidedIdentities(alice.identities) + ), + Json.obj("crossProject" -> Json.fromString("updated")), + 2, + alice + ) + + test("Update fails if the cross-project resolver does not exist") { + eval(None, updateCrossProject).intercept(ResolverNotFound(updateCrossProject.id, updateCrossProject.project)) + } + + test("Update fails if the provided revision for the cross-project resolver is incorrect") { + val invalidCommand = updateCrossProject.copy(rev = 1) + eval(Some(crossProjectCurrent), invalidCommand).intercept(IncorrectRev(invalidCommand.rev, inProjectCurrent.rev)) + } + + test("Update fails if the cross-project resolver is deprecated") { + val deprecated = crossProjectCurrent.copy(deprecated = true) + eval(Some(deprecated), updateCrossProject).intercept(ResolverIsDeprecated(deprecated.id)) + } + + test("Update fails if no identities are provided for a cross-project resolver") { + val invalidValue = crossProjectValue.copy(identityResolution = ProvidedIdentities(Set.empty)) + val invalidCommand = updateCrossProject.copy(value = invalidValue) + eval(Some(crossProjectCurrent), invalidCommand).intercept(NoIdentities) + } + + test("Update fails if some provided identities don't belong to the caller for a cross-project resolver") { + val invalidValue = crossProjectValue.copy(identityResolution = ProvidedIdentities(Set(bob.subject, alice.subject))) + val invalidCommand = updateCrossProject.copy(value = invalidValue) + eval(Some(crossProjectCurrent), invalidCommand).intercept(InvalidIdentities(Set(bob.subject))) + } + + test("Update fails if we try to change from cross-project to in-project type") { + val expectedError = DifferentResolverType(updateInProject.id, InProject, CrossProject) + eval(Some(crossProjectCurrent), updateInProject).intercept(expectedError) + } + + test("Update succeeds for a cross-project resolver with provided entities") { + val expected = ResolverUpdated( + cpId, + project, + updateCrossProject.value, + updateCrossProject.source, + 3, + epoch, + alice.subject + ) + eval(Some(crossProjectCurrent), updateCrossProject).assertEquals(expected) + } + + test("Update succeeds for a cross-project resolver with current caller") { + val userCallerResolution = crossProjectValue.copy(identityResolution = UseCurrentCaller) + val command = updateCrossProject.copy(value = userCallerResolution) + val expected = ResolverUpdated( + cpId, + project, + command.value, + command.source, + 3, + epoch, + alice.subject + ) + eval(Some(crossProjectCurrent), command).assertEquals(expected) + } + + private val tagCommand = TagResolver(ipId, project, 1, UserTag.unsafe("tag1"), 2, bob.subject) + + test("Tag fails if the resolver does not exist") { + val expectedError = ResolverNotFound(tagCommand.id, tagCommand.project) + eval(None, tagCommand).intercept(expectedError) + } + + bothStates.foreach { state => + test(s"Tag fails for an ${state.value.tpe} resolver if the provided revision is incorrect") { + val incorrectRev = tagCommand.copy(rev = 5) + val expectedError = IncorrectRev(incorrectRev.rev, state.rev) + eval(Some(state), incorrectRev).intercept(expectedError) + } + } + + bothStates.foreach { state => + test(s"Tag succeeds for the ${state.value.tpe} resolver") { + val expected = ResolverTagAdded( + tagCommand.id, + project, + state.value.tpe, + targetRev = tagCommand.targetRev, + tag = tagCommand.tag, + 3, + epoch, + bob.subject + ) + eval(Some(state), tagCommand).assertEquals(expected) + } + + test(s"Tag succeeds for if the ${state.value.tpe} is deprecated") { + val deprecated = state.copy(deprecated = true) + val expected = ResolverTagAdded( + tagCommand.id, + project, + state.value.tpe, + targetRev = tagCommand.targetRev, + tag = tagCommand.tag, + 3, + epoch, + bob.subject + ) + eval(Some(deprecated), tagCommand).assertEquals(expected) + } + } + + private val deprecateCommand = DeprecateResolver(ipId, project, 2, bob.subject) + + test("Deprecate fails if resolver does not exist") { + val expectedError = ResolverNotFound(deprecateCommand.id, deprecateCommand.project) + eval(None, deprecateCommand).intercept(expectedError) + } + + bothStates.foreach { state => + test(s"Deprecate fails for an ${state.value.tpe} resolver if the provided revision is incorrect") { + val incorrectRev = deprecateCommand.copy(rev = 5) + val expectedError = IncorrectRev(incorrectRev.rev, state.rev) + eval(Some(state), incorrectRev).intercept(expectedError) + } + + test(s"Deprecate fails for an ${state.value.tpe} resolver if it is already deprecated") { + val deprecated = state.copy(deprecated = true) + val expectedError = ResolverIsDeprecated(deprecated.id) + eval(Some(deprecated), deprecateCommand).intercept(expectedError) + } + + test(s"Deprecate succeeds for an ${state.value.tpe} resolver") { + val expected = ResolverDeprecated(deprecateCommand.id, project, state.value.tpe, 3, epoch, bob.subject) + eval(Some(state), deprecateCommand).assertEquals(expected) + } + } +} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversImplSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversImplSpec.scala index 0a9c97caf0..412126edc0 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversImplSpec.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversImplSpec.scala @@ -2,6 +2,7 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resolvers import cats.data.NonEmptyList import cats.syntax.all._ +import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination.FromPagination import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, nxv, schema} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi} @@ -11,7 +12,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.generators.ResolverGen.{resolverResourc 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._ -import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination.FromPagination import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams.ResolverSearchParams import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings import ch.epfl.bluebrain.nexus.delta.sdk.projects.{FetchContextDummy, Projects} @@ -27,8 +27,9 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Authenticated, Gro import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef} import ch.epfl.bluebrain.nexus.delta.sourcing.postgres.DoobieScalaTestFixture -import ch.epfl.bluebrain.nexus.testkit.{CirceLiteral, IOFixedClock, IOValues} -import monix.bio.{IO, UIO} +import ch.epfl.bluebrain.nexus.testkit.CirceLiteral +import ch.epfl.bluebrain.nexus.testkit.ce.{CatsIOValues, IOFixedClock} +import monix.bio.UIO import org.scalatest.matchers.should.Matchers import org.scalatest.{CancelAfterFailure, Inspectors, OptionValues} @@ -36,8 +37,8 @@ import java.util.UUID class ResolversImplSpec extends DoobieScalaTestFixture + with CatsIOValues with Matchers - with IOValues with IOFixedClock with CancelAfterFailure with CirceLiteral @@ -63,10 +64,7 @@ class ResolversImplSpec contexts.resolversMetadata -> jsonContentOf("/contexts/resolvers-metadata.json").topContextValueOrEmpty ) - private val resolverContextResolution: ResolverContextResolution = new ResolverContextResolution( - res, - (_, _, _) => IO.raiseError(ResourceResolutionReport()) - ) + private val resolverContextResolution: ResolverContextResolution = ResolverContextResolution(res) private val org = Label.unsafe("org") private val apiMappings = ApiMappings("nxv" -> nxv.base, "Person" -> schema.Person) @@ -91,7 +89,7 @@ class ResolversImplSpec private lazy val resolvers: Resolvers = ResolversImpl( fetchContext, resolverContextResolution, - ResolversConfig(eventLogConfig, pagination, defaults), + ResolversConfig(eventLogConfig, defaults), xas ) @@ -210,9 +208,7 @@ class ResolversImplSpec ) { case (id, value) => val payloadId = nxv + "resolver-fail" val payload = sourceFrom(payloadId, value) - resolvers - .create(id, projectRef, payload) - .rejected shouldEqual UnexpectedResolverId(id, payloadId) + resolvers.create(id, projectRef, payload).rejected(UnexpectedResolverId(id, payloadId)) } } @@ -224,14 +220,14 @@ class ResolversImplSpec ) ) { case (id, value) => val payload = sourceWithoutId(value) - resolvers.create(id, projectRef, payload).rejected shouldEqual InvalidResolverId(id) + resolvers.create(id, projectRef, payload).rejected(InvalidResolverId(id)) } } "fail if priority already exists" in { resolvers .create(nxv + "in-project-other", projectRef, inProjectValue) - .rejected shouldEqual PriorityAlreadyExists(projectRef, nxv + "in-project", inProjectValue.priority) + .rejected(PriorityAlreadyExists(projectRef, nxv + "in-project", inProjectValue.priority)) } "fail if it already exists" in { @@ -245,15 +241,10 @@ class ResolversImplSpec val payload = sourceWithoutId(value) resolvers .create(id.toString, projectRef, payload) - .rejected shouldEqual ResourceAlreadyExists(id, projectRef) + .rejected(ResourceAlreadyExists(id, projectRef)) val payloadWithId = sourceFrom(id, value) - resolvers - .create(projectRef, payloadWithId) - .rejected shouldEqual ResourceAlreadyExists( - id, - projectRef - ) + resolvers.create(projectRef, payloadWithId).rejected(ResourceAlreadyExists(id, projectRef)) } } @@ -302,7 +293,7 @@ class ResolversImplSpec val payload = sourceWithoutId(invalidValue) resolvers .create(nxv + "cross-project-no-id", projectRef, payload) - .rejected shouldEqual NoIdentities + .rejected(NoIdentities) } "fail if some provided identities don't belong to the caller for a cross-project resolver" in { @@ -315,7 +306,7 @@ class ResolversImplSpec val payload = sourceWithoutId(invalidValue) resolvers .create(nxv + "cross-project-miss-id", projectRef, payload) - .rejected shouldEqual InvalidIdentities(Set(alice.subject)) + .rejected(InvalidIdentities(Set(alice.subject))) } "fail if mandatory values in source are missing" in { @@ -376,7 +367,7 @@ class ResolversImplSpec val payload = sourceWithoutId(value) resolvers .update(id, projectRef, 1, payload) - .rejected shouldEqual ResolverNotFound(id, projectRef) + .rejected(ResolverNotFound(id, projectRef)) } } @@ -390,7 +381,7 @@ class ResolversImplSpec val payload = sourceWithoutId(value) resolvers .update(id, projectRef, 5, payload) - .rejected shouldEqual IncorrectRev(5, 2) + .rejected(IncorrectRev(5, 2)) } } @@ -405,7 +396,7 @@ class ResolversImplSpec val payload = sourceFrom(payloadId, value) resolvers .update(id, projectRef, 2, payload) - .rejected shouldEqual UnexpectedResolverId(id = id, payloadId = payloadId) + .rejected(UnexpectedResolverId(id = id, payloadId = payloadId)) } } @@ -442,7 +433,7 @@ class ResolversImplSpec val payload = sourceWithoutId(invalidValue) resolvers .update(nxv + "cross-project", projectRef, 2, payload) - .rejected shouldEqual NoIdentities + .rejected(NoIdentities) } "fail if some provided identities don't belong to the caller for a cross-project resolver" in { @@ -454,7 +445,7 @@ class ResolversImplSpec val payload = sourceWithoutId(invalidValue) resolvers .update(nxv + "cross-project", projectRef, 2, payload) - .rejected shouldEqual InvalidIdentities(Set(alice.subject)) + .rejected(InvalidIdentities(Set(alice.subject))) } } @@ -488,7 +479,7 @@ class ResolversImplSpec nxv + "cross-project-xxx" ) ) { id => - resolvers.tag(id, projectRef, tag, 1, 2).rejected shouldEqual ResolverNotFound(id, projectRef) + resolvers.tag(id, projectRef, tag, 1, 2).rejected(ResolverNotFound(id, projectRef)) } } @@ -499,7 +490,7 @@ class ResolversImplSpec nxv + "cross-project" ) ) { id => - resolvers.tag(id, projectRef, tag, 1, 21).rejected shouldEqual IncorrectRev(21, 3) + resolvers.tag(id, projectRef, tag, 1, 21).rejected(IncorrectRev(21, 3)) } } @@ -510,7 +501,7 @@ class ResolversImplSpec nxv + "cross-project" ) ) { id => - resolvers.tag(id, projectRef, tag, 20, 3).rejected shouldEqual RevisionNotFound(20, 3) + resolvers.tag(id, projectRef, tag, 20, 3).rejected(RevisionNotFound(20, 3)) } } @@ -565,7 +556,7 @@ class ResolversImplSpec nxv + "cross-project-xxx" ) ) { id => - resolvers.deprecate(id, projectRef, 3).rejected shouldEqual ResolverNotFound(id, projectRef) + resolvers.deprecate(id, projectRef, 3).rejected(ResolverNotFound(id, projectRef)) } } @@ -576,7 +567,7 @@ class ResolversImplSpec nxv + "cross-project" ) ) { id => - resolvers.deprecate(id, projectRef, 3).rejected shouldEqual IncorrectRev(3, 4) + resolvers.deprecate(id, projectRef, 3).rejected(IncorrectRev(3, 4)) } } @@ -609,7 +600,7 @@ class ResolversImplSpec nxv + "cross-project" ) ) { id => - resolvers.deprecate(id, projectRef, 4).rejected shouldEqual ResolverIsDeprecated(id) + resolvers.deprecate(id, projectRef, 4).rejected(ResolverIsDeprecated(id)) } } @@ -622,7 +613,7 @@ class ResolversImplSpec ) { case (id, value) => resolvers .update(id, projectRef, 4, sourceWithoutId(value)) - .rejected shouldEqual ResolverIsDeprecated(id) + .rejected(ResolverIsDeprecated(id)) } } @@ -728,14 +719,12 @@ class ResolversImplSpec } "fail if revision does not exist" in { - resolvers.fetch(IdSegmentRef(nxv + "in-project", 30), projectRef).rejected shouldEqual - RevisionNotFound(30, 5) + resolvers.fetch(IdSegmentRef(nxv + "in-project", 30), projectRef).rejected(RevisionNotFound(30, 5)) } "fail if tag does not exist" in { val unknownTag = UserTag.unsafe("xxx") - resolvers.fetch(IdSegmentRef(nxv + "in-project", unknownTag), projectRef).rejected shouldEqual - TagNotFound(unknownTag) + resolvers.fetch(IdSegmentRef(nxv + "in-project", unknownTag), projectRef).rejected(TagNotFound(unknownTag)) } } diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversNextSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversNextSuite.scala new file mode 100644 index 0000000000..480d0a2770 --- /dev/null +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversNextSuite.scala @@ -0,0 +1,190 @@ +package ch.epfl.bluebrain.nexus.delta.sdk.resolvers + +import cats.data.NonEmptyList +import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv +import ch.epfl.bluebrain.nexus.delta.sdk.model.Tags +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.Resolvers.next +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.IdentityResolution.ProvidedIdentities +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverEvent.{ResolverCreated, ResolverDeprecated, ResolverTagAdded, ResolverUpdated} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverValue.{CrossProjectValue, InProjectValue} +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{Priority, ResolverState, ResolverType} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag +import ch.epfl.bluebrain.nexus.testkit.NexusSuite +import ch.epfl.bluebrain.nexus.testkit.bio.OptionAssertions +import io.circe.Json + +class ResolversNextSuite extends NexusSuite with ResolverStateMachineFixture with OptionAssertions { + + private val inProjectCreated = ResolverCreated( + ipId, + project, + InProjectValue(Priority.unsafe(22)), + Json.obj("inProject" -> Json.fromString("created")), + 1, + epoch, + bob.subject + ) + + private val crossProjectCreated = ResolverCreated( + cpId, + project, + CrossProjectValue( + Priority.unsafe(55), + Set(nxv + "resource"), + NonEmptyList.of( + ProjectRef.unsafe("org2", "proj"), + ProjectRef.unsafe("org2", "proj2") + ), + ProvidedIdentities(bob.identities) + ), + Json.obj("crossProject" -> Json.fromString("created")), + 1, + epoch, + bob.subject + ) + + test("A create event gives a new in-project state from None") { + val expected = ResolverState( + ipId, + project, + inProjectCreated.value, + inProjectCreated.source, + Tags.empty, + 1, + deprecated = false, + epoch, + bob.subject, + epoch, + bob.subject + ) + next(None, inProjectCreated).assertSome(expected) + } + + test("A create event gives a new cross-project resolver state from None") { + val expected = ResolverState( + cpId, + project, + crossProjectCreated.value, + crossProjectCreated.source, + Tags.empty, + 1, + deprecated = false, + epoch, + bob.subject, + epoch, + bob.subject + ) + next(None, crossProjectCreated).assertSome(expected) + } + + List( + inProjectCurrent -> inProjectCreated, + crossProjectCurrent -> crossProjectCreated + ).foreach { case (state, event) => + test(s"A create event returns None for an existing ${state.value.tpe} resolver") { + next(Some(state), event).assertNone() + } + } + + val inProjectUpdated = ResolverUpdated( + ipId, + project, + InProjectValue(Priority.unsafe(40)), + Json.obj("inProject" -> Json.fromString("updated")), + 3, + instant, + bob.subject + ) + + val crossCrojectUpdated = ResolverUpdated( + cpId, + project, + CrossProjectValue( + Priority.unsafe(999), + Set(nxv + "r", nxv + "r2"), + NonEmptyList.of( + ProjectRef.unsafe("org2", "proj"), + ProjectRef.unsafe("org3", "proj2") + ), + ProvidedIdentities(alice.identities) + ), + Json.obj("crossProject" -> Json.fromString("updated")), + 3, + epoch, + bob.subject + ) + + test("An update event gives a new revision of an existing in-project resolver") { + val expected = inProjectCurrent.copy( + value = inProjectUpdated.value, + source = inProjectUpdated.source, + rev = inProjectUpdated.rev, + updatedAt = inProjectUpdated.instant, + updatedBy = inProjectUpdated.subject + ) + next(Some(inProjectCurrent), inProjectUpdated).assertSome(expected) + } + + test("An update event gives a new revision of an existing cross-project resolver") { + val expected = crossProjectCurrent.copy( + value = crossCrojectUpdated.value, + source = crossCrojectUpdated.source, + rev = crossCrojectUpdated.rev, + updatedAt = crossCrojectUpdated.instant, + updatedBy = crossCrojectUpdated.subject + ) + next(Some(crossProjectCurrent), crossCrojectUpdated).assertSome(expected) + } + + List(inProjectUpdated, crossCrojectUpdated).foreach { event => + test(s"Return None when attempting to update a non-existing ${event.value.tpe} resolver") { + next(None, event).assertNone() + } + } + + List(inProjectCurrent -> crossCrojectUpdated, crossProjectCurrent -> inProjectUpdated).foreach { + case (state, event) => + test(s"Return None when attempting to update an existing ${event.value.tpe} resolver with the other type") { + next(Some(state), event).assertNone() + } + } + + private val tagEvent = + ResolverTagAdded(ipId, project, ResolverType.InProject, 1, UserTag.unsafe("tag2"), 3, instant, alice.subject) + + bothStates.foreach { state => + test(s"Update the tag list fot a ${state.value.tpe} resolver") { + val expected = state.copy( + tags = state.tags + (tagEvent.tag -> tagEvent.targetRev), + rev = tagEvent.rev, + updatedAt = tagEvent.instant, + updatedBy = tagEvent.subject + ) + next(Some(state), tagEvent).assertSome(expected) + } + } + + test(s"Return None when attempting to tag a non-existing resolver") { + next(None, tagEvent).assertNone() + } + + private val deprecatedEvent = ResolverDeprecated(ipId, project, ResolverType.InProject, 3, instant, alice.subject) + + bothStates.foreach { state => + test(s"mark the current state as deprecated for a ${state.value.tpe} resolver") { + val expected = state.copy( + deprecated = true, + rev = deprecatedEvent.rev, + updatedAt = deprecatedEvent.instant, + updatedBy = deprecatedEvent.subject + ) + next(Some(state), deprecatedEvent).assertSome(expected) + } + } + + test(s"Return None when attempting to deprecate a non-existing resolver") { + next(None, deprecatedEvent).assertNone() + } + +} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversSpec.scala deleted file mode 100644 index 53a9556b72..0000000000 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolversSpec.scala +++ /dev/null @@ -1,572 +0,0 @@ -package ch.epfl.bluebrain.nexus.delta.sdk.resolvers - -import cats.data.NonEmptyList -import cats.implicits._ -import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv -import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller -import ch.epfl.bluebrain.nexus.delta.sdk.model.Tags -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.Resolvers.{evaluate, next, ValidatePriority} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.IdentityResolution.{ProvidedIdentities, UseCurrentCaller} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverCommand.{CreateResolver, DeprecateResolver, TagResolver, UpdateResolver} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverEvent.{ResolverCreated, ResolverDeprecated, ResolverTagAdded, ResolverUpdated} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.{DifferentResolverType, IncorrectRev, InvalidIdentities, NoIdentities, PriorityAlreadyExists, ResolverIsDeprecated, ResolverNotFound, ResourceAlreadyExists, RevisionNotFound} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverType.{CrossProject, InProject} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverValue.{CrossProjectValue, InProjectValue} -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{Priority, ResolverRejection, ResolverState, ResolverType} -import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, User} -import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag -import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef} -import ch.epfl.bluebrain.nexus.testkit.{IOFixedClock, IOValues} -import io.circe.Json -import monix.bio.{IO, UIO} -import monix.execution.Scheduler -import org.scalatest.{Inspectors, OptionValues} -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import java.time.Instant - -class ResolversSpec - extends AnyWordSpec - with Matchers - with OptionValues - with IOValues - with IOFixedClock - with Inspectors { - - private val epoch = Instant.EPOCH - private val instant = Instant.ofEpochMilli(1000L) - private val realm = Label.unsafe("myrealm") - private val bob = Caller(User("Bob", realm), Set(User("Bob", realm), Group("mygroup", realm), Authenticated(realm))) - private val alice = Caller(User("Alice", realm), Set(User("Alice", realm), Group("mygroup2", realm))) - - private val project = ProjectRef.unsafe("org", "proj") - private val priority = Priority.unsafe(42) - - private val ipId = nxv + "in-project" - private val cpId = nxv + "cross-project" - - private val inProjectCurrent = ResolverState( - ipId, - project, - InProjectValue(priority), - Json.obj(), - Tags.empty, - 2, - deprecated = false, - epoch, - bob.subject, - instant, - Anonymous - ) - - private val crossProjectCurrent = ResolverState( - cpId, - project, - CrossProjectValue( - priority, - Set.empty, - NonEmptyList.of( - ProjectRef.unsafe("org2", "proj") - ), - ProvidedIdentities(bob.identities) - ), - Json.obj(), - Tags(UserTag.unsafe("tag1") -> 5), - 2, - deprecated = false, - epoch, - alice.subject, - instant, - bob.subject - ) - - val validatePriority: ValidatePriority = (_, _, _) => UIO.unit - - private def eval = evaluate(validatePriority)(_, _) - - "The Resolvers evaluation" when { - implicit val sc: Scheduler = Scheduler.global - - val createInProject = CreateResolver( - ipId, - project, - InProjectValue(priority), - Json.obj("inProject" -> Json.fromString("created")), - bob - ) - - val crossProjectValue = CrossProjectValue( - priority, - Set(nxv + "resource"), - NonEmptyList.of( - ProjectRef.unsafe("org2", "proj"), - ProjectRef.unsafe("org2", "proj2") - ), - ProvidedIdentities(bob.identities) - ) - - val createCrossProject = CreateResolver( - cpId, - project, - crossProjectValue, - Json.obj("crossProject" -> Json.fromString("created")), - bob - ) - - val updateInProject = UpdateResolver( - ipId, - project, - InProjectValue(Priority.unsafe(99)), - Json.obj("inProject" -> Json.fromString("updated")), - 2, - alice - ) - - val updateCrossProject = UpdateResolver( - cpId, - project, - CrossProjectValue( - Priority.unsafe(99), - Set(nxv + "resource"), - NonEmptyList.of( - ProjectRef.unsafe("org2", "proj"), - ProjectRef.unsafe("org2", "proj2") - ), - ProvidedIdentities(alice.identities) - ), - Json.obj("crossProject" -> Json.fromString("updated")), - 2, - alice - ) - - "evaluating a create command" should { - - "fail if the resolver already exists" in { - forAll( - (List(inProjectCurrent, crossProjectCurrent), List(createInProject, createCrossProject)).tupled - ) { case (state, command) => - eval(Some(state), command).rejected shouldEqual ResourceAlreadyExists(command.id, command.project) - } - } - - "create a in-project creation event" in { - eval(None, createInProject).accepted shouldEqual ResolverCreated( - ipId, - project, - createInProject.value, - createInProject.source, - 1, - epoch, - bob.subject - ) - } - - "fail if no identities are provided for a cross-project resolver" in { - val invalidValue = crossProjectValue.copy(identityResolution = ProvidedIdentities(Set.empty)) - eval(None, createCrossProject.copy(value = invalidValue)).rejected shouldEqual NoIdentities - } - - "fail if the priority already exists" in { - val validatePriority: ValidatePriority = - (ref, _, priority) => IO.raiseError(PriorityAlreadyExists(ref, nxv + "same-prio", priority)) - evaluate(validatePriority)(None, createInProject).rejectedWith[PriorityAlreadyExists] - } - - "fail if some provided identities don't belong to the caller for a cross-project resolver" in { - val invalidValue = - crossProjectValue.copy(identityResolution = ProvidedIdentities(Set(bob.subject, alice.subject))) - eval(None, createCrossProject.copy(value = invalidValue)).rejected shouldEqual InvalidIdentities( - Set(alice.subject) - ) - } - - "create a cross-project creation event" in { - val userCallerResolution = crossProjectValue.copy(identityResolution = UseCurrentCaller) - - forAll(List(createCrossProject, createCrossProject.copy(value = userCallerResolution))) { command => - eval(None, command).accepted shouldEqual ResolverCreated( - cpId, - project, - command.value, - command.source, - 1, - epoch, - bob.subject - ) - } - } - } - - "eval an update command" should { - - "fail if the resolver doesn't exist" in { - forAll(List(updateInProject, updateCrossProject)) { command => - eval(None, command).rejected shouldEqual ResolverNotFound(command.id, command.project) - } - } - - "fail if the provided revision is incorrect" in { - forAll( - ( - List(inProjectCurrent, crossProjectCurrent), - List(updateInProject.copy(rev = 4), updateCrossProject.copy(rev = 1)) - ).tupled - ) { case (state, command) => - eval(Some(state), command).rejected shouldEqual IncorrectRev(command.rev, state.rev) - } - } - - "fail if the current state is deprecated" in { - forAll( - ( - List(inProjectCurrent.copy(deprecated = true), crossProjectCurrent.copy(deprecated = true)), - List(updateInProject, updateCrossProject) - ).tupled - ) { case (state, command) => - eval(Some(state), command).rejected shouldEqual ResolverIsDeprecated(state.id) - } - } - - "fail if we try to change from in-project to cross-project type" in { - eval(Some(inProjectCurrent), updateCrossProject).rejected shouldEqual DifferentResolverType( - updateCrossProject.id, - CrossProject, - InProject - ) - } - - "create an in-project resolver update event" in { - eval(Some(inProjectCurrent), updateInProject).accepted shouldEqual ResolverUpdated( - ipId, - project, - updateInProject.value, - updateInProject.source, - 3, - epoch, - alice.subject - ) - } - - "fail if the priority already exists" in { - val validatePriority: ValidatePriority = - (ref, _, priority) => IO.raiseError(PriorityAlreadyExists(ref, nxv + "same-prio", priority)) - evaluate(validatePriority)(Some(inProjectCurrent), updateInProject).rejectedWith[PriorityAlreadyExists] - } - - "fail if no identities are provided for a cross-project resolver" in { - val invalidValue = crossProjectValue.copy(identityResolution = ProvidedIdentities(Set.empty)) - eval(Some(crossProjectCurrent), updateCrossProject.copy(value = invalidValue)).rejected shouldEqual NoIdentities - } - - "fail if some provided identities don't belong to the caller for a cross-project resolver" in { - val invalidValue = - crossProjectValue.copy(identityResolution = ProvidedIdentities(Set(bob.subject, alice.subject))) - eval( - Some(crossProjectCurrent), - updateCrossProject.copy(value = invalidValue) - ).rejected shouldEqual InvalidIdentities(Set(bob.subject)) - } - - "fail if we try to change from cross-project to in-project type" in { - eval(Some(crossProjectCurrent), updateInProject).rejected shouldEqual DifferentResolverType( - updateInProject.id, - InProject, - CrossProject - ) - } - - "create an cross-project update event" in { - val userCallerResolution = crossProjectValue.copy(identityResolution = UseCurrentCaller) - - forAll(List(updateCrossProject, updateCrossProject.copy(value = userCallerResolution))) { command => - eval(Some(crossProjectCurrent), command).accepted shouldEqual ResolverUpdated( - cpId, - project, - command.value, - command.source, - 3, - epoch, - alice.subject - ) - } - } - - } - - "eval a tag command" should { - - val tagResolver = TagResolver(ipId, project, 1, UserTag.unsafe("tag1"), 2, bob.subject) - - "fail if the resolver doesn't exist" in { - eval(None, tagResolver).rejected shouldEqual ResolverNotFound(tagResolver.id, tagResolver.project) - } - - "fail if the provided revision is incorrect" in { - val incorrectRev = tagResolver.copy(rev = 5) - forAll(List(inProjectCurrent, crossProjectCurrent)) { state => - eval(Some(state), incorrectRev) - .rejectedWith[ResolverRejection] shouldEqual IncorrectRev(incorrectRev.rev, state.rev) - } - } - - "succeed if the resolver is deprecated" in { - forAll(List(inProjectCurrent.copy(deprecated = true), crossProjectCurrent.copy(deprecated = true))) { state => - eval(Some(state), tagResolver).accepted shouldEqual ResolverTagAdded( - tagResolver.id, - project, - state.value.tpe, - targetRev = tagResolver.targetRev, - tag = tagResolver.tag, - 3, - epoch, - bob.subject - ) - } - } - - "fail if the version to tag is invalid" in { - val incorrectTagRev = tagResolver.copy(targetRev = 5) - forAll(List(inProjectCurrent, crossProjectCurrent)) { state => - eval(Some(state), incorrectTagRev).rejected shouldEqual RevisionNotFound(incorrectTagRev.targetRev, state.rev) - } - } - - "create a tag event" in { - forAll(List(inProjectCurrent, crossProjectCurrent)) { state => - eval(Some(state), tagResolver).accepted shouldEqual ResolverTagAdded( - tagResolver.id, - project, - state.value.tpe, - targetRev = tagResolver.targetRev, - tag = tagResolver.tag, - 3, - epoch, - bob.subject - ) - } - } - } - - "eval a deprecate command" should { - - val deprecateResolver = DeprecateResolver(ipId, project, 2, bob.subject) - - "fail if the resolver doesn't exist" in { - eval(None, deprecateResolver).rejected shouldEqual ResolverNotFound( - deprecateResolver.id, - deprecateResolver.project - ) - } - - "fail if the provided revision is incorrect" in { - val incorrectRev = deprecateResolver.copy(rev = 5) - forAll(List(inProjectCurrent, crossProjectCurrent)) { state => - eval(Some(state), incorrectRev).rejected shouldEqual IncorrectRev(incorrectRev.rev, state.rev) - } - } - - "fail if the resolver is already deprecated" in { - forAll(List(inProjectCurrent.copy(deprecated = true), crossProjectCurrent.copy(deprecated = true))) { state => - eval(Some(state), deprecateResolver).rejected shouldEqual ResolverIsDeprecated(state.id) - } - } - - "deprecate the resolver" in { - forAll(List(inProjectCurrent, crossProjectCurrent)) { state => - eval(Some(state), deprecateResolver).accepted shouldEqual ResolverDeprecated( - deprecateResolver.id, - project, - state.value.tpe, - 3, - epoch, - bob.subject - ) - } - } - } - } - - "The Resolvers next state" when { - - "applying a create event" should { - - val inProjectCreated = ResolverCreated( - ipId, - project, - InProjectValue(Priority.unsafe(22)), - Json.obj("inProject" -> Json.fromString("created")), - 1, - epoch, - bob.subject - ) - - val crossProjectCreated = ResolverCreated( - cpId, - project, - CrossProjectValue( - Priority.unsafe(55), - Set(nxv + "resource"), - NonEmptyList.of( - ProjectRef.unsafe("org2", "proj"), - ProjectRef.unsafe("org2", "proj2") - ), - ProvidedIdentities(bob.identities) - ), - Json.obj("crossProject" -> Json.fromString("created")), - 1, - epoch, - bob.subject - ) - - "give a new in-project resolver state from None" in { - next(None, inProjectCreated).value shouldEqual ResolverState( - ipId, - project, - inProjectCreated.value, - inProjectCreated.source, - Tags.empty, - 1, - deprecated = false, - epoch, - bob.subject, - epoch, - bob.subject - ) - } - - "give a new cross-project resolver state from None" in { - next(None, crossProjectCreated).value shouldEqual ResolverState( - cpId, - project, - crossProjectCreated.value, - crossProjectCreated.source, - Tags.empty, - 1, - deprecated = false, - epoch, - bob.subject, - epoch, - bob.subject - ) - } - - "return None for an existing entity" in { - forAll( - ( - List(inProjectCurrent, crossProjectCurrent), - List(inProjectCreated, crossProjectCreated) - ).tupled - ) { case (state, event) => - next(Some(state), event) shouldEqual None - } - } - } - - "applying an update event" should { - val inProjectUpdated = ResolverUpdated( - ipId, - project, - InProjectValue(Priority.unsafe(40)), - Json.obj("inProject" -> Json.fromString("updated")), - 3, - instant, - bob.subject - ) - - val crossCrojectUpdated = ResolverUpdated( - cpId, - project, - CrossProjectValue( - Priority.unsafe(999), - Set(nxv + "r", nxv + "r2"), - NonEmptyList.of( - ProjectRef.unsafe("org2", "proj"), - ProjectRef.unsafe("org3", "proj2") - ), - ProvidedIdentities(alice.identities) - ), - Json.obj("crossProject" -> Json.fromString("updated")), - 3, - epoch, - bob.subject - ) - - "give a new revision of the in-project resolver state from an existing in-project state" in { - next(Some(inProjectCurrent), inProjectUpdated).value shouldEqual inProjectCurrent.copy( - value = inProjectUpdated.value, - source = inProjectUpdated.source, - rev = inProjectUpdated.rev, - updatedAt = inProjectUpdated.instant, - updatedBy = inProjectUpdated.subject - ) - } - - "give a new revision of the cross-project resolver state from an existing cross-project state" in { - next(Some(crossProjectCurrent), crossCrojectUpdated).value shouldEqual crossProjectCurrent.copy( - value = crossCrojectUpdated.value, - source = crossCrojectUpdated.source, - rev = crossCrojectUpdated.rev, - updatedAt = crossCrojectUpdated.instant, - updatedBy = crossCrojectUpdated.subject - ) - } - - "return None for other combinations" in { - forAll( - List( - None -> inProjectUpdated, - None -> crossCrojectUpdated, - Some(inProjectCurrent) -> crossCrojectUpdated, - Some(crossProjectCurrent) -> inProjectUpdated - ) - ) { case (state, event) => - next(state, event) shouldEqual None - } - } - } - - "applying a tag event" should { - val resolverTagAdded = - ResolverTagAdded(ipId, project, ResolverType.InProject, 1, UserTag.unsafe("tag2"), 3, instant, alice.subject) - - "update the tag list" in { - forAll(List(inProjectCurrent, crossProjectCurrent)) { state => - next(Some(state), resolverTagAdded).value shouldEqual state.copy( - tags = state.tags + (resolverTagAdded.tag -> resolverTagAdded.targetRev), - rev = resolverTagAdded.rev, - updatedAt = resolverTagAdded.instant, - updatedBy = resolverTagAdded.subject - ) - } - } - - "doesn't result in any change on an initial state" in { - next(None, resolverTagAdded) shouldEqual None - } - - } - - "applying a deprecate event" should { - - val deprecated = ResolverDeprecated(ipId, project, ResolverType.InProject, 3, instant, alice.subject) - - "mark the current state as deprecated for a resolver" in { - forAll(List(inProjectCurrent, crossProjectCurrent)) { state => - next(Some(state), deprecated).value shouldEqual state.copy( - deprecated = true, - rev = deprecated.rev, - updatedAt = deprecated.instant, - updatedBy = deprecated.subject - ) - } - } - - "doesn't result in any change on an initial state" in { - next(None, deprecated) shouldEqual None - } - - } - } - -} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImplSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImplSpec.scala index 8b288d58cb..0851d880d6 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImplSpec.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImplSpec.scala @@ -1,6 +1,8 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resources +import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, nxv, schema, schemas} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords @@ -25,7 +27,6 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Identity, Label, ProjectRef, ResourceRef} import ch.epfl.bluebrain.nexus.delta.sourcing.postgres.DoobieScalaTestFixture import ch.epfl.bluebrain.nexus.testkit.{CirceLiteral, IOFixedClock, IOValues} -import monix.bio.UIO import org.scalatest.matchers.should.Matchers import org.scalatest.{CancelAfterFailure, Inspectors, OptionValues} @@ -70,9 +71,9 @@ class ResourcesImplSpec private val schema2 = SchemaGen.schema(schema.Person, project.ref, schemaSource.removeKeys(keywords.id)) private val fetchSchema: (ResourceRef, ProjectRef) => FetchResource[Schema] = { - case (ref, _) if ref.iri == schema2.id => UIO.some(SchemaGen.resourceFor(schema2, deprecated = true)) - case (ref, _) if ref.iri == schema1.id => UIO.some(SchemaGen.resourceFor(schema1)) - case _ => UIO.none + case (ref, _) if ref.iri == schema2.id => IO.pure(Some(SchemaGen.resourceFor(schema2, deprecated = true))) + case (ref, _) if ref.iri == schema1.id => IO.pure(Some(SchemaGen.resourceFor(schema1))) + case _ => IO.none } private val resourceResolution: ResourceResolution[Schema] = ResourceResolutionGen.singleInProject(projectRef, fetchSchema) @@ -89,7 +90,7 @@ class ResourcesImplSpec private val resolverContextResolution: ResolverContextResolution = new ResolverContextResolution( res, - (r, p, _) => resources.fetch(r, p).bimap(_ => ResourceResolutionReport(), identity) + (r, p, _) => resources.fetch(r, p).bimap(_ => ResourceResolutionReport(), identity).attempt ) private lazy val resources: Resources = ResourcesImpl( diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesTrialSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesTrialSuite.scala index bfd737d63f..67c3fad44d 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesTrialSuite.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesTrialSuite.scala @@ -10,11 +10,11 @@ import ch.epfl.bluebrain.nexus.delta.sdk.generators.{ProjectGen, ResourceGen, Sc import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings -import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution import ch.epfl.bluebrain.nexus.delta.sdk.resources.ValidationResult._ -import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.{Resource, ResourceGenerationResult, ResourceRejection} import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.{InvalidResource, ProjectContextRejection, ReservedResourceId} +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.{Resource, ResourceGenerationResult, ResourceRejection} +import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ import ch.epfl.bluebrain.nexus.delta.sourcing.model.ResourceRef.Revision import ch.epfl.bluebrain.nexus.testkit.bio.BioSuite import ch.epfl.bluebrain.nexus.testkit.{IOFixedClock, TestHelpers} @@ -41,10 +41,7 @@ class ResourcesTrialSuite extends BioSuite with ValidateResourceFixture with Tes private val fetchResourceFail = IO.terminate(new IllegalStateException("Should not be attempt to fetch a resource")) - private val resolverContextResolution: ResolverContextResolution = new ResolverContextResolution( - res, - (_, _, _) => fetchResourceFail - ) + private val resolverContextResolution: ResolverContextResolution = ResolverContextResolution(res) private val am = ApiMappings(Map("nxv" -> nxv.base, "Person" -> schema.Person)) private val allApiMappings = am + Resources.mappings diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemaImportsSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemaImportsSpec.scala deleted file mode 100644 index 2ad510a4a1..0000000000 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemaImportsSpec.scala +++ /dev/null @@ -1,129 +0,0 @@ -package ch.epfl.bluebrain.nexus.delta.sdk.schemas - -import cats.data.NonEmptyList -import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.ExpandedJsonLd -import ch.epfl.bluebrain.nexus.delta.sdk.Resolve -import ch.epfl.bluebrain.nexus.delta.sdk.generators.{ProjectGen, ResourceGen} -import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller -import ch.epfl.bluebrain.nexus.delta.sdk.model.Tags -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport -import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.Resource -import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema -import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.SchemaRejection.InvalidSchemaResolution -import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ -import ch.epfl.bluebrain.nexus.delta.sdk.utils.Fixtures -import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.User -import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ResourceRef} -import ch.epfl.bluebrain.nexus.testkit.{CirceLiteral, IOValues, TestHelpers} -import monix.bio.IO -import org.scalatest.OptionValues -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpecLike - -import scala.collection.immutable.VectorMap - -class SchemaImportsSpec - extends AnyWordSpecLike - with Matchers - with TestHelpers - with IOValues - with OptionValues - with CirceLiteral - with Fixtures { - - private val alice = User("alice", Label.unsafe("wonderland")) - implicit val aliceCaller: Caller = Caller(alice, Set(alice)) - - "A SchemaImports" should { - val neuroshapes = "https://neuroshapes.org" - val parcellationlabel = iri"$neuroshapes/dash/parcellationlabel" - val json = jsonContentOf("schemas/parcellationlabel.json") - val projectRef = ProjectGen.project("org", "proj").ref - - val entitySource = jsonContentOf("schemas/entity.json") - - val entityExpandedSchema = ExpandedJsonLd(jsonContentOf("schemas/entity-expanded.json")).accepted - val identifierExpandedSchema = ExpandedJsonLd(jsonContentOf("schemas/identifier-expanded.json")).accepted - val licenseExpandedSchema = ExpandedJsonLd(jsonContentOf("schemas/license-expanded.json")).accepted - val propertyValueExpandedSchema = ExpandedJsonLd(jsonContentOf("schemas/property-value-expanded.json")).accepted - - val expandedSchemaMap = Map( - iri"$neuroshapes/commons/entity" -> - Schema( - iri"$neuroshapes/commons/entity", - projectRef, - Tags.empty, - entitySource, - entityExpandedSchema.toCompacted(entitySource.topContextValueOrEmpty).accepted, - NonEmptyList.of( - entityExpandedSchema, - identifierExpandedSchema, - licenseExpandedSchema, - propertyValueExpandedSchema - ) - ) - ) - - // format: off - val resourceMap = VectorMap( - iri"$neuroshapes/commons/vocabulary" -> jsonContentOf("schemas/vocabulary.json"), - iri"$neuroshapes/wrong/vocabulary" -> jsonContentOf("schemas/vocabulary.json").replace("owl:Ontology", "owl:Other") - ).map { case (iri, json) => iri -> ResourceGen.resource(iri, projectRef, json) } - // format: on - - val errorReport = ResourceResolutionReport() - - val fetchSchema: Resolve[Schema] = { - case (ref, `projectRef`, _) => IO.fromOption(expandedSchemaMap.get(ref.iri), errorReport) - case (_, _, _) => IO.raiseError(errorReport) - } - val fetchResource: Resolve[Resource] = { - case (ref, `projectRef`, _) => IO.fromOption(resourceMap.get(ref.iri), errorReport) - case (_, _, _) => IO.raiseError(errorReport) - } - - val imports = new SchemaImports(fetchSchema, fetchResource) - - "resolve all the imports" in { - val expanded = ExpandedJsonLd(json).accepted - val result = imports.resolve(parcellationlabel, projectRef, expanded).accepted - - result.toList.toSet shouldEqual - (resourceMap.take(1).values.map(_.expanded).toSet ++ Set( - entityExpandedSchema, - identifierExpandedSchema, - licenseExpandedSchema, - propertyValueExpandedSchema - ) + expanded) - } - - "fail to resolve an import if it is not found" in { - val other = iri"$neuroshapes/other" - val other2 = iri"$neuroshapes/other2" - val parcellation = json deepMerge json"""{"imports": ["$neuroshapes/commons/entity", "$other", "$other2"]}""" - val expanded = ExpandedJsonLd(parcellation).accepted - - imports.resolve(parcellationlabel, projectRef, expanded).rejected shouldEqual - InvalidSchemaResolution( - parcellationlabel, - schemaImports = Map(ResourceRef(other) -> errorReport, ResourceRef(other2) -> errorReport), - resourceImports = Map(ResourceRef(other) -> errorReport, ResourceRef(other2) -> errorReport), - nonOntologyResources = Set.empty - ) - } - - "fail to resolve an import if it is a resource without owl:Ontology type" in { - val wrong = iri"$neuroshapes/wrong/vocabulary" - val parcellation = json deepMerge json"""{"imports": ["$neuroshapes/commons/entity", "$wrong"]}""" - val expanded = ExpandedJsonLd(parcellation).accepted - - imports.resolve(parcellationlabel, projectRef, expanded).rejected shouldEqual - InvalidSchemaResolution( - parcellationlabel, - schemaImports = Map(ResourceRef(wrong) -> errorReport), - resourceImports = Map.empty, - nonOntologyResources = Set(ResourceRef(wrong)) - ) - } - } -} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemaImportsSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemaImportsSuite.scala new file mode 100644 index 0000000000..674f1d36d5 --- /dev/null +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemaImportsSuite.scala @@ -0,0 +1,128 @@ +package ch.epfl.bluebrain.nexus.delta.sdk.schemas + +import cats.data.NonEmptyList +import cats.effect.IO +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.ExpandedJsonLd +import ch.epfl.bluebrain.nexus.delta.sdk.Resolve +import ch.epfl.bluebrain.nexus.delta.sdk.generators.ResourceGen +import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller +import ch.epfl.bluebrain.nexus.delta.sdk.model.Tags +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.Resource +import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema +import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.SchemaRejection.InvalidSchemaResolution +import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ +import ch.epfl.bluebrain.nexus.delta.sdk.utils.Fixtures +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.User +import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef} +import ch.epfl.bluebrain.nexus.testkit.ce.CatsEffectSuite +import ch.epfl.bluebrain.nexus.testkit.{CirceLiteral, TestHelpers} +import io.circe.Json + +import scala.collection.immutable.VectorMap + +class SchemaImportsSuite extends CatsEffectSuite with TestHelpers with CirceLiteral with Fixtures { + + private val alice = User("alice", Label.unsafe("wonderland")) + implicit val aliceCaller: Caller = Caller(alice, Set(alice)) + + private val neuroshapes = "https://neuroshapes.org" + private val parcellationlabel = iri"$neuroshapes/dash/parcellationlabel" + private val json = jsonContentOf("schemas/parcellationlabel.json") + val projectRef = ProjectRef.unsafe("org", "proj") + + val entitySource = jsonContentOf("schemas/entity.json") + + val entityExpandedSchema = ExpandedJsonLd(jsonContentOf("schemas/entity-expanded.json")).accepted + val identifierExpandedSchema = ExpandedJsonLd(jsonContentOf("schemas/identifier-expanded.json")).accepted + val licenseExpandedSchema = ExpandedJsonLd(jsonContentOf("schemas/license-expanded.json")).accepted + val propertyValueExpandedSchema = ExpandedJsonLd(jsonContentOf("schemas/property-value-expanded.json")).accepted + + val expandedSchemaMap = Map( + iri"$neuroshapes/commons/entity" -> + Schema( + iri"$neuroshapes/commons/entity", + projectRef, + Tags.empty, + entitySource, + entityExpandedSchema.toCompacted(entitySource.topContextValueOrEmpty).accepted, + NonEmptyList.of( + entityExpandedSchema, + identifierExpandedSchema, + licenseExpandedSchema, + propertyValueExpandedSchema + ) + ) + ) + + // format: off + val resourceMap = VectorMap( + iri"$neuroshapes/commons/vocabulary" -> jsonContentOf("schemas/vocabulary.json"), + iri"$neuroshapes/wrong/vocabulary" -> jsonContentOf("schemas/vocabulary.json").replace("owl:Ontology", "owl:Other") + ).map { case (iri, json) => iri -> ResourceGen.resource(iri, projectRef, json) } + // format: on + + val errorReport = ResourceResolutionReport() + + val fetchSchema: Resolve[Schema] = { + case (ref, `projectRef`, _) => IO.pure(expandedSchemaMap.get(ref.iri).toRight(errorReport)) + case (_, _, _) => IO.pure(Left(errorReport)) + } + val fetchResource: Resolve[Resource] = { + case (ref, `projectRef`, _) => IO.pure(resourceMap.get(ref.iri).toRight(errorReport)) + case (_, _, _) => IO.pure(Left(errorReport)) + } + + private def toExpanded(json: Json) = toCatsIO(ExpandedJsonLd(json)) + + val imports = new SchemaImports(fetchSchema, fetchResource) + + test("Resolve all the imports") { + for { + expanded <- toExpanded(json) + result <- imports.resolve(parcellationlabel, projectRef, expanded) + } yield { + val expected = (resourceMap.take(1).values.map(_.expanded).toSet ++ Set( + entityExpandedSchema, + identifierExpandedSchema, + licenseExpandedSchema, + propertyValueExpandedSchema + ) + expanded) + assertEquals(result.toList.toSet, expected) + } + } + + test("Fail to resolve an import if it is not found") { + val other = iri"$neuroshapes/other" + val other2 = iri"$neuroshapes/other2" + val parcellation = json deepMerge json"""{"imports": ["$neuroshapes/commons/entity", "$other", "$other2"]}""" + + val expectedError = InvalidSchemaResolution( + parcellationlabel, + schemaImports = Map(ResourceRef(other) -> errorReport, ResourceRef(other2) -> errorReport), + resourceImports = Map(ResourceRef(other) -> errorReport, ResourceRef(other2) -> errorReport), + nonOntologyResources = Set.empty + ) + + toExpanded(parcellation).flatMap { expanded => + imports.resolve(parcellationlabel, projectRef, expanded).intercept(expectedError) + } + } + + test("Fail to resolve an import if it is a resource without owl:Ontology type") { + val wrong = iri"$neuroshapes/wrong/vocabulary" + val parcellation = json deepMerge json"""{"imports": ["$neuroshapes/commons/entity", "$wrong"]}""" + + val expectedError = InvalidSchemaResolution( + parcellationlabel, + schemaImports = Map(ResourceRef(wrong) -> errorReport), + resourceImports = Map.empty, + nonOntologyResources = Set(ResourceRef(wrong)) + ) + + toExpanded(parcellation).flatMap { expanded => + imports.resolve(parcellationlabel, projectRef, expanded).intercept(expectedError) + } + } +} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemasImplSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemasImplSuite.scala index 4eb0090979..8f48436b6d 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemasImplSuite.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/schemas/SchemasImplSuite.scala @@ -13,7 +13,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.model.{IdSegmentRef, Tags} import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.SchemaRejection._ import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject @@ -22,7 +21,6 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Identity, Label, ProjectRef import ch.epfl.bluebrain.nexus.delta.sourcing.postgres.Doobie import ch.epfl.bluebrain.nexus.testkit.ce.{CatsEffectSuite, IOFixedClock} import ch.epfl.bluebrain.nexus.testkit.{CirceLiteral, TestHelpers} -import monix.bio.{IO => BIO} import munit.AnyFixture import java.util.UUID @@ -53,15 +51,9 @@ class SchemasImplSuite contexts.schemasMetadata -> ContextValue.fromFile("contexts/schemas-metadata.json") ) - private val schemaImports: SchemaImports = new SchemaImports( - (_, _, _) => BIO.raiseError(ResourceResolutionReport()), - (_, _, _) => BIO.raiseError(ResourceResolutionReport()) - ) + private val schemaImports: SchemaImports = SchemaImports.alwaysFail - private val resolverContextResolution: ResolverContextResolution = new ResolverContextResolution( - res, - (_, _, _) => BIO.raiseError(ResourceResolutionReport()) - ) + private val resolverContextResolution: ResolverContextResolution = ResolverContextResolution(res) private val org = Label.unsafe("myorg") private val am = ApiMappings("nxv" -> nxv.base) diff --git a/delta/sourcing-psql/src/main/scala/ch/epfl/bluebrain/nexus/delta/sourcing/rejection/Rejection.scala b/delta/sourcing-psql/src/main/scala/ch/epfl/bluebrain/nexus/delta/sourcing/rejection/Rejection.scala index d5ab0a6f94..5ec5d0b48b 100644 --- a/delta/sourcing-psql/src/main/scala/ch/epfl/bluebrain/nexus/delta/sourcing/rejection/Rejection.scala +++ b/delta/sourcing-psql/src/main/scala/ch/epfl/bluebrain/nexus/delta/sourcing/rejection/Rejection.scala @@ -7,6 +7,8 @@ abstract class Rejection extends Exception with Product with Serializable { self override def fillInStackTrace(): Throwable = self + override def getMessage: String = reason + def reason: String } diff --git a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/ce/CatsEffectSuite.scala b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/ce/CatsEffectSuite.scala index 5c61d78599..5a618b9925 100644 --- a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/ce/CatsEffectSuite.scala +++ b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/ce/CatsEffectSuite.scala @@ -3,11 +3,11 @@ package ch.epfl.bluebrain.nexus.testkit.ce import cats.effect.{ContextShift, IO, Timer} import ch.epfl.bluebrain.nexus.testkit.NexusSuite import ch.epfl.bluebrain.nexus.testkit.bio.{CollectionAssertions, EitherAssertions, StreamAssertions} +import monix.bio.{IO => BIO} +import monix.execution.Scheduler import scala.concurrent.ExecutionContext import scala.concurrent.duration.{DurationInt, FiniteDuration} -import monix.bio.{IO => BIO} -import monix.execution.Scheduler /** * Adapted from: @@ -21,6 +21,8 @@ abstract class CatsEffectSuite with EitherAssertions { protected val ioTimeout: FiniteDuration = 45.seconds + implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global) + override def munitValueTransforms: List[ValueTransform] = super.munitValueTransforms ++ List(munitIOTransform, munitBIOTransform) diff --git a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/ce/CatsIOValues.scala b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/ce/CatsIOValues.scala index ea21b5afe9..b28bbbdbfb 100644 --- a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/ce/CatsIOValues.scala +++ b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/ce/CatsIOValues.scala @@ -2,7 +2,8 @@ package ch.epfl.bluebrain.nexus.testkit.ce import cats.effect.IO import org.scalactic.source -import org.scalatest.Assertions.fail +import org.scalatest.Assertion +import org.scalatest.Assertions._ import scala.reflect.ClassTag @@ -11,6 +12,9 @@ trait CatsIOValues { implicit final class CatsIOValuesOps[A](private val io: IO[A]) { def accepted: A = io.unsafeRunSync() + def rejected[E](expected: E)(implicit pos: source.Position, EE: ClassTag[E]): Assertion = + assertResult(expected)(rejectedWith[E]) + def rejectedWith[E](implicit pos: source.Position, EE: ClassTag[E]): E = { io.attempt.unsafeRunSync() match { case Left(EE(value)) => value