diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala index 0ac7f96eef..d535808d8e 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala @@ -24,7 +24,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.resources.{read import ch.epfl.bluebrain.nexus.delta.sdk.resources.NexusSource.DecodingOption import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.{InvalidJsonLdFormat, InvalidSchemaRejection, ResourceNotFound} import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.{Resource, ResourceRejection} -import ch.epfl.bluebrain.nexus.delta.sdk.resources.{NexusSource, Resources, ResourcesPractice} +import ch.epfl.bluebrain.nexus.delta.sdk.resources.{NexusSource, Resources} import io.circe.{Json, Printer} import monix.bio.IO import monix.execution.Scheduler @@ -47,7 +47,6 @@ final class ResourcesRoutes( identities: Identities, aclCheck: AclCheck, resources: Resources, - resourcesPractice: ResourcesPractice, schemeDirectives: DeltaSchemeDirectives, index: IndexingAction.Execute[Resource] )(implicit @@ -173,16 +172,6 @@ final class ResourcesRoutes( ) } }, - (pathPrefix("validate") & get & pathEndOrSingleSlash & idSegmentRef(id)) { id => - authorizeFor(ref, Write).apply { - emit( - resourcesPractice - .validate(id, ref, schemaOpt) - .leftWiden[ResourceRejection] - ) - } - - }, // Fetch a resource original source (pathPrefix("source") & get & pathEndOrSingleSlash & idSegmentRef(id)) { id => authorizeFor(ref, Read).apply { @@ -273,7 +262,6 @@ object ResourcesRoutes { identities: Identities, aclCheck: AclCheck, resources: Resources, - resourcesPractice: ResourcesPractice, projectsDirectives: DeltaSchemeDirectives, index: IndexingAction.Execute[Resource] )(implicit @@ -283,7 +271,7 @@ object ResourcesRoutes { ordering: JsonKeyOrdering, fusionConfig: FusionConfig, decodingOption: DecodingOption - ): Route = new ResourcesRoutes(identities, aclCheck, resources, resourcesPractice, projectsDirectives, index).routes + ): Route = new ResourcesRoutes(identities, aclCheck, resources, projectsDirectives, index).routes def asSourceWithMetadata( resource: ResourceF[Resource] diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesTrialRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesTrialRoutes.scala new file mode 100644 index 0000000000..0a1e688917 --- /dev/null +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesTrialRoutes.scala @@ -0,0 +1,162 @@ +package ch.epfl.bluebrain.nexus.delta.routes + +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.Route +import cats.syntax.all._ +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ +import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.schemas +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution +import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering +import ch.epfl.bluebrain.nexus.delta.routes.ResourcesTrialRoutes.SchemaInput._ +import ch.epfl.bluebrain.nexus.delta.routes.ResourcesTrialRoutes.{GenerateSchema, GenerationInput} +import ch.epfl.bluebrain.nexus.delta.sdk.SchemaResource +import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck +import ch.epfl.bluebrain.nexus.delta.sdk.circe.CirceUnmarshalling +import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._ +import ch.epfl.bluebrain.nexus.delta.sdk.directives.{AuthDirectives, DeltaSchemeDirectives} +import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities +import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller +import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfMarshalling +import ch.epfl.bluebrain.nexus.delta.sdk.model.IdSegment.IriSegment +import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegment} +import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.resources.{write => Write} +import ch.epfl.bluebrain.nexus.delta.sdk.resources.NexusSource.DecodingOption +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection +import ch.epfl.bluebrain.nexus.delta.sdk.resources.{NexusSource, ResourcesTrial} +import ch.epfl.bluebrain.nexus.delta.sdk.schemas.Schemas +import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.SchemaRejection +import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef +import io.circe.generic.extras.Configuration +import io.circe.generic.extras.semiauto.deriveConfiguredDecoder +import io.circe.{Decoder, Json} +import monix.bio.IO +import monix.execution.Scheduler + +import scala.annotation.nowarn + +/** + * The resource trial routes allowing to do read-only operations on resources + */ +final class ResourcesTrialRoutes( + identities: Identities, + aclCheck: AclCheck, + generateSchema: GenerateSchema, + resourcesTrial: ResourcesTrial, + schemeDirectives: DeltaSchemeDirectives +)(implicit + baseUri: BaseUri, + s: Scheduler, + cr: RemoteContextResolution, + ordering: JsonKeyOrdering, + decodingOption: DecodingOption +) extends AuthDirectives(identities, aclCheck) + with CirceUnmarshalling + with RdfMarshalling { + + import schemeDirectives._ + def routes: Route = + baseUriPrefix(baseUri.prefix) { + concat(validateRoute, generateRoute) + } + + private def validateRoute: Route = + pathPrefix("resources") { + extractCaller { implicit caller => + resolveProjectRef.apply { project => + authorizeFor(project, Write).apply { + (get & idSegment & idSegmentRef & pathPrefix("validate") & pathEndOrSingleSlash) { (schema, id) => + val schemaOpt = underscoreToOption(schema) + emit( + resourcesTrial.validate(id, project, schemaOpt).leftWiden[ResourceRejection] + ) + } + } + } + } + } + + private def generateRoute: Route = + (get & pathPrefix("trial") & pathPrefix("resources")) { + extractCaller { implicit caller => + (resolveProjectRef & pathEndOrSingleSlash) { project => + authorizeFor(project, Write).apply { + (entity(as[GenerationInput])) { input => + generate(project, input) + } + } + } + } + } + + // Call the generate method matching the schema input + private def generate(project: ProjectRef, input: GenerationInput)(implicit caller: Caller) = + input.schema match { + case ExistingSchema(schemaId) => + emit(resourcesTrial.generate(project, schemaId, input.resource).flatMap(_.asJson)) + case NewSchema(schemaSource) => + emit( + generateSchema(project, schemaSource, caller).flatMap { schema => + resourcesTrial.generate(project, schema, input.resource).flatMap(_.asJson) + } + ) + } + +} + +object ResourcesTrialRoutes { + + type GenerateSchema = (ProjectRef, Json, Caller) => IO[SchemaRejection, SchemaResource] + + sealed private[routes] trait SchemaInput extends Product + + private[routes] object SchemaInput { + + // Validate the generated resource with an existing schema + final case class ExistingSchema(id: IdSegment) extends SchemaInput + + // Validate the generated resource with the new schema bundled in the request + final case class NewSchema(json: Json) extends SchemaInput + + implicit val schemaInputDecoder: Decoder[SchemaInput] = + Decoder.instance { hc => + val value = hc.value + val existingSchema = value.asString.map { s => ExistingSchema(IdSegment(s)) } + val newSchema = NewSchema(value) + Right(existingSchema.getOrElse(newSchema)) + } + } + + private val noSchema = ExistingSchema(IriSegment(schemas.resources)) + + final private[routes] case class GenerationInput(schema: SchemaInput = noSchema, resource: NexusSource) + + private[routes] object GenerationInput { + @nowarn("cat=unused") + implicit def generationInputDecoder(implicit decodingOption: DecodingOption): Decoder[GenerationInput] = { + implicit val configuration: Configuration = Configuration.default.withDefaults + implicit val nexusSourceDecoder: Decoder[NexusSource] = NexusSource.nexusSourceDecoder + deriveConfiguredDecoder[GenerationInput] + } + } + + def apply( + identities: Identities, + aclCheck: AclCheck, + schemas: Schemas, + resourcesTrial: ResourcesTrial, + schemeDirectives: DeltaSchemeDirectives + )(implicit + baseUri: BaseUri, + s: Scheduler, + cr: RemoteContextResolution, + ordering: JsonKeyOrdering, + decodingOption: DecodingOption + ): ResourcesTrialRoutes = + new ResourcesTrialRoutes( + identities, + aclCheck, + (project, source, caller) => schemas.createDryRun(project, source)(caller).toBIO[SchemaRejection], + resourcesTrial, + schemeDirectives + ) +} 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 08c7f0408c..cc6b14907d 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 @@ -166,6 +166,7 @@ class DeltaModule(appCfg: AppConfig, config: Config)(implicit classLoader: Class include(ResolversModule) include(SchemasModule) include(ResourcesModule) + include(ResourcesTrialModule) include(MultiFetchModule) include(IdentitiesModule) include(VersionModule) diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResourcesModule.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResourcesModule.scala index f00f863f73..d4ec23b9b8 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResourcesModule.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResourcesModule.scala @@ -22,7 +22,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.ResourceRe import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.{ResolverContextResolution, Resolvers, ResourceResolution} import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.ProjectContextRejection import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.{Resource, ResourceEvent} -import ch.epfl.bluebrain.nexus.delta.sdk.resources.{Resources, ResourcesImpl, ResourcesPractice, ValidateResource} +import ch.epfl.bluebrain.nexus.delta.sdk.resources.{Resources, ResourcesConfig, ResourcesImpl, ValidateResource} import ch.epfl.bluebrain.nexus.delta.sdk.schemas.Schemas import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema import ch.epfl.bluebrain.nexus.delta.sdk.sse.SseEncoder @@ -40,11 +40,13 @@ object ResourcesModule extends ModuleDef { ValidateResource(resourceResolution)(api) } + make[ResourcesConfig].from { (config: AppConfig) => config.resources } + make[Resources].from { ( validate: ValidateResource, fetchContext: FetchContext[ContextRejection], - config: AppConfig, + config: ResourcesConfig, resolverContextResolution: ResolverContextResolution, api: JsonLdApi, xas: Transactors, @@ -55,7 +57,7 @@ object ResourcesModule extends ModuleDef { validate, fetchContext.mapRejection(ProjectContextRejection), resolverContextResolution, - config.resources, + config, xas )( api, @@ -64,24 +66,6 @@ object ResourcesModule extends ModuleDef { ) } - make[ResourcesPractice].from { - ( - resources: Resources, - validate: ValidateResource, - fetchContext: FetchContext[ContextRejection], - contextResolution: ResolverContextResolution, - api: JsonLdApi, - clock: Clock[UIO], - uuidF: UUIDF - ) => - ResourcesPractice( - resources.fetch(_, _, None), - validate, - fetchContext.mapRejection(ProjectContextRejection), - contextResolution - )(api, clock, uuidF) - } - make[ResolverContextResolution].from { (aclCheck: AclCheck, resolvers: Resolvers, resources: Resources, rcr: RemoteContextResolution @Id("aggregate")) => ResolverContextResolution(aclCheck, resolvers, resources, rcr) @@ -96,7 +80,6 @@ object ResourcesModule extends ModuleDef { identities: Identities, aclCheck: AclCheck, resources: Resources, - resourcesPractice: ResourcesPractice, schemeDirectives: DeltaSchemeDirectives, indexingAction: IndexingAction @Id("aggregate"), shift: Resource.Shift, @@ -105,13 +88,12 @@ object ResourcesModule extends ModuleDef { cr: RemoteContextResolution @Id("aggregate"), ordering: JsonKeyOrdering, fusionConfig: FusionConfig, - config: AppConfig + config: ResourcesConfig ) => new ResourcesRoutes( identities, aclCheck, resources, - resourcesPractice, schemeDirectives, indexingAction(_, _, _)(shift, cr) )( @@ -120,7 +102,7 @@ object ResourcesModule extends ModuleDef { cr, ordering, fusionConfig, - config.resources.decodingOption + config.decodingOption ) } diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResourcesTrialModule.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResourcesTrialModule.scala new file mode 100644 index 0000000000..7135e8d258 --- /dev/null +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResourcesTrialModule.scala @@ -0,0 +1,81 @@ +package ch.epfl.bluebrain.nexus.delta.wiring + +import cats.effect.Clock +import ch.epfl.bluebrain.nexus.delta.Main.pluginsMinPriority +import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.JsonLdApi +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution +import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering +import ch.epfl.bluebrain.nexus.delta.routes.ResourcesTrialRoutes +import ch.epfl.bluebrain.nexus.delta.sdk.PriorityRoute +import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck +import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaSchemeDirectives +import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities +import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri +import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContext +import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContext.ContextRejection +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.ProjectContextRejection +import ch.epfl.bluebrain.nexus.delta.sdk.resources.{Resources, ResourcesConfig, ResourcesTrial, ValidateResource} +import ch.epfl.bluebrain.nexus.delta.sdk.schemas.Schemas +import distage.ModuleDef +import izumi.distage.model.definition.Id +import monix.bio.UIO +import monix.execution.Scheduler + +/** + * Resources trial wiring + */ +object ResourcesTrialModule extends ModuleDef { + + make[ResourcesTrial].from { + ( + resources: Resources, + validate: ValidateResource, + fetchContext: FetchContext[ContextRejection], + contextResolution: ResolverContextResolution, + api: JsonLdApi, + clock: Clock[UIO], + uuidF: UUIDF + ) => + ResourcesTrial( + resources.fetch(_, _, None), + validate, + fetchContext.mapRejection(ProjectContextRejection), + contextResolution + )(api, clock, uuidF) + } + + make[ResourcesTrialRoutes].from { + ( + identities: Identities, + aclCheck: AclCheck, + schemas: Schemas, + resourcesTrial: ResourcesTrial, + schemeDirectives: DeltaSchemeDirectives, + baseUri: BaseUri, + s: Scheduler, + cr: RemoteContextResolution @Id("aggregate"), + ordering: JsonKeyOrdering, + config: ResourcesConfig + ) => + ResourcesTrialRoutes( + identities, + aclCheck, + schemas, + resourcesTrial, + schemeDirectives + )( + baseUri, + s, + cr, + ordering, + config.decodingOption + ) + } + + many[PriorityRoute].add { (route: ResourcesTrialRoutes) => + PriorityRoute(pluginsMinPriority - 1, route.routes, requiresStrictEntity = true) + } + +} diff --git a/delta/app/src/test/resources/trial/generated-resource.json b/delta/app/src/test/resources/trial/generated-resource.json new file mode 100644 index 0000000000..54a43920a5 --- /dev/null +++ b/delta/app/src/test/resources/trial/generated-resource.json @@ -0,0 +1,24 @@ +{ + "@context": [ + "https://bluebrain.github.io/nexus/contexts/metadata.json", + { + "@vocab": "https://bluebrain.github.io/nexus/vocabulary/" + }, + "https://neuroshapes.org" + ], + "@id": "https://bluebrain.github.io/nexus/vocabulary/myId", + "@type": "Morphology", + "name": "Morphology 001", + "_constrainedBy": "https://bluebrain.github.io/nexus/vocabulary/myschema", + "_createdAt": "1970-01-01T00:00:00Z", + "_createdBy": "http://localhost/v1/realms/wonderland/users/alice", + "_deprecated": false, + "_incoming": "http://localhost/v1/resources/myorg/myproj/_/https:%2F%2Fbluebrain.github.io%2Fnexus%2Fvocabulary%2FmyId/incoming", + "_outgoing": "http://localhost/v1/resources/myorg/myproj/_/https:%2F%2Fbluebrain.github.io%2Fnexus%2Fvocabulary%2FmyId/outgoing", + "_project": "http://localhost/v1/projects/myorg/myproj", + "_rev": 1, + "_schemaProject": "http://localhost/v1/projects/myorg/myproj", + "_self": "http://localhost/v1/resources/myorg/myproj/_/https:%2F%2Fbluebrain.github.io%2Fnexus%2Fvocabulary%2FmyId", + "_updatedAt": "1970-01-01T00:00:00Z", + "_updatedBy": "http://localhost/v1/realms/wonderland/users/alice" +} \ No newline at end of file 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 8008dd5389..3a4d0b3492 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 @@ -25,7 +25,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.FetchResou 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, ResourcesPractice, ValidateResource} +import ch.epfl.bluebrain.nexus.delta.sdk.resources.{Resources, ResourcesConfig, ResourcesImpl, ValidateResource} import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject} @@ -71,6 +71,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { private val myIdEncoded = UrlUtils.encode(myId.toString) private val myId2Encoded = UrlUtils.encode(myId2.toString) private val payload = jsonContentOf("resources/resource.json", "id" -> myId) + private val payloadWithoutId = payload.removeKeys(keywords.id) private val payloadWithBlankId = jsonContentOf("resources/resource.json", "id" -> "") private val payloadWithUnderscoreFields = jsonContentOf("resources/resource-with-underscore-fields.json", "id" -> myId5) @@ -110,12 +111,6 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { IdentitiesDummy(caller), aclCheck, resources, - ResourcesPractice( - resources.fetch(_, _, None), - validator, - fetchContext, - resolverContextResolution - ), DeltaSchemeDirectives(fetchContext, ioFromMap(uuid -> projectRef.organization), ioFromMap(uuid -> projectRef)), IndexingAction.noop ) @@ -181,15 +176,23 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { } "fail to create a resource that does not validate against a schema" in { + val payloadFailingSchemaConstraints = payloadWithoutId.replaceKeyWithValue("number", "wrong") Put( "/v1/resources/myorg/myproject/nxv:myschema/wrong", - payload.removeKeys(keywords.id).replaceKeyWithValue("number", "wrong").toEntity + payloadFailingSchemaConstraints.toEntity ) ~> routes ~> check { response.status shouldEqual StatusCodes.BadRequest response.asJson shouldEqual jsonContentOf("/resources/errors/invalid-resource.json") } } + "fail to create a resource against a schema that does not exist" in { + Put("/v1/resources/myorg/myproject/pretendschema/someid", payloadWithoutId.toEntity) ~> routes ~> check { + status shouldEqual StatusCodes.NotFound + response.asJson shouldEqual jsonContentOf("/schemas/errors/invalid-schema-2.json") + } + } + "fail if the id is blank" in { Post("/v1/resources/myorg/myproject/_/", payloadWithBlankId.toEntity) ~> routes ~> check { response.status shouldEqual StatusCodes.BadRequest @@ -490,68 +493,6 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { } } - "validate a resource successfully against the unconstrained schema" in { - Get( - s"/v1/resources/myorg/myproject/${UrlUtils.encode(schemas.resources.toString)}/myid2/validate" - ) ~> routes ~> check { - status shouldEqual StatusCodes.OK - response.asJson shouldEqual - json"""{ - "@context" : "https://bluebrain.github.io/nexus/contexts/validation.json", - "@type" : "NoValidation", - "project": "myorg/myproject", - "schema" : "https://bluebrain.github.io/nexus/schemas/unconstrained.json?rev=1" - }""" - } - } - - "validate a resource successfully against its latest schema" in { - Get("/v1/resources/myorg/myproject/_/myid2/validate") ~> routes ~> check { - status shouldEqual StatusCodes.OK - response.asJson shouldEqual - json"""{ - "@context" : [ - "https://bluebrain.github.io/nexus/contexts/shacl-20170720.json", - "https://bluebrain.github.io/nexus/contexts/validation.json" - ], - "@type" : "Validated", - "project": "myorg/myproject", - "schema" : "https://bluebrain.github.io/nexus/vocabulary/myschema?rev=1", - "report": { - "@type" : "sh:ValidationReport", - "conforms" : true, - "targetedNodes" : 10 - } - }""" - } - } - - "validate a resource against a schema that does not exist" in { - Get("/v1/resources/myorg/myproject/pretendschema/myid2/validate") ~> routes ~> check { - status shouldEqual StatusCodes.NotFound - response.asJson shouldEqual jsonContentOf("/schemas/errors/invalid-schema-2.json") - } - } - - "validate a resource that does not exist" in { - Get("/v1/resources/myorg/myproject/_/pretendresource/validate") ~> routes ~> check { - status shouldEqual StatusCodes.NotFound - response.asJson shouldEqual jsonContentOf( - "/resources/errors/not-found.json", - "id" -> (nxv + "pretendresource").toString, - "proj" -> "myorg/myproject" - ) - } - } - - "fail to validate a resource without resources/write permission" in { - aclCheck.subtract(AclAddress.Root, Anonymous -> Set(resources.write)).accepted - Get("/v1/resources/myorg/myproject/_/myid2/validate") ~> routes ~> check { - response.status shouldEqual StatusCodes.Forbidden - response.asJson shouldEqual jsonContentOf("errors/authorization-failed.json") - } - } - "fetch the resource tags" in { aclCheck.append(AclAddress.Root, Anonymous -> Set(resources.write)).accepted Get("/v1/resources/myorg/myproject/_/myid2/tags?rev=1", payload.toEntity) ~> routes ~> check { diff --git a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesTrialRoutesSpec.scala b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesTrialRoutesSpec.scala new file mode 100644 index 0000000000..af1b8a7852 --- /dev/null +++ b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesTrialRoutesSpec.scala @@ -0,0 +1,256 @@ +package ch.epfl.bluebrain.nexus.delta.routes + +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.model.headers.OAuth2BearerToken +import akka.http.scaladsl.server.Route +import ch.epfl.bluebrain.nexus.delta.kernel.utils.UrlUtils +import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, nxv, schemas} +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords +import ch.epfl.bluebrain.nexus.delta.routes.ResourcesTrialRoutes.GenerateSchema +import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclSimpleCheck +import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaSchemeDirectives +import ch.epfl.bluebrain.nexus.delta.sdk.generators.SchemaGen +import ch.epfl.bluebrain.nexus.delta.sdk.identities.IdentitiesDummy +import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller +import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ +import ch.epfl.bluebrain.nexus.delta.sdk.model.IdSegment.{IriSegment, StringSegment} +import ch.epfl.bluebrain.nexus.delta.sdk.model.{IdSegment, IdSegmentRef, ResourceF, Tags} +import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions +import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy +import ch.epfl.bluebrain.nexus.delta.sdk.resources.NexusSource.DecodingOption +import ch.epfl.bluebrain.nexus.delta.sdk.resources.ValidationResult._ +import ch.epfl.bluebrain.nexus.delta.sdk.resources._ +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.{ProjectContextRejection, ReservedResourceId, ResourceNotFound} +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.{ResourceGenerationResult, ResourceRejection, ResourceState} +import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema +import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.SchemaRejection._ +import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.ResourceRef.Revision +import monix.bio.{IO, UIO} + +import java.time.Instant + +class ResourcesTrialRoutesSpec extends BaseRouteSpec with ResourceInstanceFixture with ValidateResourceFixture { + + implicit private val caller: Caller = + Caller(alice, Set(alice, Anonymous, Authenticated(realm), Group("group", realm))) + + private val asAlice = addCredentials(OAuth2BearerToken("alice")) + + private val permissions = Set(Permissions.resources.write) + private val aclCheck = AclSimpleCheck((alice, projectRef, permissions)).accepted + private val fetchContext = FetchContextDummy(List.empty, ProjectContextRejection) + + private val schemaSource = jsonContentOf("resources/schema.json").addContext(contexts.shacl, contexts.schemasMetadata) + private val schemaId = nxv + "myschema" + private val schema1 = SchemaGen.schema(schemaId, projectRef, schemaSource.removeKeys(keywords.id)) + + private val validSource = source + private val invalidSource = json"""{ "invalid": "xxx"}""" + + private val instant: Instant = Instant.EPOCH + private val resourceF = ResourceState( + myId, + projectRef, + projectRef, + source, + compacted, + expanded, + remoteContextRefs, + rev = 1, + deprecated = false, + Revision(schemaId, 1), + types, + Tags.empty, + createdAt = instant, + createdBy = alice, + updatedAt = instant, + updatedBy = alice + ).toResource + + private val expectedError = ReservedResourceId(nxv + "invalid") + + private val resourcesTrial = new ResourcesTrial { + override def generate(project: ProjectRef, schema: IdSegment, source: NexusSource)(implicit + caller: Caller + ): UIO[ResourceGenerationResult] = + generate(source, None) + + override def generate(project: ProjectRef, schema: ResourceF[Schema], source: NexusSource)(implicit + caller: Caller + ): UIO[ResourceGenerationResult] = + generate(source, Some(schema)) + + // Successfully generates a resource if `validSource` is passed, fails otherwise + private def generate(source: NexusSource, schemaOpt: Option[ResourceF[Schema]]): UIO[ResourceGenerationResult] = + UIO.pure { + source match { + case NexusSource(`validSource`) => ResourceGenerationResult(schemaOpt, Right(resourceF)) + case _ => ResourceGenerationResult(schemaOpt, Left(expectedError)) + } + } + + override def validate(id: IdSegmentRef, project: ProjectRef, schemaOpt: Option[IdSegment])(implicit + caller: Caller + ): IO[ResourceRejection, ValidationResult] = + (id.value, schemaOpt) match { + // Returns a validated result for myId when no schema is provided + case (StringSegment("myId") | IriSegment(`myId`), None) => + UIO.pure(Validated(projectRef, ResourceRef.Revision(schemaId, defaultSchemaRevision), defaultReport)) + // Returns no validation result for myId for `schemas.resources` + case (StringSegment("myId") | IriSegment(`myId`), Some(IriSegment(schemas.resources))) => + UIO.pure(NoValidation(projectRef)) + case (IriSegment(iri), None) => IO.raiseError(ResourceNotFound(iri, project, None)) + case _ => IO.terminate(new IllegalStateException("Should not happen !")) + } + } + + private val generateSchema: GenerateSchema = { + case (_, `schemaSource`, _) => UIO.pure(SchemaGen.resourceFor(schema1)) + case _ => IO.raiseError(SchemaShaclEngineRejection(nxv + "invalid", "Invalid schema")) + } + + implicit val decodingOption: DecodingOption = DecodingOption.Strict + + private lazy val routes = + Route.seal( + new ResourcesTrialRoutes( + IdentitiesDummy(caller), + aclCheck, + generateSchema, + resourcesTrial, + DeltaSchemeDirectives(fetchContext) + ).routes + ) + + "A resource trial route" should { + + "fail to generate a resource for a user without access" in { + val payload = json"""{ "resource": $validSource }""" + Get(s"/v1/trial/resources/$projectRef/", payload.toEntity) ~> routes ~> check { + response.status shouldEqual StatusCodes.Forbidden + response.asJson shouldEqual jsonContentOf("errors/authorization-failed.json") + } + } + + "generate a resource without passing a schema" in { + val payload = json"""{ "resource": $validSource }""" + Get(s"/v1/trial/resources/$projectRef/", payload.toEntity) ~> asAlice ~> routes ~> check { + response.status shouldEqual StatusCodes.OK + val jsonResponse = response.asJsonObject + jsonResponse("schema") shouldBe empty + jsonResponse("result") shouldEqual Some(jsonContentOf("trial/generated-resource.json")) + jsonResponse("error") shouldBe empty + } + } + + "generate a resource passing a new schema" in { + val payload = json"""{ "schema": $schemaSource, "resource": $validSource }""" + Get(s"/v1/trial/resources/$projectRef/", payload.toEntity) ~> asAlice ~> routes ~> check { + response.status shouldEqual StatusCodes.OK + val jsonResponse = response.asJsonObject + jsonResponse("schema") should not be empty + jsonResponse("result") should not be empty + jsonResponse("error") shouldBe empty + } + } + + "fails to generate a resource when passing an invalid new schema" in { + val payload = json"""{ "schema": { "invalid": "xxx" }, "resource": $validSource }""" + Get(s"/v1/trial/resources/$projectRef/", payload.toEntity) ~> asAlice ~> routes ~> check { + response.status shouldEqual StatusCodes.BadRequest + response.asJson shouldEqual + json"""{ + "@context" : "https://bluebrain.github.io/nexus/contexts/error.json", + "@type" : "SchemaShaclEngineRejection", + "reason" : "Schema 'https://bluebrain.github.io/nexus/vocabulary/invalid' failed to produce a SHACL engine for the SHACL schema.", + "details" : "Invalid schema" + } + """ + } + } + + "fails to generate a resource when the resource payload is invalid and without passing a schema" in { + val payload = json"""{ "resource": $invalidSource }""" + Get(s"/v1/trial/resources/$projectRef/", payload.toEntity) ~> asAlice ~> routes ~> check { + response.status shouldEqual StatusCodes.OK + response.asJson shouldEqual + json""" + { + "error" : { + "@context" : "https://bluebrain.github.io/nexus/contexts/error.json", + "@type" : "ReservedResourceId", + "reason" : "Resource identifier 'https://bluebrain.github.io/nexus/vocabulary/invalid' is reserved for the platform." + } + }""" + } + } + + "fail to generate a resource passing a new schema" in { + val payload = json"""{ "schema": $schemaSource, "resource": $invalidSource }""" + Get(s"/v1/trial/resources/$projectRef/", payload.toEntity) ~> asAlice ~> routes ~> check { + response.status shouldEqual StatusCodes.OK + val jsonResponse = response.asJsonObject + jsonResponse("schema") should not be empty + jsonResponse("result") shouldBe empty + jsonResponse("error") should not be empty + } + } + + "fail to validate for a user without access" in { + Get(s"/v1/resources/$projectRef/_/myId/validate") ~> routes ~> check { + response.status shouldEqual StatusCodes.Forbidden + response.asJson shouldEqual jsonContentOf("errors/authorization-failed.json") + } + } + + s"successfully validate $myId for a user with access against the unconstrained schema" in { + val unconstrained = UrlUtils.encode(schemas.resources.toString) + Get(s"/v1/resources/$projectRef/$unconstrained/myId/validate") ~> asAlice ~> routes ~> check { + response.status shouldEqual StatusCodes.OK + response.asJson shouldEqual + json"""{ + "@context" : "https://bluebrain.github.io/nexus/contexts/validation.json", + "@type" : "NoValidation", + "project": "myorg/myproj", + "schema" : "https://bluebrain.github.io/nexus/schemas/unconstrained.json?rev=1" + }""" + } + } + + s"successfully validate $myId for a user with access against its latest schema" in { + Get(s"/v1/resources/$projectRef/_/myId/validate") ~> asAlice ~> routes ~> check { + response.status shouldEqual StatusCodes.OK + response.asJson shouldEqual + json"""{ + "@context" : [ + "https://bluebrain.github.io/nexus/contexts/shacl-20170720.json", + "https://bluebrain.github.io/nexus/contexts/validation.json" + ], + "@type" : "Validated", + "project": "myorg/myproj", + "schema" : "https://bluebrain.github.io/nexus/vocabulary/myschema?rev=1", + "report": { + "conforms" : "true" + } + }""" + } + } + + "fail to validate an unknown resource" in { + val unknownResource = nxv + "unknown" + val unknownEncoded = UrlUtils.encode(unknownResource.toString) + Get(s"/v1/resources/$projectRef/_/$unknownEncoded/validate") ~> asAlice ~> routes ~> check { + response.status shouldEqual StatusCodes.NotFound + response.asJson shouldEqual jsonContentOf( + "/resources/errors/not-found.json", + "id" -> unknownResource.toString, + "proj" -> projectRef.toString + ) + } + } + } + +} diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesPractice.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesTrial.scala similarity index 89% rename from delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesPractice.scala rename to delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesTrial.scala index b1868acd61..987b77a0f1 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesPractice.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesTrial.scala @@ -12,7 +12,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContext import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution import ch.epfl.bluebrain.nexus.delta.sdk.resources.Resources.expandResourceRef import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.{ProjectContextRejection, ResourceFetchRejection} -import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.{ResourceRejection, ResourceState} +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.{ResourceGenerationResult, ResourceRejection, ResourceState} import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef import monix.bio.{IO, UIO} @@ -20,7 +20,7 @@ import monix.bio.{IO, UIO} /** * Operations allowing to perform read-only operations on resources */ -trait ResourcesPractice { +trait ResourcesTrial { /** * Generates the resource and validate it against the provided schema reference @@ -35,7 +35,7 @@ trait ResourcesPractice { */ def generate(project: ProjectRef, schema: IdSegment, source: NexusSource)(implicit caller: Caller - ): IO[ResourceRejection, DataResource] + ): UIO[ResourceGenerationResult] /** * Generates the resource and validate it against the provided schema @@ -51,7 +51,7 @@ trait ResourcesPractice { */ def generate(project: ProjectRef, schema: ResourceF[Schema], source: NexusSource)(implicit caller: Caller - ): IO[ResourceRejection, DataResource] + ): UIO[ResourceGenerationResult] /** * Validates an existing resource. @@ -69,36 +69,42 @@ trait ResourcesPractice { ): IO[ResourceRejection, ValidationResult] } -object ResourcesPractice { +object ResourcesTrial { def apply( fetchResource: (IdSegmentRef, ProjectRef) => IO[ResourceFetchRejection, DataResource], validateResource: ValidateResource, fetchContext: FetchContext[ProjectContextRejection], contextResolution: ResolverContextResolution - )(implicit api: JsonLdApi, clock: Clock[UIO], uuidF: UUIDF): ResourcesPractice = new ResourcesPractice { + )(implicit api: JsonLdApi, clock: Clock[UIO], uuidF: UUIDF): ResourcesTrial = new ResourcesTrial { private val sourceParser = JsonLdSourceResolvingParser[ResourceRejection](contextResolution, uuidF) override def generate(project: ProjectRef, schema: IdSegment, source: NexusSource)(implicit caller: Caller - ): IO[ResourceRejection, DataResource] = + ): UIO[ResourceGenerationResult] = { for { projectContext <- fetchContext.onRead(project) schemaRef <- Resources.expandResourceRef(schema, projectContext) jsonld <- sourceParser(project, projectContext, source.value) validation <- validateResource(jsonld.iri, jsonld.expanded, schemaRef, project, caller) - res <- toResourceF(project, jsonld, source, validation) - } yield res + result <- toResourceF(project, jsonld, source, validation) + } yield result + }.attempt.map { attempt => + ResourceGenerationResult(None, attempt) + } override def generate(project: ProjectRef, schema: ResourceF[Schema], source: NexusSource)(implicit caller: Caller - ): IO[ResourceRejection, DataResource] = + ): UIO[ResourceGenerationResult] = { for { projectContext <- fetchContext.onRead(project) jsonld <- sourceParser(project, projectContext, source.value) validation <- validateResource(jsonld.iri, jsonld.expanded, schema) - res <- toResourceF(project, jsonld, source, validation) - } yield res + result <- toResourceF(project, jsonld, source, validation) + } yield result + }.attempt.map { attempt => + ResourceGenerationResult(Some(schema), attempt) + } def validate(id: IdSegmentRef, project: ProjectRef, schemaOpt: Option[IdSegment])(implicit caller: Caller diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceGenerationResult.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceGenerationResult.scala new file mode 100644 index 0000000000..4fa1041b6c --- /dev/null +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceGenerationResult.scala @@ -0,0 +1,54 @@ +package ch.epfl.bluebrain.nexus.delta.sdk.resources.model + +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.{DataResource, SchemaResource} +import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceF} +import io.circe.Json +import monix.bio.{IO, UIO} +import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ +import ResourceGenerationResult._ +import ch.epfl.bluebrain.nexus.delta.rdf.RdfError +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder + +/** + * Result of the generation of a resource + * @param schema + * the schema if it has been generated + * @param attempt + * the result of the generation attempt + */ +final case class ResourceGenerationResult( + schema: Option[SchemaResource], + attempt: Either[ResourceRejection, DataResource] +) { + + def asJson(implicit base: BaseUri, rcr: RemoteContextResolution): UIO[Json] = { + for { + schema <- schema.fold(emptySchema)(toJsonField("schema", _)) + resourceOrError <- attempt.fold( + toJsonField("error", _), + toJsonField("result", _) + ) + } yield schema deepMerge resourceOrError + }.hideErrors + + private def toJsonField[A](fieldName: String, value: A)(implicit + encoder: JsonLdEncoder[A], + rcr: RemoteContextResolution + ) = + value.toCompactedJsonLd.map { v => v.json }.map { s => Json.obj(fieldName -> s) } + + private def toJsonField[A](fieldName: String, value: ResourceF[A])(implicit + encoder: JsonLdEncoder[A], + base: BaseUri, + rcr: RemoteContextResolution + ) = + value.toCompactedJsonLd.map { v => v.json }.map { s => Json.obj(fieldName -> s) } +} + +object ResourceGenerationResult { + implicit private[model] val api: JsonLdApi = JsonLdJavaApi.lenient + + val emptySchema: IO[RdfError, Json] = IO.pure(Json.obj()) +} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourceFixture.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourceInstanceFixture.scala similarity index 97% rename from delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourceFixture.scala rename to delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourceInstanceFixture.scala index fd98f9adfe..87b832dd7f 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourceFixture.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourceInstanceFixture.scala @@ -14,7 +14,7 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef} import ch.epfl.bluebrain.nexus.testkit.CirceLiteral import io.circe.{Json, JsonObject} -private[resources] object ResourceFixture extends CirceLiteral { +trait ResourceInstanceFixture extends CirceLiteral { val org: Label = Label.unsafe("myorg") val proj: Label = Label.unsafe("myproj") diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesSpec.scala index a6bb315817..2f6d865825 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesSpec.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesSpec.scala @@ -40,6 +40,7 @@ class ResourcesSpec with CirceLiteral with OptionValues with ValidateResourceFixture + with ResourceInstanceFixture with Fixtures { "The Resources state machine" when { @@ -48,8 +49,6 @@ class ResourcesSpec val subject = User("myuser", Label.unsafe("myrealm")) val caller = Caller(subject, Set.empty) - import ch.epfl.bluebrain.nexus.delta.sdk.resources.ResourceFixture._ - val jsonld = JsonLdResult(myId, compacted, expanded, remoteContexts) val schema1 = nxv + "myschema" diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesPracticeSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesTrialSuite.scala similarity index 77% rename from delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesPracticeSuite.scala rename to delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesTrialSuite.scala index 59e7c3118d..bfd737d63f 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesPracticeSuite.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesTrialSuite.scala @@ -5,6 +5,7 @@ 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.JsonLdContext.keywords import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution} +import ch.epfl.bluebrain.nexus.delta.sdk.SchemaResource import ch.epfl.bluebrain.nexus.delta.sdk.generators.{ProjectGen, ResourceGen, SchemaGen} import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy @@ -12,15 +13,17 @@ 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.sourcing.model.ResourceRef.Revision import ch.epfl.bluebrain.nexus.testkit.bio.BioSuite import ch.epfl.bluebrain.nexus.testkit.{IOFixedClock, TestHelpers} -import monix.bio.IO +import monix.bio.{IO, UIO} +import munit.Location import java.util.UUID -class ResourcesPracticeSuite extends BioSuite with ValidateResourceFixture with TestHelpers with IOFixedClock { +class ResourcesTrialSuite extends BioSuite with ValidateResourceFixture with TestHelpers with IOFixedClock { private val uuid = UUID.randomUUID() implicit private val uuidF: UUIDF = UUIDF.fixed(uuid) @@ -58,8 +61,24 @@ class ResourcesPracticeSuite extends BioSuite with ValidateResourceFixture with private val source = NexusSource(jsonContentOf("resources/resource.json", "id" -> id)) private val resourceSchema = nxv + "schema" + private def assertSuccess( + io: UIO[ResourceGenerationResult] + )(schema: Option[SchemaResource], result: Resource)(implicit loc: Location) = + io.map { generated => + assertEquals(generated.schema, schema) + assertEquals(generated.attempt.map(_.value), Right(result)) + } + + private def assertError( + io: UIO[ResourceGenerationResult] + )(schema: Option[SchemaResource], error: ResourceRejection)(implicit loc: Location) = + io.map { generated => + assertEquals(generated.schema, schema) + assertEquals(generated.attempt.map(_.value), Left(error)) + } + test("Successfully generates a resource") { - val practice = ResourcesPractice( + val trial = ResourcesTrial( (_, _) => fetchResourceFail, alwaysValidate, fetchContext, @@ -68,12 +87,11 @@ class ResourcesPracticeSuite extends BioSuite with ValidateResourceFixture with val expectedData = ResourceGen.resource(id, projectRef, source.value, Revision(resourceSchema, defaultSchemaRevision)) - - practice.generate(projectRef, resourceSchema, source).map(_.value).assert(expectedData) + assertSuccess(trial.generate(projectRef, resourceSchema, source))(None, expectedData) } test("Successfully generates a resource with a new schema") { - val practice = ResourcesPractice( + val trial = ResourcesTrial( (_, _) => fetchResourceFail, alwaysValidate, fetchContext, @@ -88,18 +106,19 @@ class ResourcesPracticeSuite extends BioSuite with ValidateResourceFixture with val expectedData = ResourceGen.resource(id, projectRef, source.value, Revision(anotherSchema, defaultSchemaRevision)) - practice.generate(projectRef, schema, source).map(_.value).assert(expectedData) + assertSuccess(trial.generate(projectRef, schema, source))(Some(schema), expectedData) } test("Fail when validation raises an error") { val expectedError = ReservedResourceId(id) - val practice = ResourcesPractice( + val trial = ResourcesTrial( (_, _) => fetchResourceFail, alwaysFail(expectedError), fetchContext, resolverContextResolution ) - practice.generate(projectRef, resourceSchema, source).error(expectedError) + + assertError(trial.generate(projectRef, resourceSchema, source))(None, expectedError) } test("Validate a resource against a new schema reference") { @@ -108,7 +127,7 @@ class ResourcesPracticeSuite extends BioSuite with ValidateResourceFixture with ) val anotherSchema = nxv + "anotherSchema" - val practice = ResourcesPractice( + val trial = ResourcesTrial( (_, _) => IO.pure(resource), alwaysValidate, fetchContext, @@ -116,7 +135,7 @@ class ResourcesPracticeSuite extends BioSuite with ValidateResourceFixture with ) val expected = Validated(projectRef, Revision(anotherSchema, defaultSchemaRevision), defaultReport) - practice.validate(id, projectRef, Some(anotherSchema)).assert(expected) + trial.validate(id, projectRef, Some(anotherSchema)).assert(expected) } test("Validate a resource against its own schema") { @@ -124,7 +143,7 @@ class ResourcesPracticeSuite extends BioSuite with ValidateResourceFixture with ResourceGen.resource(id, projectRef, source.value, Revision(resourceSchema, 1)) ) - val practice = ResourcesPractice( + val trial = ResourcesTrial( (_, _) => IO.pure(resource), alwaysValidate, fetchContext, @@ -132,7 +151,7 @@ class ResourcesPracticeSuite extends BioSuite with ValidateResourceFixture with ) val expected = Validated(projectRef, Revision(resourceSchema, defaultSchemaRevision), defaultReport) - practice.validate(id, projectRef, None).assert(expected) + trial.validate(id, projectRef, None).assert(expected) } test("Fail to validate a resource against the specified schema") { @@ -144,14 +163,14 @@ class ResourcesPracticeSuite extends BioSuite with ValidateResourceFixture with val expectedError = InvalidResource(id, Revision(anotherSchema, defaultSchemaRevision), defaultReport, resource.value.expanded) - val practice = ResourcesPractice( + val trial = ResourcesTrial( (_, _) => IO.pure(resource), alwaysFail(expectedError), fetchContext, resolverContextResolution ) - practice.validate(id, projectRef, Some(anotherSchema)).error(expectedError) + trial.validate(id, projectRef, Some(anotherSchema)).error(expectedError) } } diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResourceFixture.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResourceFixture.scala index 0a906af805..f44b7439d2 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResourceFixture.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResourceFixture.scala @@ -10,11 +10,12 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resources.ValidationResult._ import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} import io.circe.Json +import io.circe.syntax.KeyOps import monix.bio.IO trait ValidateResourceFixture { - val defaultReport: ValidationReport = ValidationReport(conforms = true, 5, Json.obj()) + val defaultReport: ValidationReport = ValidationReport(conforms = true, 5, Json.obj("conforms" := "true")) val defaultSchemaRevision = 1 def alwaysValidate: ValidateResource = new ValidateResource { diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceSerializationSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceSerializationSuite.scala index ed1e4dd097..d01a6dbced 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceSerializationSuite.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceSerializationSuite.scala @@ -5,6 +5,7 @@ import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.schemas import ch.epfl.bluebrain.nexus.delta.sdk.SerializationSuite import ch.epfl.bluebrain.nexus.delta.sdk.model.Tags import ch.epfl.bluebrain.nexus.delta.sdk.model.metrics.EventMetric._ +import ch.epfl.bluebrain.nexus.delta.sdk.resources.ResourceInstanceFixture import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceEvent._ import ch.epfl.bluebrain.nexus.delta.sdk.sse.SseEncoder.SseData import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Subject, User} @@ -15,7 +16,7 @@ import io.circe.JsonObject import java.time.Instant -class ResourceSerializationSuite extends SerializationSuite { +class ResourceSerializationSuite extends SerializationSuite with ResourceInstanceFixture { private val sseEncoder = ResourceEvent.sseEncoder @@ -23,8 +24,6 @@ class ResourceSerializationSuite extends SerializationSuite { val realm: Label = Label.unsafe("myrealm") val subject: Subject = User("username", realm) - import ch.epfl.bluebrain.nexus.delta.sdk.resources.ResourceFixture._ - private val created = ResourceCreated( myId, diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/utils/RouteHelpers.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/utils/RouteHelpers.scala index 9743ce5511..417a8d63ce 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/utils/RouteHelpers.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/utils/RouteHelpers.scala @@ -10,7 +10,7 @@ import akka.testkit.TestDuration import akka.util.ByteString import ch.epfl.bluebrain.nexus.testkit.EitherValuable import io.circe.parser.parse -import io.circe.{Decoder, Json, Printer} +import io.circe.{Decoder, Json, JsonObject, Printer} import org.scalatest.concurrent.ScalaFutures import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike @@ -69,6 +69,13 @@ final class HttpResponseOps(private val http: HttpResponse) extends Consumer { def asJson(implicit materializer: Materializer): Json = asJson(http.entity.dataBytes) + def asJsonObject(implicit materializer: Materializer): JsonObject = { + val json = asJson(http.entity.dataBytes) + json.asObject.getOrElse( + fail(s"Error converting '$json' to a JsonObject.") + ) + } + def as[A: Decoder](implicit materializer: Materializer, A: ClassTag[A]): A = asJson.as[A] match { case Left(err) => fail(s"Error converting th json to '${A.runtimeClass.getName}'. Details: '${err.getMessage()}'") diff --git a/docs/src/main/paradox/docs/delta/api/assets/trial/resources/generate.sh b/docs/src/main/paradox/docs/delta/api/assets/trial/resources/generate.sh new file mode 100644 index 0000000000..13f05eeb63 --- /dev/null +++ b/docs/src/main/paradox/docs/delta/api/assets/trial/resources/generate.sh @@ -0,0 +1,19 @@ +curl -X PUT \ + -H "Content-Type: application/json" \ + "http://localhost:8080/trial/resources/myorg/myproj/" \ + -d \ +'{ + "schema": "https://bbp.epfl.ch/nexus/schema/morphology" + "resource": { + "@context": [ + "https://neuroshapes.org", + "https://bluebrain.github.io/nexus/contexts/metadata.json", + { + "@vocab": "https://bluebrain.github.io/nexus/vocabulary/" + } + ], + "@id": "https://bbp.epfl.ch/nexus/data/morphology-001", + "@type": "Morphology", + "name": "Morphology 001" + } + }' \ No newline at end of file diff --git a/docs/src/main/paradox/docs/delta/api/assets/trial/resources/generated.json b/docs/src/main/paradox/docs/delta/api/assets/trial/resources/generated.json new file mode 100644 index 0000000000..7526b8cbc1 --- /dev/null +++ b/docs/src/main/paradox/docs/delta/api/assets/trial/resources/generated.json @@ -0,0 +1,26 @@ +{ + "result": { + "@context": [ + "https://bluebrain.github.io/nexus/contexts/metadata.json", + { + "@vocab": "https://bluebrain.github.io/nexus/vocabulary/" + }, + "https://neuroshapes.org" + ], + "@id": "https://bbp.epfl.ch/nexus/data/morphology-001", + "@type": "Morphology", + "name": "Morphology 001", + "_constrainedBy": "https://bbp.epfl.ch/nexus/schema/morphology", + "_createdAt": "2023-09-18T12:00:00Z", + "_createdBy": "http://localhost/v1/realms/wonderland/users/alice", + "_deprecated": false, + "_incoming": "http://localhost/v1/resources/myorg/myproj/_/https:%2F%2Fbluebrain.github.io%2Fnexus%2Fvocabulary%2FmyId/incoming", + "_outgoing": "http://localhost/v1/resources/myorg/myproj/_/https:%2F%2Fbluebrain.github.io%2Fnexus%2Fvocabulary%2FmyId/outgoing", + "_project": "http://localhost/v1/projects/myorg/myproj", + "_rev": 1, + "_schemaProject": "http://localhost/v1/projects/myorg/myproj", + "_self": "http://localhost/v1/resources/myorg/myproj/_/https:%2F%2Fbluebrain.github.io%2Fnexus%2Fvocabulary%2FmyId", + "_updatedAt": "2023-09-18T12:00:00Z", + "_updatedBy": "http://localhost/v1/realms/wonderland/users/alice" + } +} \ No newline at end of file diff --git a/docs/src/main/paradox/docs/delta/api/assets/trial/resources/payload.json b/docs/src/main/paradox/docs/delta/api/assets/trial/resources/payload.json new file mode 100644 index 0000000000..1a65b84a64 --- /dev/null +++ b/docs/src/main/paradox/docs/delta/api/assets/trial/resources/payload.json @@ -0,0 +1,15 @@ +{ + "schema": "https://bbp.epfl.ch/nexus/schema/morphology" + "resource": { + "@context": [ + "https://neuroshapes.org", + "https://bluebrain.github.io/nexus/contexts/metadata.json", + { + "@vocab": "https://bluebrain.github.io/nexus/vocabulary/" + } + ], + "@id": "https://bbp.epfl.ch/nexus/data/morphology-001", + "@type": "Morphology", + "name": "Morphology 001" + } +} \ No newline at end of file diff --git a/docs/src/main/paradox/docs/delta/api/index.md b/docs/src/main/paradox/docs/delta/api/index.md index 6f988d6f1d..0a06bb2b29 100644 --- a/docs/src/main/paradox/docs/delta/api/index.md +++ b/docs/src/main/paradox/docs/delta/api/index.md @@ -13,6 +13,7 @@ * @ref:[Quotas](quotas.md) * @ref:[Schemas](schemas-api.md) * @ref:[Resources](resources-api.md) +* @ref:[Practice](trial.md) * @ref:[Multi-fetch](multi-fetch.md) * @ref:[Resolvers](resolvers-api.md) * @ref:[Views](views/index.md) diff --git a/docs/src/main/paradox/docs/delta/api/resources-api.md b/docs/src/main/paradox/docs/delta/api/resources-api.md index c15e325974..c9006b03da 100644 --- a/docs/src/main/paradox/docs/delta/api/resources-api.md +++ b/docs/src/main/paradox/docs/delta/api/resources-api.md @@ -161,24 +161,6 @@ Request Response : @@snip [refreshed.json](assets/resources/updated.json) - -## Validate - -This operation runs validation of a resource against a schema. This would be useful to test whether resources would -match the shape of a new schema. - -``` -GET /v1/resources/{org_label}/{project_label}/{schema_id}/{resource_id}/validate -``` - -**Example** - -Request -: @@snip [validate.sh](assets/resources/validate.sh) - -Response -: @@snip [validated.json](assets/resources/validated.json) - ## Tag Links a resource revision to a specific name. diff --git a/docs/src/main/paradox/docs/delta/api/trial.md b/docs/src/main/paradox/docs/delta/api/trial.md new file mode 100644 index 0000000000..80131023a5 --- /dev/null +++ b/docs/src/main/paradox/docs/delta/api/trial.md @@ -0,0 +1,69 @@ +# Trial +Trial operations contain read-only operations designed to help users compose and validate their +resources before effectively saving them in Nexus. + +@@@ note { .tip title="Authorization notes" } + +When performing a request, the caller must have `resources/read` permission on the project each resource belongs to. + +Please visit @ref:[Authentication & authorization](authentication.md) section to learn more about it. + +@@@ + +## Resource generation + +This endpoint allows to create and get the output of a resource, optionally validating with an +existing schema or a new one. + +It applies the same validation steps than the creation/update of resources, the main difference being +that nothing is persisted. + +``` +GET /v1/trial/resources/{org_label}/{project_label} + +{ + "schema": {schema}, + "resource": {resource} +} +``` + +Where: + +* `{schema}`: String/Json: The schema to validate the provided resource. If a string is provided, it will attempt to resolve it as an existing schema. +If a json payload is provided, it will attempt to generate the schema and then use the result to validate the resource. +This field is optional and defaults to no SHACL validation. +* `{resource}`: Json: The resource payload to test and validate + +The Json response will contain: + +* The generated resource in the compacted JSON-LD format if the generation and the validation was successful +* The generated schema if a new schema payload was provided +* The error if the one of the steps fails (invalid resource/invalid new schema/existing schema not found/...) + +**Example** + +Request +: @@snip [create.sh](assets/trial/resources/generate.sh) + +Payload +: @@snip [payload.json](assets/trial/resources/payload.json) + +Response +: @@snip [created.json](assets/trial/resources/generated.json) + +## Validate + +This operation runs validation of a resource against a schema. This would be useful to test whether resources would +match the shape of a new schema. + +``` +GET /v1/resources/{org_label}/{project_label}/{schema_id}/{resource_id}/validate +``` + +**Example** + +Request +: @@snip [validate.sh](assets/resources/validate.sh) + +Response +: @@snip [validated.json](assets/resources/validated.json) \ No newline at end of file diff --git a/docs/src/main/paradox/docs/releases/index.md b/docs/src/main/paradox/docs/releases/index.md index 7d672fcd30..914118ff47 100644 --- a/docs/src/main/paradox/docs/releases/index.md +++ b/docs/src/main/paradox/docs/releases/index.md @@ -55,7 +55,7 @@ In the upcoming version, the support of the tar format to download archives will - Only trigger reindexing when indexing is impacted - Project deletion has been rewritten - @ref:[A refresh operation is now available for resources](../delta/api/resources-api.md#refresh) -- @ref:[A validate operation is now available for resources](../delta/api/resources-api.md#validate) +- @ref:[A validate operation is now available for resources](../delta/api/trial.md#validate) - Archives can now be downloaded as a zip A detailed list of changes included in the release can be found in the @ref:[release notes](v1.8-release-notes.md). diff --git a/docs/src/main/paradox/docs/releases/v1.8-release-notes.md b/docs/src/main/paradox/docs/releases/v1.8-release-notes.md index 0785133b8d..1141171f14 100644 --- a/docs/src/main/paradox/docs/releases/v1.8-release-notes.md +++ b/docs/src/main/paradox/docs/releases/v1.8-release-notes.md @@ -134,7 +134,7 @@ upstream dependencies has changed without having to provide again its original p This new validate operation allows to check a resource against a provided schema. This operation is read-only and allows to test a schema against different resources -@ref:[More information](../delta/api/resources-api.md#validate) +@ref:[More information](../delta/api/trial.md#validate) #### Annotated original payload diff --git a/docs/src/main/paradox/docs/releases/v1.9-release-notes.md b/docs/src/main/paradox/docs/releases/v1.9-release-notes.md index d5d71099e5..8c0bbfbdbe 100644 --- a/docs/src/main/paradox/docs/releases/v1.9-release-notes.md +++ b/docs/src/main/paradox/docs/releases/v1.9-release-notes.md @@ -30,6 +30,14 @@ Multiple resources can now be retrieved within a single call with the multi-fetc @ref:[More information](../delta/api/multi-fetch.md) +#### Resources trial and resource generation + +Until 1.9, operations about resources were resulting in writes in the primary store and indexing in the knowledge graph. +However, users may also want to first experiment/refine/polish/validate a resource and schema and only create/update the resource +when they are happy with it. + +@ref:[More information](../delta/api/trial.md#resource-generation) + #### Payload validation It is now forbidden for JSON payloads to contain fields beginning with underscore (_). This can be disabled be setting `app.resources.decoding-option` to `lenient`, however it is not recommended as specification of this data in payloads can have unexpected consequences in both data and the user-interface diff --git a/tests/src/test/resources/kg/resources/simple-resource-response.json b/tests/src/test/resources/kg/resources/simple-resource-response.json index 28b59847c0..6c79ddc744 100644 --- a/tests/src/test/resources/kg/resources/simple-resource-response.json +++ b/tests/src/test/resources/kg/resources/simple-resource-response.json @@ -6,7 +6,7 @@ }, "https://bluebrain.github.io/nexus/contexts/metadata.json" ], - "@id": "https://dev.nexus.test.com/simplified-resource/{{resourceId}}", + "@id": "{{resourceId}}", "@type": "nxv:TestResource", "other:projects": [ "testProject2", diff --git a/tests/src/test/resources/kg/resources/simple-resource-with-id.json b/tests/src/test/resources/kg/resources/simple-resource-with-id.json deleted file mode 100644 index 1197fdf118..0000000000 --- a/tests/src/test/resources/kg/resources/simple-resource-with-id.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "@context": { - "nxv": "https://bluebrain.github.io/nexus/vocabulary/", - "other": "https://some.other.prefix.com/" - }, - {{#resourceId}} - "@id": "{{resourceId}}", - {{/resourceId}} - - "@type": "nxv:TestResource", - "other:priority": {{priority}}, - "other:projects": [ - "testProject", - "testProject2" - ], - "nullValue": null -} \ No newline at end of file diff --git a/tests/src/test/resources/kg/resources/simple-resource-with-metadata.json b/tests/src/test/resources/kg/resources/simple-resource-with-metadata.json index bb7ebdee01..9c9ddc4c89 100644 --- a/tests/src/test/resources/kg/resources/simple-resource-with-metadata.json +++ b/tests/src/test/resources/kg/resources/simple-resource-with-metadata.json @@ -1,6 +1,6 @@ { "@context": "https://bluebrain.github.io/nexus/contexts/metadata.json", - "@id": "https://dev.nexus.test.com/simplified-resource/{{resourceId}}", + "@id": "{{resourceId}}", "@type": "https://bluebrain.github.io/nexus/vocabulary/TestResource", "_constrainedBy": "https://dev.nexus.test.com/test-schema", "_createdBy": "{{deltaUri}}/realms/{{realm}}/users/{{user}}", diff --git a/tests/src/test/resources/kg/resources/simple-resource.json b/tests/src/test/resources/kg/resources/simple-resource.json index 8816b20be1..5bc63af85a 100644 --- a/tests/src/test/resources/kg/resources/simple-resource.json +++ b/tests/src/test/resources/kg/resources/simple-resource.json @@ -4,7 +4,7 @@ "other": "https://some.other.prefix.com/" }, {{#resourceId}} - "@id": "https://dev.nexus.test.com/simplified-resource/{{resourceId}}", + "@id": "{{resourceId}}", {{/resourceId}} {{#if resourceType}} "@type": "{{resourceType}}", diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/BaseSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/BaseSpec.scala index cb459f091b..8cbd1e1de2 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/BaseSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/BaseSpec.scala @@ -15,6 +15,7 @@ import ch.epfl.bluebrain.nexus.tests.admin.AdminDsl import ch.epfl.bluebrain.nexus.tests.config.ConfigLoader._ import ch.epfl.bluebrain.nexus.tests.config.TestsConfig import ch.epfl.bluebrain.nexus.tests.iam.types.Permission +import ch.epfl.bluebrain.nexus.tests.iam.types.Permission.Organizations import ch.epfl.bluebrain.nexus.tests.iam.{AclDsl, PermissionDsl} import ch.epfl.bluebrain.nexus.tests.kg.{ElasticSearchViewsDsl, KgDsl} import com.typesafe.config.ConfigFactory @@ -36,6 +37,8 @@ trait BaseSpec with CirceLiteral with CirceEq with BeforeAndAfterAll + with HandleBarsFixture + with SelfFixture with TestHelpers with ScalatestRouteTest with Eventually @@ -196,6 +199,19 @@ trait BaseSpec } yield () } + /** + * Create projects and the parent organization for the provided user + */ + def createProjects(user: Authenticated, org: String, projects: String*): Task[Unit] = + for { + _ <- aclDsl.addPermission("/", user, Organizations.Create) + _ <- adminDsl.createOrganization(org, org, user, ignoreConflict = true) + _ <- projects.traverse { project => + val projectRef = s"$org/$project" + adminDsl.createProject(org, project, kgDsl.projectJson(name = projectRef), user) + } + } yield () + private[tests] def dispositionType(response: HttpResponse): ContentDispositionType = response.header[`Content-Disposition`].value.dispositionType @@ -216,13 +232,6 @@ trait BaseSpec private[tests] def decodeGzip(input: ByteString): String = Coders.Gzip.decode(input).map(_.utf8String)(global).futureValue - private[tests] def replacements(authenticated: Authenticated, otherReplacements: (String, String)*) = - Seq( - "deltaUri" -> config.deltaUri.toString(), - "realm" -> authenticated.realm.name, - "user" -> authenticated.name - ) ++ otherReplacements - private[tests] def genId(length: Int = 15): String = genString(length = length, Vector.range('a', 'z') ++ Vector.range('0', '9')) @@ -236,26 +245,6 @@ trait BaseSpec private[tests] def expectOk[A] = expect(StatusCodes.OK) private[tests] def tag(name: String, rev: Int) = json"""{"tag": "$name", "rev": $rev}""" - - private[tests] def resourceSelf(project: String, id: String): String = { - val uri = Uri(s"${config.deltaUri}/resources/$project/_") - uri.copy(path = uri.path / id).toString - } - - private[tests] def resolverSelf(project: String, id: String): String = { - val uri = Uri(s"${config.deltaUri}/resolvers/$project") - uri.copy(path = uri.path / id).toString - } - - private[tests] def viewSelf(project: String, id: String): String = { - val uri = Uri(s"${config.deltaUri}/views/$project") - uri.copy(path = uri.path / id).toString - } - - private[tests] def storageSelf(project: String, id: String): String = { - val uri = Uri(s"${config.deltaUri}/storages/$project") - uri.copy(path = uri.path / id).toString - } } object BaseSpec { diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/HandleBarsFixture.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/HandleBarsFixture.scala new file mode 100644 index 0000000000..5fb1cb208a --- /dev/null +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/HandleBarsFixture.scala @@ -0,0 +1,20 @@ +package ch.epfl.bluebrain.nexus.tests + +import ch.epfl.bluebrain.nexus.tests.Identity.Authenticated +import ch.epfl.bluebrain.nexus.tests.config.TestsConfig + +/** + * Utility methods for the handlebars templating + */ +trait HandleBarsFixture { + + def replacements(authenticated: Authenticated, otherReplacements: (String, String)*)(implicit + config: TestsConfig + ): Seq[(String, String)] = + Seq( + "deltaUri" -> config.deltaUri.toString(), + "realm" -> authenticated.realm.name, + "user" -> authenticated.name + ) ++ otherReplacements + +} diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/SelfFixture.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/SelfFixture.scala new file mode 100644 index 0000000000..a0d7903e21 --- /dev/null +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/SelfFixture.scala @@ -0,0 +1,31 @@ +package ch.epfl.bluebrain.nexus.tests + +import akka.http.scaladsl.model.Uri +import ch.epfl.bluebrain.nexus.tests.config.TestsConfig + +/** + * Utility methods to generate the self for the different entities + */ +trait SelfFixture { + + def resourceSelf(project: String, id: String)(implicit config: TestsConfig): String = { + val uri = Uri(s"${config.deltaUri}/resources/$project/_") + uri.copy(path = uri.path / id).toString + } + + def resolverSelf(project: String, id: String)(implicit config: TestsConfig): String = { + val uri = Uri(s"${config.deltaUri}/resolvers/$project") + uri.copy(path = uri.path / id).toString + } + + def viewSelf(project: String, id: String)(implicit config: TestsConfig): String = { + val uri = Uri(s"${config.deltaUri}/views/$project") + uri.copy(path = uri.path / id).toString + } + + def storageSelf(project: String, id: String)(implicit config: TestsConfig): String = { + val uri = Uri(s"${config.deltaUri}/storages/$project") + uri.copy(path = uri.path / id).toString + } + +} diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/AggregationsSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/AggregationsSpec.scala index e5fa098915..da6942532c 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/AggregationsSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/AggregationsSpec.scala @@ -6,6 +6,7 @@ import ch.epfl.bluebrain.nexus.tests.{BaseSpec, SchemaPayload} import ch.epfl.bluebrain.nexus.tests.Identity.Anonymous import ch.epfl.bluebrain.nexus.tests.Identity.aggregations.{Charlie, Rose} import ch.epfl.bluebrain.nexus.tests.iam.types.Permission.{Organizations, Resources, Views} +import ch.epfl.bluebrain.nexus.tests.resources.SimpleResource import io.circe.Json import org.scalatest.Inspectors @@ -37,11 +38,7 @@ final class AggregationsSpec extends BaseSpec with Inspectors with EitherValuabl _ <- aclDsl.addPermission(s"/$ref12", Rose, Views.Query) } yield () - val resourcePayload = - jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "5" - ) + val resourcePayload = SimpleResource.sourcePayload(5) val schemaPayload = SchemaPayload.loadSimple() val postResources = for { // Creation diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ArchiveSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ArchiveSpec.scala index 90a41cec9a..c22fbefa2e 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ArchiveSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ArchiveSpec.scala @@ -9,9 +9,9 @@ import ch.epfl.bluebrain.nexus.testkit.CirceEq import ch.epfl.bluebrain.nexus.testkit.archive.ArchiveHelpers import ch.epfl.bluebrain.nexus.tests.HttpClient._ import ch.epfl.bluebrain.nexus.tests.Identity.archives.Tweety -import ch.epfl.bluebrain.nexus.tests.Identity.testRealm import ch.epfl.bluebrain.nexus.tests.Optics._ import ch.epfl.bluebrain.nexus.tests.iam.types.Permission.{Projects, Resources} +import ch.epfl.bluebrain.nexus.tests.resources.SimpleResource import ch.epfl.bluebrain.nexus.tests.{BaseSpec, Identity, SchemaPayload} import io.circe.Json @@ -27,41 +27,15 @@ class ArchiveSpec extends BaseSpec with ArchiveHelpers with CirceEq { private val schemaPayload = SchemaPayload.loadSimple() - private val payload1 = jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "5", - "resourceId" -> "1" - ) - - private val payload2 = jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "6", - "resourceId" -> "2" - ) - - private val payloadResponse1 = jsonContentOf( - "/kg/resources/simple-resource-response.json", - "deltaUri" -> config.deltaUri, - "realm" -> testRealm.name, - "user" -> Tweety.name, - "priority" -> "5", - "rev" -> "1", - "self" -> resourceSelf(fullId, "https://dev.nexus.test.com/simplified-resource/1"), - "project" -> s"${config.deltaUri}/projects/$fullId", - "resourceId" -> "1" - ) - - private val payloadResponse2 = jsonContentOf( - "/kg/resources/simple-resource-response.json", - "deltaUri" -> config.deltaUri, - "realm" -> testRealm.name, - "user" -> Tweety.name, - "priority" -> "6", - "rev" -> "1", - "self" -> resourceSelf(fullId2, "https://dev.nexus.test.com/simplified-resource/2"), - "project" -> s"${config.deltaUri}/projects/$fullId2", - "resourceId" -> "2" - ) + private val resource1Id = "https://dev.nexus.test.com/simplified-resource/1" + private val payload1 = SimpleResource.sourcePayload(resource1Id, 5) + + private val resource2Id = "https://dev.nexus.test.com/simplified-resource/2" + private val payload2 = SimpleResource.sourcePayload(resource2Id, 6) + + private val payloadResponse1 = SimpleResource.fetchResponse(Tweety, fullId, resource1Id, 1, 5) + + private val payloadResponse2 = SimpleResource.fetchResponse(Tweety, fullId2, resource2Id, 1, 6) private val nexusLogoDigest = "edd70eff895cde1e36eaedd22ed8e9c870bb04155d05d275f970f4f255488e993a32a7c914ee195f6893d43b8be4e0b00db0a6d545a8462491eae788f664ea6b" diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/EventsSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/EventsSpec.scala index 61f48d95b1..7f57b90fc1 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/EventsSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/EventsSpec.scala @@ -5,6 +5,7 @@ import ch.epfl.bluebrain.nexus.tests.BaseSpec import ch.epfl.bluebrain.nexus.tests.Identity.events.BugsBunny import ch.epfl.bluebrain.nexus.tests.Optics._ import ch.epfl.bluebrain.nexus.tests.iam.types.Permission.{Events, Organizations, Resources} +import ch.epfl.bluebrain.nexus.tests.resources.SimpleResource import io.circe.Json import monix.bio.Task import monix.execution.Scheduler.Implicits.global @@ -89,11 +90,8 @@ class EventsSpec extends BaseSpec with Inspectors { "add events to project" in { //Created event - val payload = jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "3", - "resourceId" -> "1" - ) + val resourceId = "https://dev.nexus.test.com/simplified-resource/1" + val payload = SimpleResource.sourcePayload(resourceId, 3) for { //ResourceCreated event @@ -106,11 +104,7 @@ class EventsSpec extends BaseSpec with Inspectors { //ResourceUpdated event _ <- deltaClient.put[Json]( s"/resources/$id/_/test-resource:1?rev=1", - jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "5", - "resourceId" -> "1" - ), + SimpleResource.sourcePayload(resourceId, 5), BugsBunny ) { (_, response) => response.status shouldEqual StatusCodes.OK diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ListingsSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ListingsSpec.scala index 91642a716c..18d2959a31 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ListingsSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ListingsSpec.scala @@ -8,6 +8,7 @@ import ch.epfl.bluebrain.nexus.tests.Identity.listings.{Alice, Bob} import ch.epfl.bluebrain.nexus.tests.Identity.{Anonymous, Delta} import ch.epfl.bluebrain.nexus.tests.Optics._ import ch.epfl.bluebrain.nexus.tests.iam.types.Permission.{Organizations, Resources, Views} +import ch.epfl.bluebrain.nexus.tests.resources.SimpleResource import io.circe.Json import org.scalatest.Inspectors @@ -45,12 +46,7 @@ final class ListingsSpec extends BaseSpec with Inspectors with EitherValuable wi } "add additional resources" in { - val resourcePayload = - jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "5", - "resourceType" -> resourceType - ) + val resourcePayload = SimpleResource.sourcePayloadWithType(resourceType, 5) val schemaPayload = SchemaPayload.loadSimple(resourceType) for { // Creation diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/MultiFetchSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/MultiFetchSpec.scala index 3756e3796e..4a1bc8fc82 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/MultiFetchSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/MultiFetchSpec.scala @@ -3,9 +3,10 @@ package ch.epfl.bluebrain.nexus.tests.kg import akka.http.scaladsl.model.{ContentTypes, StatusCodes} import ch.epfl.bluebrain.nexus.tests.BaseSpec import ch.epfl.bluebrain.nexus.tests.Identity.listings.{Alice, Bob} -import ch.epfl.bluebrain.nexus.tests.iam.types.Permission.{Organizations, Resources} -import io.circe.Json import ch.epfl.bluebrain.nexus.tests.Optics._ +import ch.epfl.bluebrain.nexus.tests.iam.types.Permission.Resources +import ch.epfl.bluebrain.nexus.tests.resources.SimpleResource +import io.circe.Json class MultiFetchSpec extends BaseSpec { @@ -21,18 +22,11 @@ class MultiFetchSpec extends BaseSpec { super.beforeAll() val setup = for { - _ <- aclDsl.addPermission("/", Bob, Organizations.Create) - _ <- adminDsl.createOrganization(org1, org1, Bob) - _ <- adminDsl.createProject(org1, proj11, kgDsl.projectJson(name = proj11), Bob) - _ <- adminDsl.createProject(org1, proj12, kgDsl.projectJson(name = proj12), Bob) + _ <- createProjects(Bob, org1, proj11, proj12) _ <- aclDsl.addPermission(s"/$ref12", Alice, Resources.Read) } yield () - val resourcePayload = - jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "5" - ) + val resourcePayload = SimpleResource.sourcePayload(5) val createResources = for { // Creation diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ProjectsDeletionSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ProjectsDeletionSpec.scala index 0695f503cc..bbb7951696 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ProjectsDeletionSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ProjectsDeletionSpec.scala @@ -6,6 +6,7 @@ import ch.epfl.bluebrain.nexus.tests.Identity.projects.{Bojack, PrincessCarolyn} import ch.epfl.bluebrain.nexus.tests.Identity.{Anonymous, ServiceAccount} import ch.epfl.bluebrain.nexus.tests.Optics.{admin, listing, supervision} import ch.epfl.bluebrain.nexus.tests.iam.types.Permission.{Events, Organizations, Projects, Resources} +import ch.epfl.bluebrain.nexus.tests.resources.SimpleResource import ch.epfl.bluebrain.nexus.tests.{BaseSpec, Identity, SchemaPayload} import io.circe.Json import io.circe.optics.JsonPath.root @@ -95,11 +96,7 @@ final class ProjectsDeletionSpec extends BaseSpec with CirceEq with EitherValuab } "add additional resources" in { - val resourcePayload = - jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "5" - ) + val resourcePayload = SimpleResource.sourcePayload(5) val schemaPayload = SchemaPayload.loadSimple() val resolverPayload = jsonContentOf( diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala index e4f8c3b5a2..42e05c344c 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala @@ -10,7 +10,7 @@ import ch.epfl.bluebrain.nexus.testkit.{CirceEq, EitherValuable} import ch.epfl.bluebrain.nexus.tests.Identity.resources.{Morty, Rick} import ch.epfl.bluebrain.nexus.tests.Optics.listing._total import ch.epfl.bluebrain.nexus.tests.Optics.{filterKey, filterMetadataKeys} -import ch.epfl.bluebrain.nexus.tests.iam.types.Permission.Organizations +import ch.epfl.bluebrain.nexus.tests.resources.SimpleResource import ch.epfl.bluebrain.nexus.tests.{BaseSpec, Optics, SchemaPayload} import io.circe.Json import io.circe.optics.JsonPath.root @@ -33,33 +33,12 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { private val IdLens: Optional[Json, String] = root.`@id`.string private val TypeLens: Optional[Json, String] = root.`@type`.string - private val resource1Self = resourceSelf(id1, "https://dev.nexus.test.com/simplified-resource/1") - + private val resource1Id = "https://dev.nexus.test.com/simplified-resource/1" private def resource1Response(rev: Int, priority: Int) = - jsonContentOf( - "/kg/resources/simple-resource-response.json", - replacements( - Rick, - "priority" -> priority.toString, - "rev" -> rev.toString, - "self" -> resource1Self, - "project" -> s"${config.deltaUri}/projects/$id1", - "resourceId" -> "1" - ): _* - ) + SimpleResource.fetchResponse(Rick, id1, resource1Id, rev, priority) private def resource1AnnotatedSource(rev: Int, priority: Int) = - jsonContentOf( - "/kg/resources/simple-resource-with-metadata.json", - replacements( - Rick, - "priority" -> priority.toString, - "rev" -> rev.toString, - "self" -> resource1Self, - "project" -> s"${config.deltaUri}/projects/$id1", - "resourceId" -> "1" - ): _* - ) + SimpleResource.annotatedResource(Rick, id1, resource1Id, rev, priority) private def `@id`(expectedId: String) = HavePropertyMatcher[Json, String] { json => val actualId = IdLens.getOption(json) @@ -81,23 +60,9 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { ) } - "creating projects" should { - - "add necessary permissions for user" in { - aclDsl.addPermission( - "/", - Rick, - Organizations.Create - ) - } - - "succeed if payload is correct" in { - for { - _ <- adminDsl.createOrganization(orgId, orgId, Rick) - _ <- adminDsl.createProject(orgId, projId1, kgDsl.projectJson(name = id1), Rick) - _ <- adminDsl.createProject(orgId, projId2, kgDsl.projectJson(name = id2), Rick) - } yield succeed - } + override def beforeAll(): Unit = { + super.beforeAll() + createProjects(Rick, orgId, projId1, projId2).accepted } "adding schema" should { @@ -142,19 +107,10 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { } "succeed if the payload is correct" in { - val payload = - jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "5", - "resourceId" -> "1" - ) + val payload = SimpleResource.sourcePayload(resource1Id, 5) val id2 = URLEncoder.encode("https://dev.nexus.test.com/test-schema-imports", "UTF-8") - val payload2 = jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "5", - "resourceId" -> "a" - ) + val payload2 = SimpleResource.sourcePayload("https://dev.nexus.test.com/simplified-resource/a", 5) for { _ <- deltaClient.put[Json](s"/resources/$id1/test-schema/test-resource:1", payload, Rick) { (_, response) => @@ -176,12 +132,7 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { "fetch the original payload" in { deltaClient.get[Json](s"/resources/$id1/test-schema/test-resource:1/source", Rick) { (json, response) => - val expected = - jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "5", - "resourceId" -> "1" - ) + val expected = SimpleResource.sourcePayload(resource1Id, 5) response.status shouldEqual StatusCodes.OK json should equalIgnoreArrayOrder(expected) } @@ -197,12 +148,7 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { } "fetch the original payload with unexpanded id with metadata" in { - val payload = - jsonContentOf( - "/kg/resources/simple-resource-with-id.json", - "priority" -> "5", - "resourceId" -> "42" - ) + val payload = SimpleResource.sourcePayload("42", 5) for { _ <- deltaClient.post[Json](s"/resources/$id1/_/", payload, Rick) { (_, response) => @@ -216,10 +162,7 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { } "fetch the original payload with generated id with metadata" in { - val payload = jsonContentOf( - "/kg/resources/simple-resource-with-id.json", - "priority" -> "5" - ) + val payload = SimpleResource.sourcePayload(5) var generatedId: String = "" @@ -252,11 +195,7 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { } "fail if the schema doesn't exist in the project" in { - val payload = jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "3", - "resourceId" -> "1" - ) + val payload = SimpleResource.sourcePayload(resource1Id, 3) deltaClient.put[Json](s"/resources/$id2/test-schema/test-resource:1", payload, Rick) { (_, response) => response.status shouldEqual StatusCodes.NotFound @@ -264,11 +203,9 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { } "fail if the payload contains nexus metadata fields (underscore fields)" in { - val payload = jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "3", - "resourceId" -> "1" - ).deepMerge(json"""{"_self": "http://delta/resources/path"}""") + val payload = SimpleResource + .sourcePayload("1", 3) + .deepMerge(json"""{"_self": "http://delta/resources/path"}""") deltaClient.put[Json](s"/resources/$id2/_/test-resource:1", payload, Rick) { (_, response) => response.status shouldEqual StatusCodes.BadRequest @@ -365,31 +302,15 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { } "resolve schema from the other project" in { - val payload = jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "3", - "resourceId" -> "1" - ) - - eventually { - deltaClient.put[Json](s"/resources/$id2/test-schema/test-resource:1", payload, Rick) { (_, response) => - response.status shouldEqual StatusCodes.Created - } - } + val payload = SimpleResource.sourcePayload(resource1Id, 3) + deltaClient.put[Json](s"/resources/$id2/test-schema/test-resource:1", payload, Rick) { expectCreated } } } "updating a resource" should { "send the update" in { - val payload = jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "3", - "resourceId" -> "1" - ) - - deltaClient.put[Json](s"/resources/$id1/test-schema/test-resource:1?rev=1", payload, Rick) { (_, response) => - response.status shouldEqual StatusCodes.OK - } + val payload = SimpleResource.sourcePayload(resource1Id, 3) + deltaClient.put[Json](s"/resources/$id1/test-schema/test-resource:1?rev=1", payload, Rick) { expectOk } } "fetch the update" in { @@ -499,16 +420,11 @@ class ResourcesSpec extends BaseSpec with EitherValuable with CirceEq { "check consistency of responses" in { (2 to 100).toList.traverse { resourceId => - val payload = jsonContentOf( - "/kg/resources/simple-resource.json", - "priority" -> "3", - "resourceId" -> s"$resourceId" - ) + val payload = SimpleResource.sourcePayload(s"https://dev.nexus.test.com/simplified-resource/$resourceId", 3) for { _ <- deltaClient .put[Json](s"/resources/$id1/test-schema/test-resource:$resourceId?indexing=sync", payload, Rick) { - (_, response) => - response.status shouldEqual StatusCodes.Created + expectCreated } _ <- deltaClient.get[Json](s"/resources/$id1/test-schema", Rick) { (json, response) => response.status shouldEqual StatusCodes.OK diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/resources/ResourcesTrialSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/resources/ResourcesTrialSpec.scala new file mode 100644 index 0000000000..363eb2a9af --- /dev/null +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/resources/ResourcesTrialSpec.scala @@ -0,0 +1,112 @@ +package ch.epfl.bluebrain.nexus.tests.resources + +import akka.http.scaladsl.model.StatusCodes +import ch.epfl.bluebrain.nexus.testkit.CirceEq +import ch.epfl.bluebrain.nexus.tests.Identity.listings.{Alice, Bob} +import ch.epfl.bluebrain.nexus.tests.{BaseSpec, SchemaPayload} +import io.circe.Json +import io.circe.optics.JsonPath.root + +class ResourcesTrialSpec extends BaseSpec with CirceEq { + + private val org = genId() + private val proj = genId() + private val ref = s"$org/$proj" + + private val schemaId = "test-schema" + private val schemaPayload = SchemaPayload.loadSimple() + + private val resourceId = s"http://delta:8080/v1/resources/$ref/_/my-resource" + private val resourcePayload = SimpleResource.sourcePayload(resourceId, 5) + + override def beforeAll(): Unit = { + super.beforeAll() + val setup = for { + // Create a project + _ <- createProjects(Bob, org, proj) + // Create a schema + _ <- deltaClient.put[Json](s"/schemas/$ref/$schemaId", schemaPayload, Bob) { expectCreated } + } yield () + setup.accepted + } + + "Generating resources" should { + + val payloadWithoutSchema = json"""{ "resource": $resourcePayload }""" + val payloadWithExistingSchema = json"""{ "schema": "$schemaId" ,"resource": $resourcePayload }""" + val newSchemaId = "https://localhost/schema/new-schema" + val newSchemaPayload = root.`@id`.string.modify(_ => newSchemaId)(schemaPayload) + val payloadWithNewSchema = json"""{ "schema": $newSchemaPayload ,"resource": $resourcePayload }""" + + def errorType = root.error.`@type`.string.getOption(_) + def resultId = root.result.`@id`.string.getOption(_) + def schema = root.schema.`@id`.string.getOption(_) + + "fail for a user without access" in { + deltaClient.getWithBody[Json](s"/trial/resources/$ref/", payloadWithoutSchema, Alice)(expectForbidden) + } + + "fail for an unknown project" in { + deltaClient.getWithBody[Json](s"/trial/resources/$org/xxx/", payloadWithoutSchema, Alice)(expectForbidden) + } + + "succeed for a payload without schema" in { + deltaClient.getWithBody[Json](s"/trial/resources/$ref/", payloadWithoutSchema, Bob) { (json, response) => + response.status shouldEqual StatusCodes.OK + resultId(json).value shouldEqual resourceId + schema(json) shouldBe empty + errorType(json) shouldBe empty + } + } + + "succeed for a payload with an existing schema" in { + deltaClient.getWithBody[Json](s"/trial/resources/$ref/", payloadWithExistingSchema, Bob) { (json, response) => + response.status shouldEqual StatusCodes.OK + resultId(json).value shouldEqual resourceId + schema(json) shouldBe empty + errorType(json) shouldBe empty + } + } + + "succeed for a payload with a new schema" in { + deltaClient.getWithBody[Json](s"/trial/resources/$ref/", payloadWithNewSchema, Bob) { (json, response) => + response.status shouldEqual StatusCodes.OK + resultId(json).value shouldEqual resourceId + schema(json).value shouldBe newSchemaId + errorType(json) shouldBe empty + } + } + + "fail for a resource with an invalid context without generating any schema" in { + val payload = json"""{ "resource": { "@context": [ "https://bbp.epfl.ch/unknown-context" ], "test": "fail" } }""" + deltaClient.getWithBody[Json](s"/trial/resources/$ref/", payload, Bob) { (json, response) => + response.status shouldEqual StatusCodes.OK + resultId(json) shouldBe empty + schema(json) shouldBe empty + errorType(json).value shouldEqual "InvalidJsonLdFormat" + } + } + + "fail for a resource with an invalid context but also returning the generated schema" in { + val resourcePayload = json"""{ "@context": [ "https://bbp.epfl.ch/unknown-context" ], "test": "fail" }""" + val payload = json"""{ "schema": $newSchemaPayload, "resource": $resourcePayload }""" + deltaClient.getWithBody[Json](s"/trial/resources/$ref/", payload, Bob) { (json, response) => + response.status shouldEqual StatusCodes.OK + resultId(json) shouldBe empty + schema(json).value shouldBe newSchemaId + errorType(json).value shouldEqual "InvalidJsonLdFormat" + } + } + + "fail for a resource when shacl validation fails returning the generated " in { + val resourcePayload = SimpleResource.sourcePayloadWithType("nxv:UnexpectedType", 99) + val payload = json"""{ "schema": $newSchemaPayload ,"resource": $resourcePayload }""" + deltaClient.getWithBody[Json](s"/trial/resources/$ref/", payload, Bob) { (json, response) => + response.status shouldEqual StatusCodes.OK + resultId(json) shouldBe empty + schema(json).value shouldBe newSchemaId + errorType(json).value shouldEqual "InvalidResource" + } + } + } +} diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/resources/SimpleResource.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/resources/SimpleResource.scala new file mode 100644 index 0000000000..99cec7b9dc --- /dev/null +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/resources/SimpleResource.scala @@ -0,0 +1,65 @@ +package ch.epfl.bluebrain.nexus.tests.resources + +import ch.epfl.bluebrain.nexus.testkit.TestHelpers.jsonContentOf +import ch.epfl.bluebrain.nexus.tests.Identity.Authenticated +import ch.epfl.bluebrain.nexus.tests.config.TestsConfig +import ch.epfl.bluebrain.nexus.tests.{HandleBarsFixture, SelfFixture} +import io.circe.Json + +/** + * Utility methods to load the original payload, the fetch response for the `simple resource` used within the + * integration tests + */ +object SimpleResource extends HandleBarsFixture with SelfFixture { + + def fetchResponse(user: Authenticated, project: String, resourceId: String, rev: Int, priority: Int)(implicit + config: TestsConfig + ): Json = + jsonContentOf( + "/kg/resources/simple-resource-response.json", + replacements( + user, + "priority" -> priority.toString, + "rev" -> rev.toString, + "self" -> resourceSelf(project, resourceId), + "project" -> s"${config.deltaUri}/projects/$project", + "resourceId" -> resourceId + ): _* + ) + + def annotatedResource(user: Authenticated, project: String, resourceId: String, rev: Int, priority: Int)(implicit + config: TestsConfig + ): Json = + jsonContentOf( + "/kg/resources/simple-resource-with-metadata.json", + replacements( + user, + "priority" -> priority.toString, + "rev" -> rev.toString, + "self" -> resourceSelf(project, resourceId), + "project" -> s"${config.deltaUri}/projects/$project", + "resourceId" -> resourceId + ): _* + ) + + def sourcePayload(id: String, priority: Int): Json = + jsonContentOf( + "/kg/resources/simple-resource.json", + "resourceId" -> id, + "priority" -> priority.toString + ) + + def sourcePayload(priority: Int): Json = + jsonContentOf( + "/kg/resources/simple-resource.json", + "priority" -> priority.toString + ) + + def sourcePayloadWithType(resourceType: String, priority: Int): Json = + jsonContentOf( + "/kg/resources/simple-resource.json", + "priority" -> priority.toString, + "resourceType" -> resourceType + ) + +}