diff --git a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViews.scala b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViews.scala index 9041eaa16d..16a72fcfb7 100644 --- a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViews.scala +++ b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViews.scala @@ -259,6 +259,30 @@ final class ElasticSearchViews private ( } yield res }.span("deprecateElasticSearchView") + /** + * Undeprecates an existing ElasticSearchView. View undeprecation implies unblocking any query capabilities and in + * case of an IndexingElasticSearchView the corresponding index is created. + * + * @param id + * the view identifier + * @param project + * the view parent project + * @param rev + * the current view revision + * @param subject + * the subject that initiated the action + */ + def undeprecate( + id: IdSegment, + project: ProjectRef, + rev: Int + )(implicit subject: Subject): IO[ViewResource] = { + for { + (iri, _) <- expandWithContext(fetchContext.onModify, project, id) + res <- eval(UndeprecateElasticSearchView(iri, project, rev, subject)) + } yield res + }.span("undeprecateElasticSearchView") + /** * Deprecates an existing ElasticSearchView without applying preliminary checks on the project status * @@ -469,11 +493,16 @@ object ElasticSearchViews { s.copy(rev = e.rev, deprecated = true, updatedAt = e.instant, updatedBy = e.subject) } + def undeprecated(e: ElasticSearchViewUndeprecated): Option[ElasticSearchViewState] = state.map { s => + s.copy(rev = e.rev, deprecated = false, updatedAt = e.instant, updatedBy = e.subject) + } + event match { - case e: ElasticSearchViewCreated => created(e) - case e: ElasticSearchViewUpdated => updated(e) - case e: ElasticSearchViewTagAdded => tagAdded(e) - case e: ElasticSearchViewDeprecated => deprecated(e) + case e: ElasticSearchViewCreated => created(e) + case e: ElasticSearchViewUpdated => updated(e) + case e: ElasticSearchViewTagAdded => tagAdded(e) + case e: ElasticSearchViewDeprecated => deprecated(e) + case e: ElasticSearchViewUndeprecated => undeprecated(e) } } @@ -528,11 +557,22 @@ object ElasticSearchViews { ) } + def undeprecate(c: UndeprecateElasticSearchView) = state match { + case None => IO.raiseError(ViewNotFound(c.id, c.project)) + case Some(s) if s.rev != c.rev => IO.raiseError(IncorrectRev(c.rev, s.rev)) + case Some(s) if !s.deprecated => IO.raiseError(ViewIsNotDeprecated(c.id)) + case Some(s) => + clock.realTimeInstant.map( + ElasticSearchViewUndeprecated(c.id, c.project, s.value.tpe, s.uuid, s.rev + 1, _, c.subject) + ) + } + cmd match { - case c: CreateElasticSearchView => create(c) - case c: UpdateElasticSearchView => update(c) - case c: TagElasticSearchView => tag(c) - case c: DeprecateElasticSearchView => deprecate(c) + case c: CreateElasticSearchView => create(c) + case c: UpdateElasticSearchView => update(c) + case c: TagElasticSearchView => tag(c) + case c: DeprecateElasticSearchView => deprecate(c) + case c: UndeprecateElasticSearchView => undeprecate(c) } } diff --git a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewCommand.scala b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewCommand.scala index 240ddb72da..c45e215a40 100644 --- a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewCommand.scala +++ b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewCommand.scala @@ -106,6 +106,25 @@ object ElasticSearchViewCommand { subject: Subject ) extends ElasticSearchViewCommand + /** + * Command for the undeprecation of an ElasticSearch view. + * + * @param id + * the view id + * @param project + * a reference to the parent project + * @param rev + * the last known revision of the view + * @param subject + * the identity associated with this command + */ + final case class UndeprecateElasticSearchView( + id: Iri, + project: ProjectRef, + rev: Int, + subject: Subject + ) extends ElasticSearchViewCommand + /** * Command for adding a tag to an ElasticSearch view. * diff --git a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewEvent.scala b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewEvent.scala index fb3e69df68..54352b3624 100644 --- a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewEvent.scala +++ b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewEvent.scala @@ -185,6 +185,34 @@ object ElasticSearchViewEvent { subject: Subject ) extends ElasticSearchViewEvent + /** + * Evidence of a view undeprecation. + * + * @param id + * the view identifier + * @param project + * the view parent project + * @param tpe + * the view type + * @param uuid + * the view unique identifier + * @param rev + * the revision that the event generates + * @param instant + * the instant when the event was emitted + * @param subject + * the subject that undeprecated the view + */ + final case class ElasticSearchViewUndeprecated( + id: Iri, + project: ProjectRef, + tpe: ElasticSearchViewType, + uuid: UUID, + rev: Int, + instant: Instant, + subject: Subject + ) extends ElasticSearchViewEvent + @nowarn("cat=unused") val serializer: Serializer[Iri, ElasticSearchViewEvent] = { import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Database._ @@ -204,10 +232,11 @@ object ElasticSearchViewEvent { ProjectScopedMetric.from( event, event match { - case _: ElasticSearchViewCreated => Created - case _: ElasticSearchViewUpdated => Updated - case _: ElasticSearchViewTagAdded => Tagged - case _: ElasticSearchViewDeprecated => Deprecated + case _: ElasticSearchViewCreated => Created + case _: ElasticSearchViewUpdated => Updated + case _: ElasticSearchViewTagAdded => Tagged + case _: ElasticSearchViewDeprecated => Deprecated + case _: ElasticSearchViewUndeprecated => Undeprecated }, event.id, event.tpe.types, diff --git a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewRejection.scala b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewRejection.scala index a313757ae1..d5b03183af 100644 --- a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewRejection.scala +++ b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewRejection.scala @@ -86,6 +86,15 @@ object ElasticSearchViewRejection { final case class ViewIsDeprecated(id: Iri) extends ElasticSearchViewRejection(s"ElasticSearch view '$id' is deprecated.") + /** + * Rejection returned when attempting to undeprecate a view that is not deprecated. + * + * @param id + * the view id + */ + final case class ViewIsNotDeprecated(id: Iri) + extends ElasticSearchViewRejection(s"ElasticSearch view '$id' is not deprecated.") + /** * Rejection returned when a subject intends to perform an operation on the current view, but either provided an * incorrect revision or a concurrent update won over this attempt. diff --git a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutes.scala b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutes.scala index 8a00684b6a..e5c31aabb2 100644 --- a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutes.scala +++ b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutes.scala @@ -135,6 +135,19 @@ final class ElasticSearchViewsRoutes( } ) }, + // Undeprecate an elasticsearch view + (pathPrefix("undeprecate") & put & pathEndOrSingleSlash & parameter("rev".as[Int])) { rev => + authorizeFor(ref, Write).apply { + emit( + views + .undeprecate(id, ref, rev) + .flatTap(index(ref, _, mode)) + .mapValue(_.metadata) + .attemptNarrow[ElasticSearchViewRejection] + .rejectWhen(decodingFailedOrViewNotFound) + ) + } + }, // Query an elasticsearch view (pathPrefix("_search") & post & pathEndOrSingleSlash) { (extractQueryParams & entity(as[JsonObject])) { (qp, query) => diff --git a/delta/plugins/elasticsearch/src/test/resources/elasticsearch/database/view-undeprecated.json b/delta/plugins/elasticsearch/src/test/resources/elasticsearch/database/view-undeprecated.json new file mode 100644 index 0000000000..021ce9cdb2 --- /dev/null +++ b/delta/plugins/elasticsearch/src/test/resources/elasticsearch/database/view-undeprecated.json @@ -0,0 +1,14 @@ +{ + "id" : "https://bluebrain.github.io/nexus/vocabulary/indexing-view", + "project" : "myorg/myproj", + "uuid" : "f8468909-a797-4b10-8b5f-000cba337bfa", + "rev" : 5, + "instant" : "1970-01-01T00:00:00Z", + "subject" : { + "subject" : "username", + "realm" : "myrealm", + "@type" : "User" + }, + "@type" : "ElasticSearchViewUndeprecated", + "tpe": "ElasticSearchView" +} diff --git a/delta/plugins/elasticsearch/src/test/resources/elasticsearch/sse/view-undeprecated.json b/delta/plugins/elasticsearch/src/test/resources/elasticsearch/sse/view-undeprecated.json new file mode 100644 index 0000000000..f6e9053272 --- /dev/null +++ b/delta/plugins/elasticsearch/src/test/resources/elasticsearch/sse/view-undeprecated.json @@ -0,0 +1,19 @@ +{ + "@context": [ + "https://bluebrain.github.io/nexus/contexts/metadata.json", + "https://bluebrain.github.io/nexus/contexts/elasticsearch.json" + ], + "@type": "ElasticSearchViewUndeprecated", + "_constrainedBy": "https://bluebrain.github.io/nexus/schemas/views.json", + "_instant": "1970-01-01T00:00:00Z", + "_project": "http://localhost/v1/projects/myorg/myproj", + "_resourceId": "https://bluebrain.github.io/nexus/vocabulary/indexing-view", + "_rev": 5, + "_subject": "http://localhost/v1/realms/myrealm/users/username", + "_types": [ + "https://bluebrain.github.io/nexus/vocabulary/ElasticSearchView", + "https://bluebrain.github.io/nexus/vocabulary/View" + ], + "_uuid": "f8468909-a797-4b10-8b5f-000cba337bfa", + "_viewId": "https://bluebrain.github.io/nexus/vocabulary/indexing-view" +} \ No newline at end of file diff --git a/delta/plugins/elasticsearch/src/test/resources/routes/errors/view-not-deprecated.json b/delta/plugins/elasticsearch/src/test/resources/routes/errors/view-not-deprecated.json new file mode 100644 index 0000000000..f4a5f75ab5 --- /dev/null +++ b/delta/plugins/elasticsearch/src/test/resources/routes/errors/view-not-deprecated.json @@ -0,0 +1,5 @@ +{ + "@context": "https://bluebrain.github.io/nexus/contexts/error.json", + "@type": "ViewIsNotDeprecated", + "reason": "ElasticSearch view '{{id}}' is not deprecated." +} \ No newline at end of file diff --git a/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViewSTMSpec.scala b/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViewSTMSpec.scala index e79f2d1977..34d517abd3 100644 --- a/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViewSTMSpec.scala +++ b/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViewSTMSpec.scala @@ -193,6 +193,29 @@ class ElasticSearchViewSTMSpec extends CatsEffectSpec { } } + "evaluating the UndeprecateElasticSearchView command" should { + "emit an ElasticSearchViewUndeprecated" in { + val deprecatedState = Some(current(deprecated = true)) + val undeprecateCmd = UndeprecateElasticSearchView(id, project, 1, subject) + val undeprecatedEvent = ElasticSearchViewUndeprecated(id, project, ElasticSearchType, uuid, 2, epoch, subject) + eval(deprecatedState, undeprecateCmd).accepted shouldEqual undeprecatedEvent + } + "raise a ViewNotFound rejection" in { + val undeprecateCmd = UndeprecateElasticSearchView(id, project, 1, subject) + eval(None, undeprecateCmd).rejectedWith[ViewNotFound] + } + "raise a IncorrectRev rejection" in { + val deprecatedState = Some(current(deprecated = true)) + val undeprecateCmd = UndeprecateElasticSearchView(id, project, 2, subject) + eval(deprecatedState, undeprecateCmd).rejectedWith[IncorrectRev] + } + "raise a ViewIsNotDeprecated rejection" in { + val activeView = Some(current()) + val undeprecateCmd = UndeprecateElasticSearchView(id, project, 1, subject) + eval(activeView, undeprecateCmd).rejectedWith[ViewIsNotDeprecated] + } + } + "applying an ElasticSearchViewCreated event" should { "discard the event for a Current state" in { next( @@ -285,6 +308,19 @@ class ElasticSearchViewSTMSpec extends CatsEffectSpec { } } + "applying an ElasticSearchViewUndeprecated event" should { + "discard the event for an Initial state" in { + val undeprecatedEvent = ElasticSearchViewUndeprecated(id, project, ElasticSearchType, uuid, 2, epoch, subject) + next(None, undeprecatedEvent) shouldEqual None + } + "change the state" in { + val deprecatedState = Some(current(deprecated = true)) + val undeprecatedEvent = ElasticSearchViewUndeprecated(id, project, ElasticSearchType, uuid, 2, epoch, subject) + next(deprecatedState, undeprecatedEvent).value shouldEqual + current(deprecated = false, rev = 2, updatedBy = subject) + } + } + } } diff --git a/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViewsSpec.scala b/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViewsSpec.scala index 0327c12113..d4d21e27a9 100644 --- a/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViewsSpec.scala +++ b/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/ElasticSearchViewsSpec.scala @@ -3,7 +3,7 @@ package ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch import cats.data.NonEmptySet import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF -import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model.ElasticSearchViewRejection.{DifferentElasticSearchViewType, IncorrectRev, InvalidPipeline, InvalidViewReferences, PermissionIsNotDefined, ProjectContextRejection, ResourceAlreadyExists, RevisionNotFound, TagNotFound, TooManyViewReferences, ViewIsDeprecated, ViewNotFound} +import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model.ElasticSearchViewRejection.{DifferentElasticSearchViewType, IncorrectRev, InvalidPipeline, InvalidViewReferences, PermissionIsNotDefined, ProjectContextRejection, ResourceAlreadyExists, RevisionNotFound, TagNotFound, TooManyViewReferences, ViewIsDeprecated, ViewIsNotDeprecated, ViewNotFound} import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model.ElasticSearchViewValue.{AggregateElasticSearchViewValue, IndexingElasticSearchViewValue} import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model._ import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model.permissions.{query => queryPermissions} @@ -29,6 +29,7 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.stream.pipes.{FilterBySchema, Filt import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsEffectSpec import io.circe.Json import io.circe.literal._ +import org.scalatest.Assertion import java.time.Instant import java.util.UUID @@ -405,6 +406,55 @@ class ElasticSearchViewsSpec extends CatsEffectSpec with DoobieScalaTestFixture } } + "undeprecate a view" when { + "using the correct revision" in { + givenADeprecatedView { view => + views.undeprecate(view, projectRef, 2).accepted shouldEqual + resourceFor( + id = nxv + view, + deprecated = false, + rev = 3, + value = IndexingElasticSearchViewValue( + resourceTag = None, + IndexingElasticSearchViewValue.defaultPipeline, + mapping = Some(mapping), + settings = None, + context = None, + permission = queryPermissions + ), + source = json"""{"@type": "ElasticSearchView", "mapping": $mapping}""" + ) + views.fetch(view, projectRef).accepted.deprecated shouldEqual false + } + } + } + + "fail to undeprecate a view" when { + "the view is not deprecated" in { + givenAView { view => + views.undeprecate(view, projectRef, 1).assertRejectedWith[ViewIsNotDeprecated] + } + } + "providing an incorrect revision for an IndexingElasticSearchViewValue" in { + givenADeprecatedView { view => + views.undeprecate(view, projectRef, 100).assertRejectedWith[IncorrectRev] + } + } + "the target view is not found" in { + val nonExistentView = iri"http://localhost/${genString()}" + views.undeprecate(nonExistentView, projectRef, 2).rejectedWith[ViewNotFound] + } + "the project of the target view is not found" in { + givenAView { view => + views.undeprecate(view, unknownProjectRef, 2).assertRejectedWith[ProjectContextRejection] + } + } + "the referenced project is deprecated" in { + val id = iri"http://localhost/${genString()}" + views.undeprecate(id, deprecatedProjectRef, 2).rejectedWith[ProjectContextRejection] + } + } + "fetch a view by id" when { "no rev nor tag is provided" in { val id = iri"http://localhost/${genString()}" @@ -484,5 +534,20 @@ class ElasticSearchViewsSpec extends CatsEffectSpec with DoobieScalaTestFixture views.fetch(IdSegmentRef(id, tag), projectRef).rejectedWith[TagNotFound] } } + + def givenAView(test: String => Assertion): Assertion = { + val id = genString() + val source = json"""{"@type": "ElasticSearchView", "mapping": $mapping}""" + views.create(id, projectRef, source).accepted + test(id) + } + + def givenADeprecatedView(test: String => Assertion): Assertion = { + givenAView { view => + views.deprecate(view, projectRef, 1).accepted + test(view) + } + } + } } diff --git a/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewSerializationSuite.scala b/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewSerializationSuite.scala index f8b8b584a9..a8ea13210f 100644 --- a/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewSerializationSuite.scala +++ b/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/model/ElasticSearchViewSerializationSuite.scala @@ -2,7 +2,7 @@ package ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model import cats.data.NonEmptySet import ch.epfl.bluebrain.nexus.delta.kernel.utils.ClassUtils -import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model.ElasticSearchViewEvent.{ElasticSearchViewCreated, ElasticSearchViewDeprecated, ElasticSearchViewTagAdded, ElasticSearchViewUpdated} +import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model.ElasticSearchViewEvent._ import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model.ElasticSearchViewType.{ElasticSearch => ElasticSearchType} import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model.ElasticSearchViewValue.{AggregateElasticSearchViewValue, IndexingElasticSearchViewValue} import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv @@ -65,6 +65,7 @@ class ElasticSearchViewSerializationSuite extends SerializationSuite { private val updated2 = ElasticSearchViewUpdated(aggregateId, projectRef, uuid, aggregateValue, aggregateSource, 2, instant, subject) private val tagged = ElasticSearchViewTagAdded(indexingId, projectRef, ElasticSearchType, uuid, targetRev = 1, tag, 3, instant, subject) private val deprecated = ElasticSearchViewDeprecated(indexingId, projectRef, ElasticSearchType, uuid, 4, instant, subject) + private val undeprecated = ElasticSearchViewUndeprecated(indexingId, projectRef, ElasticSearchType, uuid, 5, instant, subject) // format: on private val elasticsearchViewsMapping = List( @@ -75,27 +76,28 @@ class ElasticSearchViewSerializationSuite extends SerializationSuite { (updated1, loadEvents("elasticsearch", "indexing-view-updated.json"), Updated), (updated2, loadEvents("elasticsearch", "aggregate-view-updated.json"), Updated), (tagged, loadEvents("elasticsearch", "view-tag-added.json"), Tagged), - (deprecated, loadEvents("elasticsearch", "view-deprecated.json"), Deprecated) + (deprecated, loadEvents("elasticsearch", "view-deprecated.json"), Deprecated), + (undeprecated, loadEvents("elasticsearch", "view-undeprecated.json"), Undeprecated) ) private val sseEncoder = ElasticSearchViewEvent.sseEncoder elasticsearchViewsMapping.foreach { case (event, (database, sse), action) => - test(s"Correctly serialize ${event.getClass.getName}") { + test(s"Correctly serialize ${event.getClass.getSimpleName}") { assertOutput(ElasticSearchViewEvent.serializer, event, database) } - test(s"Correctly deserialize ${event.getClass.getName}") { + test(s"Correctly deserialize ${event.getClass.getSimpleName}") { assertEquals(ElasticSearchViewEvent.serializer.codec.decodeJson(database), Right(event)) } - test(s"Correctly serialize ${event.getClass.getName} as an SSE") { + test(s"Correctly serialize ${event.getClass.getSimpleName} as an SSE") { sseEncoder.toSse .decodeJson(database) .assertRight(SseData(ClassUtils.simpleName(event), Some(projectRef), sse)) } - test(s"Correctly encode ${event.getClass.getName} to metric") { + test(s"Correctly encode ${event.getClass.getSimpleName} to metric") { ElasticSearchViewEvent.esViewMetricEncoder.toMetric.decodeJson(database).assertRight { ProjectScopedMetric( instant, diff --git a/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutesFixtures.scala b/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutesFixtures.scala index 294e87175a..52cd979733 100644 --- a/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutesFixtures.scala +++ b/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutesFixtures.scala @@ -9,8 +9,8 @@ import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclSimpleCheck import ch.epfl.bluebrain.nexus.delta.sdk.circe.CirceMarshalling import ch.epfl.bluebrain.nexus.delta.sdk.generators.ProjectGen +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.identities.{Identities, IdentitiesDummy} import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{RdfExceptionHandler, RdfRejectionHandler} import ch.epfl.bluebrain.nexus.delta.sdk.model._ import ch.epfl.bluebrain.nexus.delta.sdk.model.search.PaginationConfig @@ -56,13 +56,18 @@ class ElasticSearchViewsRoutesFixtures val aclCheck: AclSimpleCheck = AclSimpleCheck().accepted val realm: Label = Label.unsafe("wonderland") - val alice: User = User("alice", realm) - val caller: Caller = Caller(alice, Set(alice, Anonymous, Authenticated(realm), Group("group", realm))) + val reader = User("reader", realm) + val writer = User("writer", realm) - val identities: Identities = IdentitiesDummy(caller) + implicit private val callerReader: Caller = + Caller(reader, Set(reader, Anonymous, Authenticated(realm), Group("group", realm))) + implicit private val callerWriter: Caller = + Caller(writer, Set(writer, Anonymous, Authenticated(realm), Group("group", realm))) + val identities = IdentitiesDummy(callerReader, callerWriter) - val asAlice = addCredentials(OAuth2BearerToken("alice")) + val asReader = addCredentials(OAuth2BearerToken("reader")) + val asWriter = addCredentials(OAuth2BearerToken("writer")) val project: ProjectResource = ProjectGen.resourceFor( ProjectGen.project( diff --git a/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutesSpec.scala b/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutesSpec.scala index a308bda304..01e3d48fd2 100644 --- a/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutesSpec.scala +++ b/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutesSpec.scala @@ -24,6 +24,7 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Subject import ch.epfl.bluebrain.nexus.delta.sourcing.stream.PipeChain import ch.epfl.bluebrain.nexus.testkit.ce.IOFromMap import io.circe.Json +import org.scalatest.Assertion class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with IOFromMap { @@ -98,29 +99,31 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with ) ) + override def beforeAll(): Unit = { + super.beforeAll() + aclCheck.append(AclAddress.Root, reader -> Set(esPermissions.read)).accepted + aclCheck.append(AclAddress.Root, writer -> Set(esPermissions.write)).accepted + } + "Elasticsearch views routes" should { "fail to create a view without views/write permission" in { - aclCheck.append(AclAddress.Root, Anonymous -> Set(events.read)).accepted - Post("/v1/views/myorg/myproject", payload.toEntity) ~> routes ~> check { + Post("/v1/views/myorg/myproject", payload.toEntity) ~> asReader ~> routes ~> check { response.shouldBeForbidden } } "create a view" in { - aclCheck - .append(AclAddress.Root, Anonymous -> Set(esPermissions.write), caller.subject -> Set(esPermissions.write)) - .accepted - Post("/v1/views/myorg/myproject", payload.toEntity) ~> routes ~> check { + Post("/v1/views/myorg/myproject", payload.toEntity) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Created response.asJson shouldEqual elasticSearchViewMetadata(myId) } } "create a view with an authenticated user and provided id" in { - Put("/v1/views/myorg/myproject/myid2", payloadNoId.toEntity) ~> asAlice ~> routes ~> check { + Put("/v1/views/myorg/myproject/myid2", payloadNoId.toEntity) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Created - response.asJson shouldEqual elasticSearchViewMetadata(myId2, createdBy = alice, updatedBy = alice) + response.asJson shouldEqual elasticSearchViewMetadata(myId2) } } @@ -128,15 +131,15 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with Put( "/v1/views/myorg/myproject/myid3", payloadNoId.deepMerge(json"""{ "pipeline": [ { "name": "filterDeprecated" } ]}""").toEntity - ) ~> asAlice ~> routes ~> check { + ) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Created response.asJson shouldEqual - elasticSearchViewMetadata(myId3, createdBy = alice, updatedBy = alice) + elasticSearchViewMetadata(myId3) } } "reject the creation of a view which already exists" in { - Put("/v1/views/myorg/myproject/myid", payload.toEntity) ~> routes ~> check { + Put("/v1/views/myorg/myproject/myid", payload.toEntity) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Conflict response.asJson shouldEqual jsonContentOf("routes/errors/already-exists.json", "id" -> myId, "project" -> "myorg/myproject") @@ -147,7 +150,7 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with Put( "/v1/views/myorg/myproject/unknown-pipe", payloadNoId.deepMerge(json"""{ "pipeline": [ { "name": "xxx" } ]}""").toEntity - ) ~> routes ~> check { + ) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.BadRequest response.asJson shouldEqual jsonContentOf("routes/errors/pipe-not-found.json", "id" -> myId, "project" -> "myorg/myproject") @@ -155,20 +158,18 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with } "fail to update a view without views/write permission" in { - aclCheck.subtract(AclAddress.Root, Anonymous -> Set(esPermissions.write)).accepted - Put(s"/v1/views/myorg/myproject/myid?rev=1", payload.toEntity) ~> routes ~> check { + Put(s"/v1/views/myorg/myproject/myid?rev=1", payload.toEntity) ~> asReader ~> routes ~> check { response.shouldBeForbidden } } "update a view" in { - aclCheck.append(AclAddress.Root, Anonymous -> Set(esPermissions.write)).accepted val endpoints = List( "/v1/views/myorg/myproject/myid", s"/v1/views/myorg/myproject/$myIdEncoded" ) forAll(endpoints.zipWithIndex) { case (endpoint, idx) => - Put(s"$endpoint?rev=${idx + 1}", payloadUpdated.toEntity) ~> routes ~> check { + Put(s"$endpoint?rev=${idx + 1}", payloadUpdated.toEntity) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual elasticSearchViewMetadata(myId, rev = idx + 2) } @@ -177,7 +178,7 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with "reject the update of a non-existent view" in { val payload = payloadUpdated.removeKeys(keywords.id) - Put("/v1/views/myorg/myproject/myid10?rev=1", payload.toEntity) ~> routes ~> check { + Put("/v1/views/myorg/myproject/myid10?rev=1", payload.toEntity) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.NotFound response.asJson shouldEqual jsonContentOf("routes/errors/not-found.json", "id" -> (nxv + "myid10"), "proj" -> "myorg/myproject") @@ -185,7 +186,7 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with } "reject the update of a view at a non-existent revision" in { - Put("/v1/views/myorg/myproject/myid?rev=10", payloadUpdated.toEntity) ~> routes ~> check { + Put("/v1/views/myorg/myproject/myid?rev=10", payloadUpdated.toEntity) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Conflict response.asJson shouldEqual jsonContentOf("routes/errors/incorrect-rev.json", "provided" -> 10, "expected" -> 3) @@ -193,29 +194,27 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with } "fail to deprecate a view without views/write permission" in { - aclCheck.subtract(AclAddress.Root, Anonymous -> Set(esPermissions.write)).accepted - Delete("/v1/views/myorg/myproject/myid?rev=3") ~> routes ~> check { + Delete("/v1/views/myorg/myproject/myid?rev=3") ~> asReader ~> routes ~> check { response.shouldBeForbidden } } "deprecate a view" in { - aclCheck.append(AclAddress.Root, Anonymous -> Set(esPermissions.write)).accepted - Delete("/v1/views/myorg/myproject/myid?rev=3") ~> routes ~> check { + Delete("/v1/views/myorg/myproject/myid?rev=3") ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual elasticSearchViewMetadata(myId, rev = 4, deprecated = true) } } "reject the deprecation of a view without rev" in { - Delete("/v1/views/myorg/myproject/myid") ~> routes ~> check { + Delete("/v1/views/myorg/myproject/myid") ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.BadRequest response.asJson shouldEqual jsonContentOf("routes/errors/missing-query-param.json", "field" -> "rev") } } "reject the deprecation of a already deprecated view" in { - Delete(s"/v1/views/myorg/myproject/myid?rev=4") ~> routes ~> check { + Delete(s"/v1/views/myorg/myproject/myid?rev=4") ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.BadRequest response.asJson shouldEqual jsonContentOf("routes/errors/view-deprecated.json", "id" -> myId) } @@ -229,11 +228,55 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with } } + "fail to undeprecate a view without views/write permission" in { + givenADeprecatedView { view => + Put( + s"/v1/views/myorg/myproject/$view/undeprecate?rev=2", + payloadUpdated.toEntity + ) ~> asReader ~> routes ~> check { + response.shouldBeForbidden + } + } + } + + "undeprecate a view" in { + givenADeprecatedView { view => + Put( + s"/v1/views/myorg/myproject/$view/undeprecate?rev=2", + payloadUpdated.toEntity + ) ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.OK + response.asJson shouldEqual elasticSearchViewMetadata(nxv + view, rev = 3) + } + } + } + + "reject the undeprecation of a view without rev" in { + givenADeprecatedView { view => + Put(s"/v1/views/myorg/myproject/$view/undeprecate", payloadUpdated.toEntity) ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.BadRequest + response.asJson shouldEqual jsonContentOf("routes/errors/missing-query-param.json", "field" -> "rev") + } + } + } + + "reject the undeprecation of a view that is not deprecated" in { + givenAView { view => + Put( + s"/v1/views/myorg/myproject/$view/undeprecate?rev=1", + payloadUpdated.toEntity + ) ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.BadRequest + response.asJson shouldEqual jsonContentOf("routes/errors/view-not-deprecated.json", "id" -> (nxv + view)) + } + } + } + "tag a view" in { val payload = json"""{"tag": "mytag", "rev": 1}""" - Post("/v1/views/myorg/myproject/myid2/tags?rev=1", payload.toEntity) ~> routes ~> check { + Post("/v1/views/myorg/myproject/myid2/tags?rev=1", payload.toEntity) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Created - response.asJson shouldEqual elasticSearchViewMetadata(myId2, rev = 2, createdBy = alice) + response.asJson shouldEqual elasticSearchViewMetadata(myId2, rev = 2) } } @@ -253,7 +296,7 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with "fetch a view" in { aclCheck.append(AclAddress.Root, Anonymous -> Set(esPermissions.read)).accepted - Get("/v1/views/myorg/myproject/myid") ~> routes ~> check { + Get("/v1/views/myorg/myproject/myid") ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual elasticSearchView( myId, @@ -277,9 +320,9 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with ) forAll(endpoints) { endpoint => forAll(List("rev=1", "tag=mytag")) { param => - Get(s"$endpoint?$param") ~> routes ~> check { + Get(s"$endpoint?$param") ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK - response.asJson shouldEqual elasticSearchView(myId2, createdBy = alice, updatedBy = alice) + response.asJson shouldEqual elasticSearchView(myId2) } } } @@ -296,7 +339,7 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with s"/v1/resources/myorg/myproject/_/$myId2Encoded/source" ) forAll(endpoints) { endpoint => - Get(endpoint) ~> routes ~> check { + Get(endpoint) ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual payloadNoId } @@ -310,7 +353,7 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with ) forAll(endpoints) { endpoint => forAll(List("rev=1", "tag=mytag")) { param => - Get(s"$endpoint?$param") ~> routes ~> check { + Get(s"$endpoint?$param") ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual payloadNoId } @@ -319,25 +362,25 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with } "fetch the view tags" in { - Get("/v1/resources/myorg/myproject/_/myid2/tags?rev=1") ~> routes ~> check { + Get("/v1/resources/myorg/myproject/_/myid2/tags?rev=1") ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual json"""{"tags": []}""".addContext(contexts.tags) } - Get("/v1/views/myorg/myproject/myid2/tags") ~> routes ~> check { + Get("/v1/views/myorg/myproject/myid2/tags") ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual json"""{"tags": [{"rev": 1, "tag": "mytag"}]}""".addContext(contexts.tags) } } "return not found if tag not found" in { - Get("/v1/views/myorg/myproject/myid2?tag=myother") ~> routes ~> check { + Get("/v1/views/myorg/myproject/myid2?tag=myother") ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.NotFound response.asJson shouldEqual jsonContentOf("routes/errors/tag-not-found.json", "tag" -> "myother") } } "reject if provided rev and tag simultaneously" in { - Get("/v1/views/myorg/myproject/myid2?tag=mytag&rev=1") ~> routes ~> check { + Get("/v1/views/myorg/myproject/myid2?tag=mytag&rev=1") ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.BadRequest response.asJson shouldEqual jsonContentOf("routes/errors/tag-and-rev-error.json") } @@ -362,12 +405,30 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with } } + private def givenAView(test: String => Assertion): Assertion = { + val viewId = genString() + val viewDefPayload = payload deepMerge json"""{"@id": "$viewId"}""" + Post("/v1/views/myorg/myproject", viewDefPayload.toEntity) ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.Created + } + test(viewId) + } + + private def givenADeprecatedView(test: String => Assertion): Assertion = { + givenAView { view => + Delete(s"/v1/views/myorg/myproject/$view?rev=1") ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.OK + } + test(view) + } + } + private def elasticSearchViewMetadata( id: Iri, rev: Int = 1, deprecated: Boolean = false, - createdBy: Subject = Anonymous, - updatedBy: Subject = Anonymous + createdBy: Subject = writer, + updatedBy: Subject = writer ): Json = jsonContentOf( "routes/elasticsearch-view-write-response.json", @@ -386,8 +447,8 @@ class ElasticSearchViewsRoutesSpec extends ElasticSearchViewsRoutesFixtures with includeDeprecated: Boolean = false, rev: Int = 1, deprecated: Boolean = false, - createdBy: Subject = Anonymous, - updatedBy: Subject = Anonymous + createdBy: Subject = writer, + updatedBy: Subject = writer ): Json = jsonContentOf( "routes/elasticsearch-view-read-response.json", diff --git a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsEffectEventually.scala b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsEffectEventually.scala new file mode 100644 index 0000000000..875321d371 --- /dev/null +++ b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsEffectEventually.scala @@ -0,0 +1,42 @@ +package ch.epfl.bluebrain.nexus.testkit.scalatest.ce + +import cats.effect.IO +import cats.implicits.catsSyntaxMonadError +import ch.epfl.bluebrain.nexus.delta.kernel.RetryStrategyConfig.MaximumCumulativeDelayConfig +import ch.epfl.bluebrain.nexus.delta.kernel.syntax._ +import ch.epfl.bluebrain.nexus.delta.kernel.{Logger, RetryStrategy} +import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsEffectEventually.logger +import org.scalactic.source.Position +import org.scalatest.Assertions +import org.scalatest.enablers.Retrying +import org.scalatest.exceptions.TestFailedException +import org.scalatest.time.Span + +trait CatsEffectEventually { self: Assertions => + implicit def ioRetrying[T]: Retrying[IO[T]] = new Retrying[IO[T]] { + override def retry(timeout: Span, interval: Span, pos: Position)(fun: => IO[T]): IO[T] = { + val strategy = RetryStrategy[Throwable]( + MaximumCumulativeDelayConfig(timeout, interval), + { + case _: TestFailedException => true + case _ => false + }, + onError = (err, details) => + IO.whenA(details.givingUp) { + logger.error(err)( + s"Giving up on ${err.getClass.getSimpleName}, ${details.retriesSoFar} retries after ${details.cumulativeDelay}." + ) + } + ) + fun + .retry(strategy) + .adaptError { case e: AssertionError => + fail(s"assertion failed after retrying with eventually: ${e.getMessage}", e) + } + } + } +} + +object CatsEffectEventually { + private val logger = Logger[CatsEffectEventually] +} diff --git a/docs/src/main/paradox/docs/delta/api/assets/views/elasticsearch/undeprecate.sh b/docs/src/main/paradox/docs/delta/api/assets/views/elasticsearch/undeprecate.sh new file mode 100644 index 0000000000..4f5352e069 --- /dev/null +++ b/docs/src/main/paradox/docs/delta/api/assets/views/elasticsearch/undeprecate.sh @@ -0,0 +1,2 @@ +curl -XPUT \ + "http://localhost:8080/v1/views/myorg/myproj/myview/undeprecate?rev=4" diff --git a/docs/src/main/paradox/docs/delta/api/assets/views/elasticsearch/undeprecated.json b/docs/src/main/paradox/docs/delta/api/assets/views/elasticsearch/undeprecated.json new file mode 100644 index 0000000000..d9fce019d5 --- /dev/null +++ b/docs/src/main/paradox/docs/delta/api/assets/views/elasticsearch/undeprecated.json @@ -0,0 +1,23 @@ +{ + "@context": [ + "https://bluebrain.github.io/nexus/contexts/elasticsearch-metadata.json", + "https://bluebrain.github.io/nexus/contexts/metadata.json" + ], + "@id": "http://localhost:8080/v1/resources/myorg/myproj/_/myview", + "@type": [ + "ElasticSearchView", + "View" + ], + "_constrainedBy": "https://bluebrain.github.io/nexus/schemas/views.json", + "_createdAt": "2021-05-12T12:56:09.676Z", + "_createdBy": "http://localhost:8080/v1/anonymous", + "_deprecated": false, + "_incoming": "http://localhost:8080/v1/views/myorg/myproj/myview/incoming", + "_outgoing": "http://localhost:8080/v1/views/myorg/myproj/myview/outgoing", + "_project": "http://localhost:8080/v1/projects/myorg/myproj", + "_rev": 5, + "_self": "http://localhost:8080/v1/views/myorg/myproj/myview", + "_updatedAt": "2021-05-12T13:01:01.232Z", + "_updatedBy": "http://localhost:8080/v1/anonymous", + "_uuid": "7e737b83-30a0-4ea3-b6c9-cd1ed481d743" +} \ No newline at end of file diff --git a/docs/src/main/paradox/docs/delta/api/views/elasticsearch-view-api.md b/docs/src/main/paradox/docs/delta/api/views/elasticsearch-view-api.md index 4dc077d330..af1588b3fd 100644 --- a/docs/src/main/paradox/docs/delta/api/views/elasticsearch-view-api.md +++ b/docs/src/main/paradox/docs/delta/api/views/elasticsearch-view-api.md @@ -288,6 +288,26 @@ Request Response : @@snip [deprecated.json](../assets/views/elasticsearch/deprecated.json) +## Undeprecate + +Unlocks the view, so further operations can be performed. It also restarts indexing resources into it. + +Undeprecating a view is considered to be an update as well. + +``` +PUT /v1/views/{org_label}/{project_label}/{view_id}/undeprecate?rev={previous_rev} +``` + +... where `{previous_rev}` is the last known revision number for the view. + +**Example** + +Request +: @@snip [undeprecate.sh](../assets/views/elasticsearch/undeprecate.sh) + +Response +: @@snip [undeprecated.json](../assets/views/elasticsearch/undeprecated.json) + ## Fetch ``` diff --git a/tests/src/test/resources/kg/resources/person.json b/tests/src/test/resources/kg/resources/person.json new file mode 100644 index 0000000000..4475b63c1e --- /dev/null +++ b/tests/src/test/resources/kg/resources/person.json @@ -0,0 +1,7 @@ +{ + "@context": { + "@vocab": "http://schema.org/" + }, + "@type": "Person", + "name": "Alice" +} \ No newline at end of file diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/BaseIntegrationSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/BaseIntegrationSpec.scala index 68129d6871..502d85e7ae 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/BaseIntegrationSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/BaseIntegrationSpec.scala @@ -12,7 +12,7 @@ import cats.syntax.all._ import ch.epfl.bluebrain.nexus.delta.kernel.Logger import ch.epfl.bluebrain.nexus.testkit._ import ch.epfl.bluebrain.nexus.testkit.clock.FixedClock -import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.{CatsEffectAsyncScalaTestAdapter, CatsIOValues} +import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.{CatsEffectAsyncScalaTestAdapter, CatsEffectEventually, CatsIOValues} import ch.epfl.bluebrain.nexus.testkit.scalatest.{ClasspathResources, EitherValues, ScalaTestExtractValue} import ch.epfl.bluebrain.nexus.tests.BaseIntegrationSpec._ import ch.epfl.bluebrain.nexus.tests.HttpClient._ @@ -55,7 +55,8 @@ trait BaseIntegrationSpec with ScalatestRouteTest with Eventually with AppendedClues - with ScalaFutures { + with ScalaFutures + with CatsEffectEventually { private val logger = Logger[this.type] diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ElasticSearchViewsSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ElasticSearchViewsSpec.scala index 233b3d227b..0144a08e7f 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ElasticSearchViewsSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ElasticSearchViewsSpec.scala @@ -1,51 +1,46 @@ package ch.epfl.bluebrain.nexus.tests.kg import akka.http.scaladsl.model.StatusCodes - +import cats.effect.IO import cats.effect.unsafe.implicits._ +import cats.implicits._ import ch.epfl.bluebrain.nexus.tests.BaseIntegrationSpec import ch.epfl.bluebrain.nexus.tests.Identity.Anonymous import ch.epfl.bluebrain.nexus.tests.Identity.views.ScoobyDoo import ch.epfl.bluebrain.nexus.tests.Optics._ import ch.epfl.bluebrain.nexus.tests.iam.types.Permission.{Organizations, Views} import io.circe.{ACursor, Json} -import cats.implicits._ +import org.scalatest.Assertion class ElasticSearchViewsSpec extends BaseIntegrationSpec { private val orgId = genId() private val projId = genId() - val fullId = s"$orgId/$projId" + val project1 = s"$orgId/$projId" private val projId2 = genId() - val fullId2 = s"$orgId/$projId2" - - val projects = List(fullId, fullId2) - - "creating projects" should { - "add necessary permissions for user" in { - for { - _ <- aclDsl.addPermission("/", ScoobyDoo, Organizations.Create) - _ <- aclDsl.addPermissionAnonymous(s"/$fullId2", Views.Query) - } yield succeed - } - - "succeed if payload is correct" in { - for { - _ <- adminDsl.createOrganization(orgId, orgId, ScoobyDoo) - _ <- adminDsl.createProjectWithName(orgId, projId, name = fullId, ScoobyDoo) - _ <- adminDsl.createProjectWithName(orgId, projId2, name = fullId2, ScoobyDoo) - } yield succeed - } - - "wait until in project resolver is created" in { - eventually { - deltaClient.get[Json](s"/resolvers/$fullId", ScoobyDoo) { (json, response) => - response.status shouldEqual StatusCodes.OK - _total.getOption(json).value shouldEqual 1L - } + val project2 = s"$orgId/$projId2" + + val projects = List(project1, project2) + + override def beforeAll(): Unit = { + super.beforeAll() + val setup = for { + _ <- aclDsl.addPermission("/", ScoobyDoo, Organizations.Create) + _ <- aclDsl.addPermissionAnonymous(s"/$project2", Views.Query) + _ <- adminDsl.createOrganization(orgId, orgId, ScoobyDoo) + _ <- adminDsl.createProjectWithName(orgId, projId, name = project1, ScoobyDoo) + _ <- adminDsl.createProjectWithName(orgId, projId2, name = project2, ScoobyDoo) + } yield succeed + + setup.accepted + eventually { + deltaClient.get[Json](s"/resolvers/$project1", ScoobyDoo) { (json, response) => + response.status shouldEqual StatusCodes.OK + _total.getOption(json).value shouldEqual 1L } } + () } "creating the view" should { @@ -62,7 +57,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "create elasticsearch views with legacy fields and its pipeline equivalent" in { - List(fullId -> "kg/views/elasticsearch/legacy-fields.json", fullId2 -> "kg/views/elasticsearch/pipeline.json") + List(project1 -> "kg/views/elasticsearch/legacy-fields.json", project2 -> "kg/views/elasticsearch/pipeline.json") .parTraverse { case (project, file) => deltaClient .put[Json](s"/views/$project/test-resource:cell-view", jsonContentOf(file, "withTag" -> false), ScoobyDoo) { @@ -73,7 +68,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "create elasticsearch views filtering on tag with legacy fields and its pipeline equivalent" in { - List(fullId -> "kg/views/elasticsearch/legacy-fields.json", fullId2 -> "kg/views/elasticsearch/pipeline.json") + List(project1 -> "kg/views/elasticsearch/legacy-fields.json", project2 -> "kg/views/elasticsearch/pipeline.json") .parTraverse { case (project, file) => deltaClient.put[Json]( s"/views/$project/test-resource:cell-view-tagged", @@ -89,7 +84,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { val invalidMapping = json"""{"mapping": "fail"}""" val payloadWithInvalidMapping = json"""{ "@type": "ElasticSearchView", "mapping": $invalidMapping }""" - deltaClient.put[Json](s"/views/$fullId/invalid", payloadWithInvalidMapping, ScoobyDoo) { expectBadRequest } + deltaClient.put[Json](s"/views/$project1/invalid", payloadWithInvalidMapping, ScoobyDoo) { expectBadRequest } } "fail to create a view with invalid settings" in { @@ -97,12 +92,12 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { json"""{"analysis": "fail"}""" val payloadWithInvalidSettings = json"""{ "@type": "ElasticSearchView", "mapping": { }, "settings": $invalidSettings }""" - deltaClient.put[Json](s"/views/$fullId/invalid", payloadWithInvalidSettings, ScoobyDoo) { expectBadRequest } + deltaClient.put[Json](s"/views/$project1/invalid", payloadWithInvalidSettings, ScoobyDoo) { expectBadRequest } } "create people view in project 2" in { deltaClient.put[Json]( - s"/views/$fullId2/test-resource:people", + s"/views/$project2/test-resource:people", jsonContentOf("kg/views/elasticsearch/people-view.json"), ScoobyDoo ) { (_, response) => @@ -134,16 +129,16 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { "create an AggregateElasticSearchView" in { elasticsearchViewsDsl.aggregate( "test-resource:agg-cell-view", - fullId2, + project2, ScoobyDoo, - fullId -> "https://dev.nexus.test.com/simplified-resource/cell-view", - fullId2 -> "https://dev.nexus.test.com/simplified-resource/cell-view" + project1 -> "https://dev.nexus.test.com/simplified-resource/cell-view", + project2 -> "https://dev.nexus.test.com/simplified-resource/cell-view" ) } "get the created AggregateElasticSearchView" in { val id = "https://dev.nexus.test.com/simplified-resource/agg-cell-view" - deltaClient.get[Json](s"/views/$fullId2/test-resource:agg-cell-view", ScoobyDoo) { (json, response) => + deltaClient.get[Json](s"/views/$project2/test-resource:agg-cell-view", ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.OK val expected = jsonContentOf( @@ -151,10 +146,10 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { replacements( ScoobyDoo, "id" -> id, - "self" -> viewSelf(fullId2, id), - "project-parent" -> s"${config.deltaUri}/projects/$fullId2", - "project1" -> fullId, - "project2" -> fullId2 + "self" -> viewSelf(project2, id), + "project-parent" -> s"${config.deltaUri}/projects/$project2", + "project1" -> project1, + "project2" -> project2 ): _* ) @@ -167,7 +162,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { val payload = jsonContentOf(s"kg/views/instances/instance$i.json") val id = `@id`.getOption(payload).value val unprefixedId = id.stripPrefix("https://bbp.epfl.ch/nexus/v0/data/bbp/experiment/patchedcell/v0.1.0/") - val projectId = if (i > 5) fullId2 else fullId + val projectId = if (i > 5) project2 else project1 val indexingMode = if (i % 2 == 0) "sync" else "async" deltaClient.put[Json]( @@ -183,7 +178,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { "post instance without id" in { val payload = jsonContentOf(s"kg/views/instances/instance9.json") deltaClient.post[Json]( - s"/resources/$fullId2/resource?indexing=sync", + s"/resources/$project2/resource?indexing=sync", payload, ScoobyDoo ) { (_, response) => @@ -192,14 +187,14 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "wait until in project view is indexed" in eventually { - deltaClient.get[Json](s"/views/$fullId?type=nxv%3AElasticSearchView", ScoobyDoo) { (json, response) => + deltaClient.get[Json](s"/views/$project1?type=nxv%3AElasticSearchView", ScoobyDoo) { (json, response) => _total.getOption(json).value shouldEqual 3 response.status shouldEqual StatusCodes.OK } } "wait until all instances are indexed in default view of project 2" in eventually { - deltaClient.get[Json](s"/resources/$fullId2/resource", ScoobyDoo) { (json, response) => + deltaClient.get[Json](s"/resources/$project2/resource", ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.OK _total.getOption(json).value shouldEqual 5 } @@ -207,7 +202,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { "return 400 with bad query instances" in { deltaClient.post[Json]( - s"/views/$fullId/test-resource:cell-view/_search", + s"/views/$project1/test-resource:cell-view/_search", json"""{ "query": { "other": {} } }""", ScoobyDoo ) { (json, response) => @@ -221,7 +216,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { val matchAll = json"""{ "query": { "match_all": {} } }""" deepMerge sort "search instances on project 1 in cell-view" in eventually { - deltaClient.post[Json](s"/views/$fullId/test-resource:cell-view/_search", sortedMatchCells, ScoobyDoo) { + deltaClient.post[Json](s"/views/$project1/test-resource:cell-view/_search", sortedMatchCells, ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.OK val index = hits(0)._index.string.getOption(json).value @@ -229,7 +224,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { jsonContentOf("kg/views/elasticsearch/search-response.json", "index" -> index) deltaClient - .post[Json](s"/views/$fullId/test-resource:cell-view/_search", matchAll, ScoobyDoo) { (json2, _) => + .post[Json](s"/views/$project1/test-resource:cell-view/_search", matchAll, ScoobyDoo) { (json2, _) => filterKey("took")(json2) shouldEqual filterKey("took")(json) } .unsafeRunSync() @@ -237,7 +232,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "get no instance in cell-view-tagged in project1 as nothing is tagged yet" in eventually { - deltaClient.post[Json](s"/views/$fullId/test-resource:cell-view-tagged/_search", matchAll, ScoobyDoo) { + deltaClient.post[Json](s"/views/$project1/test-resource:cell-view-tagged/_search", matchAll, ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.OK totalHits.getOption(json).value shouldEqual 0 @@ -245,7 +240,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "search cell instances on project 2" in eventually { - deltaClient.post[Json](s"/views/$fullId2/test-resource:cell-view/_search", sortedMatchCells, ScoobyDoo) { + deltaClient.post[Json](s"/views/$project2/test-resource:cell-view/_search", sortedMatchCells, ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.OK val index = hits(0)._index.string.getOption(json).value @@ -253,7 +248,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { jsonContentOf("kg/views/elasticsearch/search-response-2.json", "index" -> index) deltaClient - .post[Json](s"/views/$fullId2/test-resource:cell-view/_search", matchAll, ScoobyDoo) { (json2, _) => + .post[Json](s"/views/$project2/test-resource:cell-view/_search", matchAll, ScoobyDoo) { (json2, _) => filterKey("took")(json2) shouldEqual filterKey("took")(json) } .unsafeRunSync() @@ -261,16 +256,17 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "the person resource created with no id in payload should have the default id in _source" in eventually { - deltaClient.post[Json](s"/views/$fullId2/test-resource:people/_search", matchAll, ScoobyDoo) { (json, response) => - response.status shouldEqual StatusCodes.OK - val id = hits(0)._id.string.getOption(json) - val sourceId = hits(0)._source.`@id`.string.getOption(json) - sourceId shouldEqual id + deltaClient.post[Json](s"/views/$project2/test-resource:people/_search", matchAll, ScoobyDoo) { + (json, response) => + response.status shouldEqual StatusCodes.OK + val id = hits(0)._id.string.getOption(json) + val sourceId = hits(0)._source.`@id`.string.getOption(json) + sourceId shouldEqual id } } "get no instance is indexed in cell-view-tagged in project2 as nothing is tagged yet" in eventually { - deltaClient.post[Json](s"/views/$fullId/test-resource:cell-view-tagged/_search", matchAll, ScoobyDoo) { + deltaClient.post[Json](s"/views/$project1/test-resource:cell-view-tagged/_search", matchAll, ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.OK totalHits.getOption(json).value shouldEqual 0 @@ -279,7 +275,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { "search instances on project AggregatedElasticSearchView when logged" in eventually { deltaClient.post[Json]( - s"/views/$fullId2/test-resource:agg-cell-view/_search", + s"/views/$project2/test-resource:agg-cell-view/_search", sortedMatchCells, ScoobyDoo ) { (json, response) => @@ -292,7 +288,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "search instances on project AggregatedElasticSearchView as anonymous" in eventually { - deltaClient.post[Json](s"/views/$fullId2/test-resource:agg-cell-view/_search", sortedMatchCells, Anonymous) { + deltaClient.post[Json](s"/views/$project2/test-resource:agg-cell-view/_search", sortedMatchCells, Anonymous) { (json, response) => response.status shouldEqual StatusCodes.OK val index = hits(0)._index.string.getOption(json).value @@ -302,7 +298,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "fetch statistics for cell-view" in eventually { - deltaClient.get[Json](s"/views/$fullId/test-resource:cell-view/statistics", ScoobyDoo) { (json, response) => + deltaClient.get[Json](s"/views/$project1/test-resource:cell-view/statistics", ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.OK val expected = jsonContentOf( "kg/views/statistics.json", @@ -317,7 +313,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "fetch statistics for cell-view-tagged" in eventually { - deltaClient.get[Json](s"/views/$fullId/test-resource:cell-view-tagged/statistics", ScoobyDoo) { + deltaClient.get[Json](s"/views/$project1/test-resource:cell-view-tagged/statistics", ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.OK val expected = filterNestedKeys("delayInSeconds")( @@ -340,7 +336,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { val id = `@id`.getOption(payload).value val unprefixedId = id.stripPrefix("https://bbp.epfl.ch/nexus/v0/data/bbp/experiment/patchedcell/v0.1.0/") deltaClient.post[Json]( - s"/resources/$fullId/resource/patchedcell:$unprefixedId/tags?rev=1", + s"/resources/$project1/resource/patchedcell:$unprefixedId/tags?rev=1", Json.obj("rev" -> Json.fromInt(1), "tag" -> Json.fromString("one")), ScoobyDoo ) { (_, response) => @@ -350,7 +346,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "get newly tagged instances in cell-view-tagged in project1" in eventually { - deltaClient.post[Json](s"/views/$fullId/test-resource:cell-view-tagged/_search", matchAll, ScoobyDoo) { + deltaClient.post[Json](s"/views/$project1/test-resource:cell-view-tagged/_search", matchAll, ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.OK val total = totalHits.getOption(json).value @@ -359,7 +355,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "get updated statistics for cell-view-tagged" in eventually { - deltaClient.get[Json](s"/views/$fullId/test-resource:cell-view-tagged/statistics", ScoobyDoo) { + deltaClient.get[Json](s"/views/$project1/test-resource:cell-view-tagged/statistics", ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.OK val expected = jsonContentOf( @@ -380,7 +376,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { val unprefixedId = id.stripPrefix("https://bbp.epfl.ch/nexus/v0/data/bbp/experiment/patchedcell/v0.1.0/") deltaClient.put[Json]( - s"/resources/$fullId/_/patchedcell:$unprefixedId?rev=2", + s"/resources/$project1/_/patchedcell:$unprefixedId?rev=2", filterKey("@id")(payload), ScoobyDoo ) { (_, response) => @@ -389,7 +385,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "search instances on project 1 after removed @type" in eventually { - deltaClient.post[Json](s"/views/$fullId/test-resource:cell-view/_search", sortedMatchCells, ScoobyDoo) { + deltaClient.post[Json](s"/views/$project1/test-resource:cell-view/_search", sortedMatchCells, ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.OK val index = hits(0)._index.string.getOption(json).value @@ -397,7 +393,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { jsonContentOf("kg/views/elasticsearch/search-response-no-type.json", "index" -> index) deltaClient - .post[Json](s"/views/$fullId/test-resource:cell-view/_search", matchAll, ScoobyDoo) { (json2, _) => + .post[Json](s"/views/$project1/test-resource:cell-view/_search", matchAll, ScoobyDoo) { (json2, _) => filterKey("took")(json2) shouldEqual filterKey("took")(json) } .unsafeRunSync() @@ -408,13 +404,13 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { val payload = filterKey("@type")(jsonContentOf("kg/views/instances/instance2.json")) val id = payload.asObject.value("@id").value.asString.value val unprefixedId = id.stripPrefix("https://bbp.epfl.ch/nexus/v0/data/bbp/experiment/patchedcell/v0.1.0/") - deltaClient.delete[Json](s"/resources/$fullId/_/patchedcell:$unprefixedId?rev=2", ScoobyDoo) { (_, response) => + deltaClient.delete[Json](s"/resources/$project1/_/patchedcell:$unprefixedId?rev=2", ScoobyDoo) { (_, response) => response.status shouldEqual StatusCodes.OK } } "search instances on project 1 after deprecated" in eventually { - deltaClient.post[Json](s"/views/$fullId/test-resource:cell-view/_search", sortedMatchCells, ScoobyDoo) { + deltaClient.post[Json](s"/views/$project1/test-resource:cell-view/_search", sortedMatchCells, ScoobyDoo) { (json, result) => result.status shouldEqual StatusCodes.OK val index = hits(0)._index.string.getOption(json).value @@ -422,7 +418,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { jsonContentOf("kg/views/elasticsearch/search-response-no-deprecated.json", "index" -> index) deltaClient - .post[Json](s"/views/$fullId/test-resource:cell-view/_search", matchAll, ScoobyDoo) { (json2, _) => + .post[Json](s"/views/$project1/test-resource:cell-view/_search", matchAll, ScoobyDoo) { (json2, _) => filterKey("took")(json2) shouldEqual filterKey("took")(json) } .unsafeRunSync() @@ -430,7 +426,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "restart the view indexing" in eventually { - deltaClient.delete[Json](s"/views/$fullId/test-resource:cell-view/offset", ScoobyDoo) { (json, response) => + deltaClient.delete[Json](s"/views/$project1/test-resource:cell-view/offset", ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.OK val expected = json"""{ "@context" : "https://bluebrain.github.io/nexus/contexts/offset.json", "@type" : "Start" }""" @@ -439,18 +435,18 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "fail to fetch mapping without permission" in { - deltaClient.get[Json](s"/views/$fullId/test-resource:cell-view/_mapping", Anonymous) { expectForbidden } + deltaClient.get[Json](s"/views/$project1/test-resource:cell-view/_mapping", Anonymous) { expectForbidden } } "fail to fetch mapping for view that doesn't exist" in { - deltaClient.get[Json](s"/views/$fullId/test-resource:wrong-view/_mapping", ScoobyDoo) { (json, response) => + deltaClient.get[Json](s"/views/$project1/test-resource:wrong-view/_mapping", ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.NotFound json shouldEqual jsonContentOf( "kg/views/elasticsearch/errors/es-view-not-found.json", replacements( ScoobyDoo, "viewId" -> "https://dev.nexus.test.com/simplified-resource/wrong-view", - "projectRef" -> fullId + "projectRef" -> project1 ): _* ) } @@ -458,7 +454,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { "fail to fetch mapping for aggregate view" in { val view = "test-resource:agg-cell-view" - deltaClient.get[Json](s"/views/$fullId2/$view/_mapping", ScoobyDoo) { (json, response) => + deltaClient.get[Json](s"/views/$project2/$view/_mapping", ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.BadRequest json shouldEqual jsonContentOf( "kg/views/elasticsearch/errors/es-incorrect-view-type.json", @@ -473,7 +469,7 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } "return the view's mapping" in { - deltaClient.get[Json](s"/views/$fullId/test-resource:cell-view/_mapping", ScoobyDoo) { (json, response) => + deltaClient.get[Json](s"/views/$project1/test-resource:cell-view/_mapping", ScoobyDoo) { (json, response) => response.status shouldEqual StatusCodes.OK def hasOnlyOneKey = (j: ACursor) => j.keys.exists(_.size == 1) @@ -486,5 +482,58 @@ class ElasticSearchViewsSpec extends BaseIntegrationSpec { } } + "undeprecate a deprecated view" in { + givenADeprecatedView { view => + val assertUndeprecated = deltaClient.get[Json](s"/views/$project1/$view", ScoobyDoo) { (json, response) => + response.status shouldEqual StatusCodes.OK + json.hcursor.get[Boolean]("_deprecated").toOption should contain(false) + } + undeprecate(view) >> assertUndeprecated + } + } + + "reindex a resource after a view is undeprecated" in { + givenADeprecatedView { view => + givenAPersonResource { _ => + undeprecate(view) >> eventually { assertOneHitIn(view) } + } + } + } + + def givenAView(test: String => IO[Assertion]): IO[Assertion] = { + val viewId = genId() + val viewPayload = jsonContentOf("kg/views/elasticsearch/people-view.json", "withTag" -> false) + val createView = deltaClient.put[Json](s"/views/$project1/$viewId", viewPayload, ScoobyDoo) { expectCreated } + + createView >> test(viewId) + } + + def givenADeprecatedView(test: String => IO[Assertion]): IO[Assertion] = + givenAView { view => + val deprecateView = deltaClient.delete[Json](s"/views/$project1/$view?rev=1", ScoobyDoo) { expectOk } + deprecateView >> test(view) + } + + def givenAPersonResource(test: String => IO[Assertion]): IO[Assertion] = { + val id = genId() + deltaClient.put[Json]( + s"/resources/$project1/_/$id?indexing=sync", + jsonContentOf("kg/resources/person.json"), + ScoobyDoo + ) { (_, response) => + response.status shouldEqual StatusCodes.Created + } >> test(id) + } + + def undeprecate(view: String, rev: Int = 2) = + deltaClient.putEmptyBody[Json](s"/views/$project1/$view/undeprecate?rev=$rev", ScoobyDoo) { expectOk } + + def assertOneHitIn(view: String): IO[Assertion] = + deltaClient.post[Json](s"/views/$project1/$view/_search", json"""{ "query": { "match_all": {} } }""", ScoobyDoo) { + (json, response) => + response.status shouldEqual StatusCodes.OK + totalHits.getOption(json).value shouldEqual 1 + } + } }