From 925007853bb045bb383c6ce3a7e4f5035645eb43 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Thu, 2 Nov 2023 15:33:32 +0100 Subject: [PATCH 01/22] Add FileUndeprecated event --- .../delta/plugins/storage/files/Files.scala | 5 +++ .../storage/files/model/FileEvent.scala | 31 +++++++++++++++++++ .../files/database/file-undeprecated.json | 14 +++++++++ .../files/sse/file-undeprecated.json | 13 ++++++++ .../files/model/FileSerializationSuite.scala | 7 +++++ 5 files changed, 70 insertions(+) create mode 100644 delta/plugins/storage/src/test/resources/files/database/file-undeprecated.json create mode 100644 delta/plugins/storage/src/test/resources/files/sse/file-undeprecated.json diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala index 88247825ba..afd88d512e 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala @@ -604,6 +604,10 @@ object Files { s.copy(rev = e.rev, deprecated = true, updatedAt = e.instant, updatedBy = e.subject) } + def undeprecated(e: FileUndeprecated): Option[FileState] = state.map { s => + s.copy(rev = e.rev, deprecated = false, updatedAt = e.instant, updatedBy = e.subject) + } + event match { case e: FileCreated => created(e) case e: FileUpdated => updated(e) @@ -611,6 +615,7 @@ object Files { case e: FileTagAdded => tagAdded(e) case e: FileTagDeleted => tagDeleted(e) case e: FileDeprecated => deprecated(e) + case e: FileUndeprecated => undeprecated(e) } } diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileEvent.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileEvent.scala index a059cd94d6..d52abb51cf 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileEvent.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileEvent.scala @@ -262,6 +262,34 @@ object FileEvent { subject: Subject ) extends FileEvent + /** + * Event for the undeprecation of a file + * + * @param id + * the file identifier + * @param project + * the project the file belongs to + * @param storage + * the reference to the used storage + * @param storageType + * the type of storage + * @param rev + * the last known revision of the file + * @param instant + * the instant this event was created + * @param subject + * the subject creating this event + */ + final case class FileUndeprecated( + id: Iri, + project: ProjectRef, + storage: ResourceRef.Revision, + storageType: StorageType, + rev: Int, + instant: Instant, + subject: Subject + ) extends FileEvent + @nowarn("cat=unused") val serializer: Serializer[Iri, FileEvent] = { import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Database._ @@ -291,6 +319,7 @@ object FileEvent { case _: FileTagAdded => Tagged case _: FileTagDeleted => TagDeleted case _: FileDeprecated => Deprecated + case _: FileUndeprecated => Undeprecated }, event.id, Set(nxvFile), @@ -427,6 +456,8 @@ object FileEvent { FileExtraFields(ftd.storage.iri, ftd.storageType, None, None, None, None) case fd: FileDeprecated => FileExtraFields(fd.storage.iri, fd.storageType, None, None, None, None) + case fud: FileUndeprecated => + FileExtraFields(fud.storage.iri, fud.storageType, None, None, None, None) } implicit val fileExtraFieldsEncoder: Encoder.AsObject[FileExtraFields] = diff --git a/delta/plugins/storage/src/test/resources/files/database/file-undeprecated.json b/delta/plugins/storage/src/test/resources/files/database/file-undeprecated.json new file mode 100644 index 0000000000..c4d08f704d --- /dev/null +++ b/delta/plugins/storage/src/test/resources/files/database/file-undeprecated.json @@ -0,0 +1,14 @@ +{ + "id" : "https://bluebrain.github.io/nexus/vocabulary/file", + "project" : "myorg/myproj", + "storage": "https://bluebrain.github.io/nexus/vocabulary/disk-storage?rev=1", + "storageType": "DiskStorage", + "rev" : 6, + "instant" : "1970-01-01T00:00:00Z", + "subject" : { + "subject" : "username", + "realm" : "myrealm", + "@type" : "User" + }, + "@type" : "FileUndeprecated" +} diff --git a/delta/plugins/storage/src/test/resources/files/sse/file-undeprecated.json b/delta/plugins/storage/src/test/resources/files/sse/file-undeprecated.json new file mode 100644 index 0000000000..2be9bdfafb --- /dev/null +++ b/delta/plugins/storage/src/test/resources/files/sse/file-undeprecated.json @@ -0,0 +1,13 @@ +{ + "@context": [ + "https://bluebrain.github.io/nexus/contexts/metadata.json", + "https://bluebrain.github.io/nexus/contexts/files.json" + ], + "@type": "FileUndeprecated", + "_fileId": "https://bluebrain.github.io/nexus/vocabulary/file", + "_instant": "1970-01-01T00:00:00Z", + "_project": "http://localhost/v1/projects/myorg/myproj", + "_resourceId": "https://bluebrain.github.io/nexus/vocabulary/file", + "_rev": 6, + "_subject": "http://localhost/v1/realms/myrealm/users/username" +} \ No newline at end of file diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileSerializationSuite.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileSerializationSuite.scala index 545d94cc4a..3882fd09b2 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileSerializationSuite.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileSerializationSuite.scala @@ -55,6 +55,7 @@ class FileSerializationSuite extends SerializationSuite with StorageFixtures { private val tagged = FileTagAdded(fileId, projectRef, storageRef, DiskStorageType, targetRev = 1, tag, 4, instant, subject) private val tagDeleted = FileTagDeleted(fileId, projectRef, storageRef, DiskStorageType, tag, 4, instant, subject) private val deprecated = FileDeprecated(fileId, projectRef, storageRef, DiskStorageType, 5, instant, subject) + private val undeprecated = FileUndeprecated(fileId, projectRef, storageRef, DiskStorageType, 6, instant, subject) // format: on private def expected(event: FileEvent, newFileWritten: Json, bytes: Json, mediaType: Json, origin: Json) = @@ -121,6 +122,12 @@ class FileSerializationSuite extends SerializationSuite with StorageFixtures { loadEvents("files", "file-deprecated.json"), Deprecated, expected(deprecated, Json.Null, Json.Null, Json.Null, Json.Null) + ), + ( + undeprecated, + loadEvents("files", "file-undeprecated.json"), + Undeprecated, + expected(undeprecated, Json.Null, Json.Null, Json.Null, Json.Null) ) ) From ab7c5f047abd167f6f9ef4e4e8a8e493f1e71fbd Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Fri, 3 Nov 2023 14:51:14 +0100 Subject: [PATCH 02/22] Add method to undeprecate files --- .../delta/plugins/storage/files/Files.scala | 29 ++++++++++ .../storage/files/model/FileCommand.scala | 14 +++++ .../storage/files/model/FileRejection.scala | 8 +++ .../plugins/storage/files/FilesSpec.scala | 53 ++++++++++++++++++- 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala index afd88d512e..ded1201861 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala @@ -334,6 +334,26 @@ final class Files( } yield res }.span("deprecateFile") + /** + * Undeprecate an existing file + * + * @param id + * the file identifier to expand as the iri of the file + * @param projectRef + * the project where the file belongs + * @param rev + * the current revision of the file + */ + def undeprecate( + id: FileId, + rev: Int + )(implicit subject: Subject): IO[FileResource] = { + for { + (iri, _) <- id.expandIri(fetchContext.onModify) + res <- eval(UndeprecateFile(iri, id.project, rev, subject)) + } yield res + }.span("undeprecateFile") + /** * Fetch the last version of a file content * @@ -683,6 +703,14 @@ object Files { IOInstant.now.map(FileDeprecated(c.id, c.project, s.storage, s.storageType, s.rev + 1, _, c.subject)) } + def undeprecate(c: UndeprecateFile) = state match { + case None => IO.raiseError(FileNotFound(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(FileIsNotDeprecated(c.id)) + case Some(s) => + IOInstant.now.map(FileUndeprecated(c.id, c.project, s.storage, s.storageType, s.rev + 1, _, c.subject)) + } + cmd match { case c: CreateFile => create(c) case c: UpdateFile => update(c) @@ -690,6 +718,7 @@ object Files { case c: TagFile => tag(c) case c: DeleteFileTag => deleteTag(c) case c: DeprecateFile => deprecate(c) + case c: UndeprecateFile => undeprecate(c) } } diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileCommand.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileCommand.scala index d3c9d4962c..24c383c02c 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileCommand.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileCommand.scala @@ -176,4 +176,18 @@ object FileCommand { * the identity associated to this command */ final case class DeprecateFile(id: Iri, project: ProjectRef, rev: Int, subject: Subject) extends FileCommand + + /** + * Command to undeprecate a file + * + * @param id + * the file identifier + * @param project + * the project the file belongs to + * @param rev + * the last known revision of the file + * @param subject + * the identity associated to this command + */ + final case class UndeprecateFile(id: Iri, project: ProjectRef, rev: Int, subject: Subject) extends FileCommand } diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileRejection.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileRejection.scala index dbcf38d430..1763ee67fb 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileRejection.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/model/FileRejection.scala @@ -130,6 +130,14 @@ object FileRejection { */ final case class FileIsDeprecated(id: Iri) extends FileRejection(s"File '$id' is deprecated.") + /** + * Rejection returned when attempting to undeprecate a file that is already deprecated. + * + * @param id + * the file identifier + */ + final case class FileIsNotDeprecated(id: Iri) extends FileRejection(s"File '$id' is not deprecated.") + /** * Rejection returned when attempting to link a file without providing a filename or a path that ends with a * filename. diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala index e4ddd7f032..d6db5714d5 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala @@ -41,7 +41,7 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.postgres.DoobieScalaTestFixture import ch.epfl.bluebrain.nexus.testkit.remotestorage.RemoteStorageDocker import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsEffectSpec import monix.execution.Scheduler -import org.scalatest.DoNotDiscover +import org.scalatest.{Assertion, DoNotDiscover} import org.scalatest.concurrent.Eventually import java.net.URLDecoder @@ -490,6 +490,41 @@ class FilesSpec(docker: RemoteStorageDocker) } + "undeprecating a file" should { + + "succeed" in { + givenADeprecatedFile { id => + files.undeprecate(id, 2).accepted.deprecated shouldEqual false + } + } + + "reject if file doesn't exists" in { + files.undeprecate(fileId("404"), 1).rejectedWith[FileNotFound] + } + + "reject if file is not deprecated" in { + givenAFile { id => + files.undeprecate(id, 1).assertRejectedWith[FileIsNotDeprecated] + } + } + + "reject if the revision passed is incorrect" in { + givenADeprecatedFile { id => + files.undeprecate(id, 3).assertRejectedEquals(IncorrectRev(3, 2)) + } + } + + "reject if project does not exist" in { + val wrongProject = ProjectRef(org, Label.unsafe("other")) + files.deprecate(FileId(nxv + "id", wrongProject), 1).rejectedWith[ProjectContextRejection] + } + + "reject if project is deprecated" in { + files.undeprecate(FileId(nxv + "id", deprecatedProject.ref), 2).rejectedWith[ProjectContextRejection] + } + + } + "fetching a file" should { val resourceRev1 = mkResource(file1, projectRef, diskRev, attributes("myfile.txt")) val resourceRev4 = mkResource(file1, projectRef, diskRev, attributes(), rev = 4) @@ -583,6 +618,22 @@ class FilesSpec(docker: RemoteStorageDocker) } } + + def givenAFile(assertion: FileId => Assertion): Assertion = { + val filename = genString() + val id = fileId(filename) + files.create(id, Some(diskId), randomEntity(filename, 1), None).accepted + files.fetch(id).accepted + assertion(id) + } + + def givenADeprecatedFile(assertion: FileId => Assertion): Assertion = { + givenAFile { id => + files.deprecate(id, 1).accepted + files.fetch(id).accepted.deprecated shouldEqual true + assertion(id) + } + } } } From 3e6a84710f5363357be9f80121e8638762af591c Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Fri, 3 Nov 2023 14:55:58 +0100 Subject: [PATCH 03/22] Improve `.accepted` Co-authored-by: Daniel Bell <shinyhappydan@users.noreply.github.com> --- .../nexus/testkit/scalatest/ce/CatsIOValues.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsIOValues.scala b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsIOValues.scala index d6fcb2bd43..86992ce627 100644 --- a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsIOValues.scala +++ b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsIOValues.scala @@ -12,8 +12,12 @@ trait CatsIOValues { self: Suite => implicit final class CatsIOValuesOps[A](private val io: IO[A]) { - def accepted: A = - io.unsafeRunTimed(45.seconds).getOrElse(fail("IO timed out during .accepted call")) + def accepted(implicit pos: source.Position): A = { + io.attempt.unsafeRunTimed(45.seconds).getOrElse(fail("IO timed out during .accepted call")) match { + case Left(e) => fail(s"IO failed when it was expected to succeed. Failure details: $e") + case Right(value) => value + } + } def rejected(implicit pos: source.Position): Throwable = rejectedWith[Throwable] From bfe30b6e9f9f97113d60d0a10e5de246ad3d1846 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:06:19 +0100 Subject: [PATCH 04/22] Improve FileSpec undeprecation tests --- .../nexus/delta/plugins/storage/files/FilesSpec.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala index d6db5714d5..e866bd4a1c 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesSpec.scala @@ -495,6 +495,7 @@ class FilesSpec(docker: RemoteStorageDocker) "succeed" in { givenADeprecatedFile { id => files.undeprecate(id, 2).accepted.deprecated shouldEqual false + assertActive(id) } } @@ -505,12 +506,14 @@ class FilesSpec(docker: RemoteStorageDocker) "reject if file is not deprecated" in { givenAFile { id => files.undeprecate(id, 1).assertRejectedWith[FileIsNotDeprecated] + assertRemainsActive(id) } } "reject if the revision passed is incorrect" in { givenADeprecatedFile { id => files.undeprecate(id, 3).assertRejectedEquals(IncorrectRev(3, 2)) + assertRemainsDeprecated(id) } } @@ -634,6 +637,13 @@ class FilesSpec(docker: RemoteStorageDocker) assertion(id) } } + + def assertRemainsDeprecated(id: FileId): Assertion = + files.fetch(id).accepted.deprecated shouldEqual true + def assertActive(id: FileId): Assertion = + files.fetch(id).accepted.deprecated shouldEqual false + def assertRemainsActive(id: FileId): Assertion = + assertActive(id) } } From e6dc8b8f179bdf2e08bcffe5fe6be6ba47f92fcc Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Fri, 3 Nov 2023 16:06:31 +0100 Subject: [PATCH 05/22] Make FileRoutesSpec independent w.r.t. permissions --- .../files/routes/FilesRoutesSpec.scala | 183 +++++++++--------- .../nexus/delta/sdk/utils/RouteFixtures.scala | 1 + 2 files changed, 95 insertions(+), 89 deletions(-) diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala index 58e25371ee..7710c60afa 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala @@ -11,7 +11,6 @@ import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.kernel.http.MediaTypeDetectorConfig import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.Digest.ComputedDigest import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{FileAttributes, FileId, FileRejection} -import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.FilesRoutesSpec.fileMetadata import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.{contexts => fileContexts, permissions, FileFixtures, Files, FilesConfig} import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.{StorageRejection, StorageStatEntry, StorageType} import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.operations.remote.client.RemoteDiskStorageClient @@ -27,19 +26,18 @@ import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclAddress import ch.epfl.bluebrain.nexus.delta.sdk.auth.{AuthTokenProvider, Credentials} import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaSchemeDirectives import ch.epfl.bluebrain.nexus.delta.sdk.http.HttpClient -import ch.epfl.bluebrain.nexus.delta.sdk.identities.{Identities, IdentitiesDummy} import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.{Caller, ServiceAccount} +import ch.epfl.bluebrain.nexus.delta.sdk.identities.{Identities, IdentitiesDummy} import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceUris} import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.events import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution -import ch.epfl.bluebrain.nexus.delta.sdk.utils.{BaseRouteSpec, RouteFixtures} +import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject, User} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef} -import ch.epfl.bluebrain.nexus.testkit._ import ch.epfl.bluebrain.nexus.testkit.bio.IOFromMap import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsIOValues import io.circe.Json @@ -71,11 +69,21 @@ class FilesRoutesSpec Vocabulary.contexts.search -> ContextValue.fromFile("contexts/search.json") ) - implicit private val caller: Caller = - Caller(alice, Set(alice, Anonymous, Authenticated(realm), Group("group", realm))) - private val aliceIdentities = IdentitiesDummy(caller) + private val reader = alice + private val writer = bob + private val s3writer = charlie + + 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))) + implicit private val callerS3Writer: Caller = + Caller(s3writer, Set(s3writer, Anonymous, Authenticated(realm), Group("group", realm))) + private val identities = IdentitiesDummy(callerReader, callerWriter, callerS3Writer) - private val asAlice = addCredentials(OAuth2BearerToken("alice")) + private val asReader = addCredentials(OAuth2BearerToken("alice")) + private val asWriter = addCredentials(OAuth2BearerToken("bob")) + private val asS3Writer = addCredentials(OAuth2BearerToken("charlie")) private val fetchContext = FetchContextDummy(Map(project.ref -> project.context)) @@ -125,7 +133,7 @@ class FilesRoutesSpec private val groupDirectives = DeltaSchemeDirectives(fetchContext, ioFromMap(uuid -> projectRef.organization), ioFromMap(uuid -> projectRef)) - private lazy val routes = routesWithIdentities(aliceIdentities) + private lazy val routes = routesWithIdentities(identities) private def routesWithIdentities(identities: Identities) = Route.seal(FilesRoutes(stCfg, identities, aclCheck, files, groupDirectives, IndexingAction.noop)) @@ -135,23 +143,33 @@ class FilesRoutesSpec private val varyHeader = RawHeader("Vary", "Accept,Accept-Encoding") - "File routes" should { + override def beforeAll(): Unit = { + super.beforeAll() + // format: off +// aclCheck.append(AclAddress.Root, writer -> Set(storagesPermissions.write), callerWriter.subject -> Set(storagesPermissions.write)).accepted +// aclCheck.append(AclAddress.Root, writer -> Set(diskWrite), callerWriter.subject -> Set(diskWrite)).accepted +// aclCheck.append(AclAddress.Root, writer -> Set(diskRead), callerWriter.subject -> Set(diskRead)).accepted +// aclCheck.append(AclAddress.Root, writer -> Set(permissions.write), callerWriter.subject -> Set(permissions.write)).accepted +// aclCheck.append(AclAddress.Root, writer -> Set(permissions.read), callerWriter.subject -> Set(permissions.read)).accepted + + val writePermissions = Set(storagesPermissions.write, diskWrite, permissions.write) + val readPermissions = Set(diskRead, s3Read, permissions.read) + aclCheck.append(AclAddress.Root, writer -> writePermissions, writer -> readPermissions).accepted + aclCheck.append(AclAddress.Root, callerWriter.subject -> writePermissions).accepted + aclCheck.append(AclAddress.Root, reader -> readPermissions).accepted + aclCheck.append(AclAddress.Root, s3writer -> Set(s3Write), callerS3Writer.subject -> Set(s3Write)).accepted + // format: on + + val defaults = json"""{"maxFileSize": 1000, "volume": "$path"}""" + val s3Perms = json"""{"readPermission": "$s3Read", "writePermission": "$s3Write"}""" + storages.create(s3Id, projectRef, diskFieldsJson deepMerge defaults deepMerge s3Perms)(callerWriter).accepted + storages + .create(dId, projectRef, diskFieldsJson deepMerge defaults deepMerge json"""{"capacity":5000}""")(callerWriter) + .void + .accepted + } - "create storages for files" in { - val defaults = json"""{"maxFileSize": 1000, "volume": "$path"}""" - val s3Perms = json"""{"readPermission": "$s3Read", "writePermission": "$s3Write"}""" - aclCheck - .append( - AclAddress.Root, - Anonymous -> Set(storagesPermissions.write), - caller.subject -> Set(storagesPermissions.write) - ) - .accepted - storages.create(s3Id, projectRef, diskFieldsJson deepMerge defaults deepMerge s3Perms).accepted - storages - .create(dId, projectRef, diskFieldsJson deepMerge defaults deepMerge json"""{"capacity":5000}""") - .accepted - } + "File routes" should { "fail to create a file without disk/write permission" in { Post("/v1/files/org/proj", entity()) ~> routes ~> check { @@ -160,8 +178,7 @@ class FilesRoutesSpec } "create a file" in { - aclCheck.append(AclAddress.Root, Anonymous -> Set(diskWrite), caller.subject -> Set(diskWrite)).accepted - Post("/v1/files/org/proj", entity()) ~> routes ~> check { + Post("/v1/files/org/proj", entity()) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Created val attr = attributes() response.asJson shouldEqual fileMetadata(projectRef, generatedId, attr, diskIdRev) @@ -170,7 +187,7 @@ class FilesRoutesSpec "create and tag a file" in { withUUIDF(uuid2) { - Post("/v1/files/org/proj?tag=mytag", entity()) ~> routes ~> check { + Post("/v1/files/org/proj?tag=mytag", entity()) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Created val attr = attributes(id = uuid2) val expected = fileMetadata(projectRef, generatedId2, attr, diskIdRev) @@ -183,7 +200,7 @@ class FilesRoutesSpec "fail to create a file link using a storage that does not allow it" in { val payload = json"""{"filename": "my.txt", "path": "my/file.txt", "mediaType": "text/plain"}""" - Put("/v1/files/org/proj/file1", payload.toEntity) ~> routes ~> check { + Put("/v1/files/org/proj/file1", payload.toEntity) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.BadRequest response.asJson shouldEqual jsonContentOf("files/errors/unsupported-operation.json", "id" -> file1, "storageId" -> dId) @@ -191,30 +208,29 @@ class FilesRoutesSpec } "fail to create a file without s3/write permission" in { - Put("/v1/files/org/proj/file1?storage=s3-storage", entity()) ~> routes ~> check { + Put("/v1/files/org/proj/file1?storage=s3-storage", entity()) ~> asWriter ~> routes ~> check { response.shouldBeForbidden } } - "create a file with an authenticated user and provided id" in { - aclCheck.append(AclAddress.Root, Anonymous -> Set(s3Write), caller.subject -> Set(s3Write)).accepted - Put("/v1/files/org/proj/file1?storage=s3-storage", entity("file2.txt")) ~> asAlice ~> routes ~> check { + "create a file on s3 with an authenticated user and provided id" in { + Put("/v1/files/org/proj/file1?storage=s3-storage", entity("file2.txt")) ~> asS3Writer ~> routes ~> check { status shouldEqual StatusCodes.Created val attr = attributes("file2.txt") response.asJson shouldEqual - fileMetadata(projectRef, file1, attr, s3IdRev, createdBy = alice, updatedBy = alice) + fileMetadata(projectRef, file1, attr, s3IdRev, createdBy = s3writer, updatedBy = s3writer) } } - "create and tag a file with an authenticated user and provided id" in { + "create and tag a file on s3 with an authenticated user and provided id" in { withUUIDF(uuid2) { Put( "/v1/files/org/proj/fileTagged?storage=s3-storage&tag=mytag", entity("fileTagged.txt") - ) ~> asAlice ~> routes ~> check { + ) ~> asS3Writer ~> routes ~> check { status shouldEqual StatusCodes.Created val attr = attributes("fileTagged.txt", id = uuid2) - val expected = fileMetadata(projectRef, fileTagged, attr, s3IdRev, createdBy = alice, updatedBy = alice) + val expected = fileMetadata(projectRef, fileTagged, attr, s3IdRev, createdBy = s3writer, updatedBy = s3writer) val fileByTag = files.fetch(FileId(generatedId2, tag, projectRef)).accepted response.asJson shouldEqual expected fileByTag.value.tags.tags should contain(tag) @@ -223,21 +239,24 @@ class FilesRoutesSpec } "reject the creation of a file which already exists" in { - Put("/v1/files/org/proj/file1", entity()) ~> routes ~> check { + Put("/v1/files/org/proj/file1", entity()) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Conflict response.asJson shouldEqual jsonContentOf("/files/errors/already-exists.json", "id" -> file1) } } "reject the creation of a file that is too large" in { - Put("/v1/files/org/proj/file-too-large", randomEntity(filename = "large-file.txt", 1100)) ~> routes ~> check { + Put( + "/v1/files/org/proj/file-too-large", + randomEntity(filename = "large-file.txt", 1100) + ) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.PayloadTooLarge response.asJson shouldEqual jsonContentOf("/files/errors/file-too-large.json") } } "reject the creation of a file to a storage that does not exist" in { - Put("/v1/files/org/proj/file2?storage=not-exist", entity()) ~> routes ~> check { + Put("/v1/files/org/proj/file2?storage=not-exist", entity()) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.NotFound response.asJson shouldEqual jsonContentOf("/storages/errors/not-found.json", "id" -> (nxv + "not-exist"), "proj" -> projectRef) @@ -245,48 +264,41 @@ class FilesRoutesSpec } "fail to update a file without disk/write permission" in { - aclCheck.subtract(AclAddress.Root, Anonymous -> Set(diskWrite)).accepted Put(s"/v1/files/org/proj/file1?rev=1", s3FieldsJson.toEntity) ~> routes ~> check { response.shouldBeForbidden } } "update a file" in { - aclCheck.append(AclAddress.Root, Anonymous -> Set(diskWrite)).accepted val endpoints = List( "/v1/files/org/proj/file1", s"/v1/files/org/proj/$file1Encoded" ) forAll(endpoints.zipWithIndex) { case (endpoint, idx) => val filename = s"file-idx-$idx.txt" - Put(s"$endpoint?rev=${idx + 1}", entity(filename)) ~> routes ~> check { + Put(s"$endpoint?rev=${idx + 1}", entity(filename)) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.OK val attr = attributes(filename) response.asJson shouldEqual - fileMetadata(projectRef, file1, attr, diskIdRev, rev = idx + 2, createdBy = alice) + fileMetadata(projectRef, file1, attr, diskIdRev, rev = idx + 2, createdBy = s3writer) } } } "update and tag a file in one request" in { - givenRoutesForUserWithPermissions(Set(diskRead, diskWrite)) { (user, route) => - val token = addCredentials(OAuth2BearerToken(user.subject)) - - givenAFile { id => - Put(s"/v1/files/org/proj/$id?rev=1&tag=mytag", entity(s"$id.txt")) ~> token ~> route ~> check { - status shouldEqual StatusCodes.OK - } - - Get(s"/v1/files/org/proj/$id?tag=mytag") ~> Accept(`*/*`) ~> token ~> route ~> check { - status shouldEqual StatusCodes.OK - } + givenAFile { id => + Put(s"/v1/files/org/proj/$id?rev=1&tag=mytag", entity(s"$id.txt")) ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.OK + } + Get(s"/v1/files/org/proj/$id?tag=mytag") ~> Accept(`*/*`) ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.OK } } } "fail to update a file link using a storage that does not allow it" in { val payload = json"""{"filename": "my.txt", "path": "my/file.txt", "mediaType": "text/plain"}""" - Put("/v1/files/org/proj/file1?rev=3", payload.toEntity) ~> routes ~> check { + Put("/v1/files/org/proj/file1?rev=3", payload.toEntity) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.BadRequest response.asJson shouldEqual jsonContentOf("files/errors/unsupported-operation.json", "id" -> file1, "storageId" -> dId) @@ -294,7 +306,7 @@ class FilesRoutesSpec } "reject the update of a non-existent file" in { - Put("/v1/files/org/proj/myid10?rev=1", entity("other.txt")) ~> routes ~> check { + Put("/v1/files/org/proj/myid10?rev=1", entity("other.txt")) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.NotFound response.asJson shouldEqual jsonContentOf("/files/errors/not-found.json", "id" -> (nxv + "myid10"), "proj" -> "org/proj") @@ -302,7 +314,7 @@ class FilesRoutesSpec } "reject the update of a non-existent file storage" in { - Put("/v1/files/org/proj/file1?rev=3&storage=not-exist", entity("other.txt")) ~> routes ~> check { + Put("/v1/files/org/proj/file1?rev=3&storage=not-exist", entity("other.txt")) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.NotFound response.asJson shouldEqual jsonContentOf("/storages/errors/not-found.json", "id" -> (nxv + "not-exist"), "proj" -> projectRef) @@ -310,7 +322,7 @@ class FilesRoutesSpec } "reject the update of a file at a non-existent revision" in { - Put("/v1/files/org/proj/file1?rev=10", entity("other.txt")) ~> routes ~> check { + Put("/v1/files/org/proj/file1?rev=10", entity("other.txt")) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Conflict response.asJson shouldEqual jsonContentOf("/files/errors/incorrect-rev.json", "provided" -> 10, "expected" -> 3) @@ -324,8 +336,7 @@ class FilesRoutesSpec } "deprecate a file" in { - aclCheck.append(AclAddress.Root, Anonymous -> Set(permissions.write)).accepted - Delete(s"/v1/files/org/proj/$uuid?rev=1") ~> routes ~> check { + Delete(s"/v1/files/org/proj/$uuid?rev=1") ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.OK val attr = attributes() response.asJson shouldEqual fileMetadata(projectRef, generatedId, attr, diskIdRev, rev = 2, deprecated = true) @@ -333,14 +344,14 @@ class FilesRoutesSpec } "reject the deprecation of a file without rev" in { - Delete("/v1/files/org/proj/file1") ~> routes ~> check { + Delete("/v1/files/org/proj/file1") ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.BadRequest response.asJson shouldEqual jsonContentOf("/errors/missing-query-param.json", "field" -> "rev") } } "reject the deprecation of an already deprecated file" in { - Delete(s"/v1/files/org/proj/$uuid?rev=2") ~> routes ~> check { + Delete(s"/v1/files/org/proj/$uuid?rev=2") ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.BadRequest response.asJson shouldEqual jsonContentOf("/files/errors/file-deprecated.json", "id" -> generatedId) } @@ -348,10 +359,10 @@ class FilesRoutesSpec "tag a file" in { val payload = json"""{"tag": "mytag", "rev": 1}""" - Post("/v1/files/org/proj/file1/tags?rev=3", payload.toEntity) ~> routes ~> check { + Post("/v1/files/org/proj/file1/tags?rev=3", payload.toEntity) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Created val attr = attributes("file-idx-1.txt") - response.asJson shouldEqual fileMetadata(projectRef, file1, attr, diskIdRev, rev = 4, createdBy = alice) + response.asJson shouldEqual fileMetadata(projectRef, file1, attr, diskIdRev, rev = 4, createdBy = charlie) } } @@ -365,9 +376,8 @@ class FilesRoutesSpec } "fail to fetch a file when the accept header does not match file media type" in { - aclCheck.append(AclAddress.Root, Anonymous -> Set(diskRead, s3Read)).accepted forAll(List("", "?rev=1", "?tags=mytag")) { suffix => - Get(s"/v1/files/org/proj/file1$suffix") ~> Accept(`video/*`) ~> routes ~> check { + Get(s"/v1/files/org/proj/file1$suffix") ~> Accept(`video/*`) ~> asReader ~> routes ~> check { response.status shouldEqual StatusCodes.NotAcceptable response.asJson shouldEqual jsonContentOf("errors/content-type.json", "expected" -> "text/plain") response.headers should not contain varyHeader @@ -380,7 +390,7 @@ class FilesRoutesSpec forAll( List("/v1/files/org/proj/file1", "/v1/resources/org/proj/_/file1", "/v1/resources/org/proj/file/file1") ) { endpoint => - Get(endpoint) ~> accept ~> routes ~> check { + Get(endpoint) ~> accept ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK contentType.value shouldEqual `text/plain(UTF-8)`.value val filename64 = "ZmlsZS1pZHgtMS50eHQ=" // file-idx-1.txt @@ -407,7 +417,7 @@ class FilesRoutesSpec ) forAll(endpoints) { endpoint => forAll(List("rev=1", "tag=mytag")) { param => - Get(s"$endpoint?$param") ~> Accept(`*/*`) ~> routes ~> check { + Get(s"$endpoint?$param") ~> Accept(`*/*`) ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK contentType.value shouldEqual `text/plain(UTF-8)`.value val filename64 = "ZmlsZTIudHh0" // file2.txt @@ -434,11 +444,10 @@ class FilesRoutesSpec } "fetch a file metadata" in { - aclCheck.append(AclAddress.Root, Anonymous -> Set(permissions.read)).accepted - Get("/v1/files/org/proj/file1") ~> Accept(`application/ld+json`) ~> routes ~> check { + Get("/v1/files/org/proj/file1") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK val attr = attributes("file-idx-1.txt") - response.asJson shouldEqual fileMetadata(projectRef, file1, attr, diskIdRev, rev = 4, createdBy = alice) + response.asJson shouldEqual fileMetadata(projectRef, file1, attr, diskIdRev, rev = 4, createdBy = s3writer) response.headers should contain(varyHeader) } } @@ -456,10 +465,10 @@ class FilesRoutesSpec ) forAll(endpoints) { endpoint => forAll(List("rev=1", "tag=mytag")) { param => - Get(s"$endpoint?$param") ~> Accept(`application/ld+json`) ~> routes ~> check { + Get(s"$endpoint?$param") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual - fileMetadata(projectRef, file1, attr, s3IdRev, createdBy = alice, updatedBy = alice) + fileMetadata(projectRef, file1, attr, s3IdRev, createdBy = s3writer, updatedBy = s3writer) response.headers should contain(varyHeader) } } @@ -467,47 +476,47 @@ class FilesRoutesSpec } "fetch the file tags" in { - Get("/v1/resources/org/proj/_/file1/tags?rev=1") ~> routes ~> check { + Get("/v1/resources/org/proj/_/file1/tags?rev=1") ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual json"""{"tags": []}""".addContext(contexts.tags) } - Get("/v1/files/org/proj/file1/tags") ~> routes ~> check { + Get("/v1/files/org/proj/file1/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/files/org/proj/file1?tag=myother") ~> Accept(`application/ld+json`) ~> routes ~> check { + Get("/v1/files/org/proj/file1?tag=myother") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.NotFound response.asJson shouldEqual jsonContentOf("/errors/tag-not-found.json", "tag" -> "myother") } } "reject if provided rev and tag simultaneously" in { - Get("/v1/files/org/proj/file1?tag=mytag&rev=1") ~> Accept(`application/ld+json`) ~> routes ~> check { + Get("/v1/files/org/proj/file1?tag=mytag&rev=1") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.BadRequest response.asJson shouldEqual jsonContentOf("/errors/tag-and-rev-error.json") } } "delete a tag on file" in { - Delete("/v1/files/org/proj/file1/tags/mytag?rev=4") ~> routes ~> check { + Delete("/v1/files/org/proj/file1/tags/mytag?rev=4") ~> asWriter ~> routes ~> check { val attr = attributes("file-idx-1.txt") status shouldEqual StatusCodes.OK - response.asJson shouldEqual fileMetadata(projectRef, file1, attr, diskIdRev, rev = 5, createdBy = alice) + response.asJson shouldEqual fileMetadata(projectRef, file1, attr, diskIdRev, rev = 5, createdBy = s3writer) } } "not return the deleted tag" in { - Get("/v1/files/org/proj/file1/tags") ~> routes ~> check { + Get("/v1/files/org/proj/file1/tags") ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual json"""{"tags": []}""".addContext(contexts.tags) } } "fail to fetch file by the deleted tag" in { - Get("/v1/files/org/proj/file1?tag=mytag") ~> Accept(`application/ld+json`) ~> routes ~> check { + Get("/v1/files/org/proj/file1?tag=mytag") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.NotFound response.asJson shouldEqual jsonContentOf("/errors/tag-not-found.json", "tag" -> "mytag") } @@ -523,7 +532,7 @@ class FilesRoutesSpec def givenAFile(test: String => Assertion): Assertion = { val id = genString() - Put(s"/v1/files/org/proj/$id", entity(s"${genString()}.txt")) ~> routes ~> check { + Put(s"/v1/files/org/proj/$id", entity(s"${genString()}.txt")) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Created } test(id) @@ -544,9 +553,6 @@ class FilesRoutesSpec aclCheck.append(AclAddress.Root, c.subject -> perms).accepted test(user, c) } -} - -object FilesRoutesSpec extends TestHelpers with RouteFixtures { def fileMetadata( project: ProjectRef, @@ -556,8 +562,8 @@ object FilesRoutesSpec extends TestHelpers with RouteFixtures { storageType: StorageType = StorageType.DiskStorage, rev: Int = 1, deprecated: Boolean = false, - createdBy: Subject = Anonymous, - updatedBy: Subject = Anonymous + createdBy: Subject = callerWriter.subject, + updatedBy: Subject = callerWriter.subject )(implicit baseUri: BaseUri): Json = jsonContentOf( "files/file-route-metadata-response.json", @@ -580,5 +586,4 @@ object FilesRoutesSpec extends TestHelpers with RouteFixtures { "type" -> storageType, "self" -> ResourceUris("files", project, id).accessUri ) - } diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/utils/RouteFixtures.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/utils/RouteFixtures.scala index e4a2d40c75..81cd9c8d7e 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/utils/RouteFixtures.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/utils/RouteFixtures.scala @@ -65,4 +65,5 @@ trait RouteFixtures { val realm: Label = Label.unsafe("wonderland") val alice: User = User("alice", realm) val bob: User = User("bob", realm) + val charlie: User = User("charlie", realm) } From 216f93172b6b704359a654ba46b119a4e12eaed1 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Fri, 3 Nov 2023 16:10:49 +0100 Subject: [PATCH 06/22] Use non-confusing names --- .../files/routes/FilesRoutesSpec.scala | 38 ++++++++----------- .../nexus/delta/sdk/utils/RouteFixtures.scala | 1 - 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala index 7710c60afa..918ff3b464 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala @@ -69,9 +69,9 @@ class FilesRoutesSpec Vocabulary.contexts.search -> ContextValue.fromFile("contexts/search.json") ) - private val reader = alice - private val writer = bob - private val s3writer = charlie + private val reader = User("reader", realm) + private val writer = User("writer", realm) + private val s3writer = User("s3writer", realm) implicit private val callerReader: Caller = Caller(reader, Set(reader, Anonymous, Authenticated(realm), Group("group", realm))) @@ -81,17 +81,18 @@ class FilesRoutesSpec Caller(s3writer, Set(s3writer, Anonymous, Authenticated(realm), Group("group", realm))) private val identities = IdentitiesDummy(callerReader, callerWriter, callerS3Writer) - private val asReader = addCredentials(OAuth2BearerToken("alice")) - private val asWriter = addCredentials(OAuth2BearerToken("bob")) - private val asS3Writer = addCredentials(OAuth2BearerToken("charlie")) + private val asReader = addCredentials(OAuth2BearerToken("reader")) + private val asWriter = addCredentials(OAuth2BearerToken("writer")) + private val asS3Writer = addCredentials(OAuth2BearerToken("s3writer")) private val fetchContext = FetchContextDummy(Map(project.ref -> project.context)) - private val s3Read = Permission.unsafe("s3/read") - private val s3Write = Permission.unsafe("s3/write") - private val diskRead = Permission.unsafe("disk/read") - private val diskWrite = Permission.unsafe("disk/write") - override val allowedPerms = + private val s3Read = Permission.unsafe("s3/read") + private val s3Write = Permission.unsafe("s3/write") + private val diskRead = Permission.unsafe("disk/read") + private val diskWrite = Permission.unsafe("disk/write") + + override val allowedPerms: Seq[Permission] = Seq( permissions.read, permissions.write, @@ -145,23 +146,16 @@ class FilesRoutesSpec override def beforeAll(): Unit = { super.beforeAll() - // format: off -// aclCheck.append(AclAddress.Root, writer -> Set(storagesPermissions.write), callerWriter.subject -> Set(storagesPermissions.write)).accepted -// aclCheck.append(AclAddress.Root, writer -> Set(diskWrite), callerWriter.subject -> Set(diskWrite)).accepted -// aclCheck.append(AclAddress.Root, writer -> Set(diskRead), callerWriter.subject -> Set(diskRead)).accepted -// aclCheck.append(AclAddress.Root, writer -> Set(permissions.write), callerWriter.subject -> Set(permissions.write)).accepted -// aclCheck.append(AclAddress.Root, writer -> Set(permissions.read), callerWriter.subject -> Set(permissions.read)).accepted val writePermissions = Set(storagesPermissions.write, diskWrite, permissions.write) - val readPermissions = Set(diskRead, s3Read, permissions.read) + val readPermissions = Set(diskRead, s3Read, permissions.read) aclCheck.append(AclAddress.Root, writer -> writePermissions, writer -> readPermissions).accepted aclCheck.append(AclAddress.Root, callerWriter.subject -> writePermissions).accepted aclCheck.append(AclAddress.Root, reader -> readPermissions).accepted aclCheck.append(AclAddress.Root, s3writer -> Set(s3Write), callerS3Writer.subject -> Set(s3Write)).accepted - // format: on - val defaults = json"""{"maxFileSize": 1000, "volume": "$path"}""" - val s3Perms = json"""{"readPermission": "$s3Read", "writePermission": "$s3Write"}""" + val defaults = json"""{"maxFileSize": 1000, "volume": "$path"}""" + val s3Perms = json"""{"readPermission": "$s3Read", "writePermission": "$s3Write"}""" storages.create(s3Id, projectRef, diskFieldsJson deepMerge defaults deepMerge s3Perms)(callerWriter).accepted storages .create(dId, projectRef, diskFieldsJson deepMerge defaults deepMerge json"""{"capacity":5000}""")(callerWriter) @@ -362,7 +356,7 @@ class FilesRoutesSpec Post("/v1/files/org/proj/file1/tags?rev=3", payload.toEntity) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Created val attr = attributes("file-idx-1.txt") - response.asJson shouldEqual fileMetadata(projectRef, file1, attr, diskIdRev, rev = 4, createdBy = charlie) + response.asJson shouldEqual fileMetadata(projectRef, file1, attr, diskIdRev, rev = 4, createdBy = s3writer) } } diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/utils/RouteFixtures.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/utils/RouteFixtures.scala index 81cd9c8d7e..e4a2d40c75 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/utils/RouteFixtures.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/utils/RouteFixtures.scala @@ -65,5 +65,4 @@ trait RouteFixtures { val realm: Label = Label.unsafe("wonderland") val alice: User = User("alice", realm) val bob: User = User("bob", realm) - val charlie: User = User("charlie", realm) } From c89bf3dd13bc2d8996488a8b7cf4615300476975 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Fri, 3 Nov 2023 16:37:17 +0100 Subject: [PATCH 07/22] Add undeprecate endpoint and test it --- .../storage/files/routes/FilesRoutes.scala | 13 +++- .../errors/file-is-not-deprecated.json | 5 ++ .../files/routes/FilesRoutesSpec.scala | 62 ++++++++++++++----- .../testkit/scalatest/ce/CatsIOValues.scala | 2 +- 4 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 delta/plugins/storage/src/test/resources/errors/file-is-not-deprecated.json diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala index 519bae3a4e..3b74470b49 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutes.scala @@ -126,7 +126,6 @@ final class FilesRoutes( }, // Create a file with id segment extractRequestEntity { entity => - println(s"DTBDTB or only here?") emit( Created, files.create(fileId, storage, entity, tag).index(mode).attemptNarrow[FileRejection] @@ -168,6 +167,7 @@ final class FilesRoutes( ) } }, + // Fetch a file (get & idSegmentRef(id)) { id => emitOrFusionRedirect(ref, id, fetch(FileId(id, ref))) @@ -213,6 +213,17 @@ final class FilesRoutes( } ) } + }, + (pathPrefix("undeprecate") & put & parameter("rev".as[Int])) { rev => + authorizeFor(ref, Write).apply { + emit( + files + .undeprecate(fileId, rev) + .index(mode) + .attemptNarrow[FileRejection] + .rejectOn[FileNotFound] + ) + } } ) } diff --git a/delta/plugins/storage/src/test/resources/errors/file-is-not-deprecated.json b/delta/plugins/storage/src/test/resources/errors/file-is-not-deprecated.json new file mode 100644 index 0000000000..2a749bfbc7 --- /dev/null +++ b/delta/plugins/storage/src/test/resources/errors/file-is-not-deprecated.json @@ -0,0 +1,5 @@ +{ + "@context" : "https://bluebrain.github.io/nexus/contexts/error.json", + "@type" : "FileIsNotDeprecated", + "reason" : "File '{{id}}' is not deprecated." +} \ No newline at end of file diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala index 918ff3b464..6060b5ce44 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala @@ -351,6 +351,46 @@ class FilesRoutesSpec } } + "fail to undeprecate a file without files/write permission" in { + givenADeprecatedFile { id => + Put(s"/v1/files/org/proj/$id/undeprecate?rev=2") ~> asReader ~> routes ~> check { + response.shouldBeForbidden + } + } + } + + "undeprecate a file" in { + givenADeprecatedFile { id => + Put(s"/v1/files/org/proj/$id/undeprecate?rev=2") ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.OK + response.asJson shouldEqual + fileMetadata(projectRef, nxv + id, attributes(id), diskIdRev, rev = 3, deprecated = false) + + Get(s"/v1/files/org/proj/$id") ~> Accept(`*/*`) ~> asReader ~> routes ~> check { + status shouldEqual StatusCodes.OK + } + } + } + } + + "reject the undeprecation of a file without rev" in { + givenADeprecatedFile { id => + Put(s"/v1/files/org/proj/$id/undeprecate") ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.BadRequest + response.asJson shouldEqual jsonContentOf("/errors/missing-query-param.json", "field" -> "rev") + } + } + } + + "reject the undeprecation of a file that is not deprecated" in { + givenAFile { id => + Put(s"/v1/files/org/proj/$id/undeprecate?rev=1") ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.BadRequest + response.asJson shouldEqual jsonContentOf("/errors/file-is-not-deprecated.json", "id" -> (nxv + id)) + } + } + } + "tag a file" in { val payload = json"""{"tag": "mytag", "rev": 1}""" Post("/v1/files/org/proj/file1/tags?rev=3", payload.toEntity) ~> asWriter ~> routes ~> check { @@ -526,28 +566,20 @@ class FilesRoutesSpec def givenAFile(test: String => Assertion): Assertion = { val id = genString() - Put(s"/v1/files/org/proj/$id", entity(s"${genString()}.txt")) ~> asWriter ~> routes ~> check { + Put(s"/v1/files/org/proj/$id", entity(s"$id")) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Created } test(id) } - def givenRoutesForUserWithPermissions( - perms: Set[Permission] - )(test: (User, Route) => Assertion): Assertion = - givenAUserWithPermissions(perms) { (user, caller) => - val authedRoutes = routesWithIdentities(IdentitiesDummy(caller)) - test(user, authedRoutes) + def givenADeprecatedFile(test: String => Assertion): Assertion = + givenAFile { id => + Delete(s"/v1/files/org/proj/$id?rev=1") ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.OK + } + test(id) } - def givenAUserWithPermissions(perms: Set[Permission])(test: (User, Caller) => Assertion): Assertion = { - val userId = genString() - val user: User = User(userId, realm) - val c: Caller = Caller(user, Set(user, Anonymous, Authenticated(realm), Group("group", realm))) - aclCheck.append(AclAddress.Root, c.subject -> perms).accepted - test(user, c) - } - def fileMetadata( project: ProjectRef, id: Iri, diff --git a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsIOValues.scala b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsIOValues.scala index 86992ce627..155742eb7f 100644 --- a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsIOValues.scala +++ b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsIOValues.scala @@ -14,7 +14,7 @@ trait CatsIOValues { implicit final class CatsIOValuesOps[A](private val io: IO[A]) { def accepted(implicit pos: source.Position): A = { io.attempt.unsafeRunTimed(45.seconds).getOrElse(fail("IO timed out during .accepted call")) match { - case Left(e) => fail(s"IO failed when it was expected to succeed. Failure details: $e") + case Left(e) => fail(s"IO failed when it was expected to succeed. Failure details: $e") case Right(value) => value } } From 2d56da31c10586c0eb09e975e0fdcae3914c4a59 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Fri, 3 Nov 2023 17:46:57 +0100 Subject: [PATCH 08/22] Make FilesRoutesSpec tests independent --- .../plugins/storage/files/FileFixtures.scala | 8 +- .../files/routes/FilesRoutesSpec.scala | 377 +++++++++++------- 2 files changed, 231 insertions(+), 154 deletions(-) diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FileFixtures.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FileFixtures.scala index b0bd6117b7..9efb9fafc2 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FileFixtures.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FileFixtures.scala @@ -19,7 +19,7 @@ import monix.bio.Task import org.scalatest.Suite import java.nio.file.{Files => JavaFiles} -import java.util.UUID +import java.util.{Base64, UUID} trait FileFixtures extends EitherValues with BIOValues { @@ -41,6 +41,7 @@ trait FileFixtures extends EitherValues with BIOValues { val fileTagged = nxv + "fileTagged" val fileTagged2 = nxv + "fileTagged2" val file1Encoded = UrlUtils.encode(file1.toString) + val encodeId = (id: String) => UrlUtils.encode((nxv + id).toString) val generatedId = project.base.iri / uuid.toString val generatedId2 = project.base.iri / uuid2.toString @@ -82,4 +83,9 @@ trait FileFixtures extends EitherValues with BIOValues { Multipart.FormData.BodyPart("file", HttpEntity(`text/plain(UTF-8)`, "0" * size), Map("filename" -> filename)) ) .toEntity() + + def base64encode(input: String) = { + val encodedBytes = Base64.getEncoder.encode(input.getBytes("UTF-8")) + new String(encodedBytes, "UTF-8") + } } diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala index 6060b5ce44..bd9e4240b9 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala @@ -140,7 +140,7 @@ class FilesRoutesSpec private val diskIdRev = ResourceRef.Revision(dId, 1) private val s3IdRev = ResourceRef.Revision(s3Id, 2) - private val tag = UserTag.unsafe("mytag") + private val tag = "mytag" private val varyHeader = RawHeader("Vary", "Accept,Accept-Encoding") @@ -185,9 +185,10 @@ class FilesRoutesSpec status shouldEqual StatusCodes.Created val attr = attributes(id = uuid2) val expected = fileMetadata(projectRef, generatedId2, attr, diskIdRev) - val fileByTag = files.fetch(FileId(generatedId2, tag, projectRef)).accepted + val userTag = UserTag.unsafe(tag) + val fileByTag = files.fetch(FileId(generatedId2, userTag, projectRef)).accepted response.asJson shouldEqual expected - fileByTag.value.tags.tags should contain(tag) + fileByTag.value.tags.tags should contain(userTag) } } } @@ -208,11 +209,12 @@ class FilesRoutesSpec } "create a file on s3 with an authenticated user and provided id" in { - Put("/v1/files/org/proj/file1?storage=s3-storage", entity("file2.txt")) ~> asS3Writer ~> routes ~> check { + val id = genString() + Put(s"/v1/files/org/proj/$id?storage=s3-storage", entity(id)) ~> asS3Writer ~> routes ~> check { status shouldEqual StatusCodes.Created - val attr = attributes("file2.txt") + val attr = attributes(id) response.asJson shouldEqual - fileMetadata(projectRef, file1, attr, s3IdRev, createdBy = s3writer, updatedBy = s3writer) + fileMetadata(projectRef, nxv + id, attr, s3IdRev, createdBy = s3writer, updatedBy = s3writer) } } @@ -225,17 +227,20 @@ class FilesRoutesSpec status shouldEqual StatusCodes.Created val attr = attributes("fileTagged.txt", id = uuid2) val expected = fileMetadata(projectRef, fileTagged, attr, s3IdRev, createdBy = s3writer, updatedBy = s3writer) - val fileByTag = files.fetch(FileId(generatedId2, tag, projectRef)).accepted + val userTag = UserTag.unsafe(tag) + val fileByTag = files.fetch(FileId(generatedId2, userTag, projectRef)).accepted response.asJson shouldEqual expected - fileByTag.value.tags.tags should contain(tag) + fileByTag.value.tags.tags should contain(userTag) } } } "reject the creation of a file which already exists" in { - Put("/v1/files/org/proj/file1", entity()) ~> asWriter ~> routes ~> check { - status shouldEqual StatusCodes.Conflict - response.asJson shouldEqual jsonContentOf("/files/errors/already-exists.json", "id" -> file1) + givenAFile { id => + Put(s"/v1/files/org/proj/$id", entity()) ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.Conflict + response.asJson shouldEqual jsonContentOf("/files/errors/already-exists.json", "id" -> (nxv + id)) + } } } @@ -258,23 +263,27 @@ class FilesRoutesSpec } "fail to update a file without disk/write permission" in { - Put(s"/v1/files/org/proj/file1?rev=1", s3FieldsJson.toEntity) ~> routes ~> check { - response.shouldBeForbidden + givenAFile { id => + Put(s"/v1/files/org/proj/$id?rev=1", s3FieldsJson.toEntity) ~> routes ~> check { + response.shouldBeForbidden + } } } "update a file" in { - val endpoints = List( - "/v1/files/org/proj/file1", - s"/v1/files/org/proj/$file1Encoded" - ) - forAll(endpoints.zipWithIndex) { case (endpoint, idx) => - val filename = s"file-idx-$idx.txt" - Put(s"$endpoint?rev=${idx + 1}", entity(filename)) ~> asWriter ~> routes ~> check { - status shouldEqual StatusCodes.OK - val attr = attributes(filename) - response.asJson shouldEqual - fileMetadata(projectRef, file1, attr, diskIdRev, rev = idx + 2, createdBy = s3writer) + givenAFile { id => + val endpoints = List( + s"/v1/files/org/proj/$id", + s"/v1/files/org/proj/${encodeId(id)}" + ) + forAll(endpoints.zipWithIndex) { case (endpoint, idx) => + val filename = s"file-idx-$idx.txt" + Put(s"$endpoint?rev=${idx + 1}", entity(filename)) ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.OK + val attr = attributes(filename) + response.asJson shouldEqual + fileMetadata(projectRef, nxv + id, attr, diskIdRev, rev = idx + 2) + } } } } @@ -291,63 +300,78 @@ class FilesRoutesSpec } "fail to update a file link using a storage that does not allow it" in { - val payload = json"""{"filename": "my.txt", "path": "my/file.txt", "mediaType": "text/plain"}""" - Put("/v1/files/org/proj/file1?rev=3", payload.toEntity) ~> asWriter ~> routes ~> check { - status shouldEqual StatusCodes.BadRequest - response.asJson shouldEqual - jsonContentOf("files/errors/unsupported-operation.json", "id" -> file1, "storageId" -> dId) + givenAFile { id => + val payload = json"""{"filename": "my.txt", "path": "my/file.txt", "mediaType": "text/plain"}""" + Put(s"/v1/files/org/proj/$id?rev=1", payload.toEntity) ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.BadRequest + response.asJson shouldEqual + jsonContentOf("files/errors/unsupported-operation.json", "id" -> (nxv + id), "storageId" -> dId) + } } } "reject the update of a non-existent file" in { - Put("/v1/files/org/proj/myid10?rev=1", entity("other.txt")) ~> asWriter ~> routes ~> check { + val nonExistentFile = genString() + Put(s"/v1/files/org/proj/$nonExistentFile?rev=1", entity("other.txt")) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.NotFound response.asJson shouldEqual - jsonContentOf("/files/errors/not-found.json", "id" -> (nxv + "myid10"), "proj" -> "org/proj") + jsonContentOf("/files/errors/not-found.json", "id" -> (nxv + nonExistentFile), "proj" -> "org/proj") } } "reject the update of a non-existent file storage" in { - Put("/v1/files/org/proj/file1?rev=3&storage=not-exist", entity("other.txt")) ~> asWriter ~> routes ~> check { - status shouldEqual StatusCodes.NotFound - response.asJson shouldEqual - jsonContentOf("/storages/errors/not-found.json", "id" -> (nxv + "not-exist"), "proj" -> projectRef) + givenAFile { id => + Put(s"/v1/files/org/proj/$id?rev=1&storage=not-exist", entity("other.txt")) ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.NotFound + response.asJson shouldEqual + jsonContentOf("/storages/errors/not-found.json", "id" -> (nxv + "not-exist"), "proj" -> projectRef) + } } } "reject the update of a file at a non-existent revision" in { - Put("/v1/files/org/proj/file1?rev=10", entity("other.txt")) ~> asWriter ~> routes ~> check { - status shouldEqual StatusCodes.Conflict - response.asJson shouldEqual - jsonContentOf("/files/errors/incorrect-rev.json", "provided" -> 10, "expected" -> 3) + givenAFile { id => + Put(s"/v1/files/org/proj/$id?rev=10", entity("other.txt")) ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.Conflict + response.asJson shouldEqual + jsonContentOf("/files/errors/incorrect-rev.json", "provided" -> 10, "expected" -> 1) + } } } "fail to deprecate a file without files/write permission" in { - Delete(s"/v1/files/org/proj/$uuid?rev=1") ~> routes ~> check { - response.shouldBeForbidden + givenAFile { id => + Delete(s"/v1/files/org/proj/$id?rev=1") ~> routes ~> check { + response.shouldBeForbidden + } } } "deprecate a file" in { - Delete(s"/v1/files/org/proj/$uuid?rev=1") ~> asWriter ~> routes ~> check { - status shouldEqual StatusCodes.OK - val attr = attributes() - response.asJson shouldEqual fileMetadata(projectRef, generatedId, attr, diskIdRev, rev = 2, deprecated = true) + givenAFile { id => + Delete(s"/v1/files/org/proj/$id?rev=1") ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.OK + val attr = attributes(id) + response.asJson shouldEqual fileMetadata(projectRef, nxv + id, attr, diskIdRev, rev = 2, deprecated = true) + } } } "reject the deprecation of a file without rev" in { - Delete("/v1/files/org/proj/file1") ~> asWriter ~> routes ~> check { - status shouldEqual StatusCodes.BadRequest - response.asJson shouldEqual jsonContentOf("/errors/missing-query-param.json", "field" -> "rev") + givenAFile { id => + Delete(s"/v1/files/org/proj/$id") ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.BadRequest + response.asJson shouldEqual jsonContentOf("/errors/missing-query-param.json", "field" -> "rev") + } } } "reject the deprecation of an already deprecated file" in { - Delete(s"/v1/files/org/proj/$uuid?rev=2") ~> asWriter ~> routes ~> check { - status shouldEqual StatusCodes.BadRequest - response.asJson shouldEqual jsonContentOf("/files/errors/file-deprecated.json", "id" -> generatedId) + givenADeprecatedFile { id => + Delete(s"/v1/files/org/proj/$id?rev=2") ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.BadRequest + response.asJson shouldEqual jsonContentOf("/files/errors/file-deprecated.json", "id" -> (nxv + id)) + } } } @@ -392,174 +416,213 @@ class FilesRoutesSpec } "tag a file" in { - val payload = json"""{"tag": "mytag", "rev": 1}""" - Post("/v1/files/org/proj/file1/tags?rev=3", payload.toEntity) ~> asWriter ~> routes ~> check { - status shouldEqual StatusCodes.Created - val attr = attributes("file-idx-1.txt") - response.asJson shouldEqual fileMetadata(projectRef, file1, attr, diskIdRev, rev = 4, createdBy = s3writer) + givenAFile { id => + val payload = json"""{"tag": "mytag", "rev": 1}""" + Post(s"/v1/files/org/proj/$id/tags?rev=1", payload.toEntity) ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.Created + val attr = attributes(id) + response.asJson shouldEqual fileMetadata(projectRef, nxv + id, attr, diskIdRev, rev = 2) + } } } "fail to fetch a file without s3/read permission" in { - forAll(List("", "?rev=1", "?tags=mytag")) { suffix => - Get(s"/v1/files/org/proj/file1$suffix") ~> Accept(`*/*`) ~> routes ~> check { - response.shouldBeForbidden - response.headers should not contain varyHeader + givenATaggedFile(tag) { id => + forAll(List("", "?rev=1", s"?tags=$tag")) { suffix => + Get(s"/v1/files/org/proj/$id$suffix") ~> Accept(`*/*`) ~> routes ~> check { + response.shouldBeForbidden + response.headers should not contain varyHeader + } } } } "fail to fetch a file when the accept header does not match file media type" in { - forAll(List("", "?rev=1", "?tags=mytag")) { suffix => - Get(s"/v1/files/org/proj/file1$suffix") ~> Accept(`video/*`) ~> asReader ~> routes ~> check { - response.status shouldEqual StatusCodes.NotAcceptable - response.asJson shouldEqual jsonContentOf("errors/content-type.json", "expected" -> "text/plain") - response.headers should not contain varyHeader + givenATaggedFile(tag) { id => + forAll(List("", "?rev=1", s"?tags=$tag")) { suffix => + Get(s"/v1/files/org/proj/$id$suffix") ~> Accept(`video/*`) ~> asReader ~> routes ~> check { + response.status shouldEqual StatusCodes.NotAcceptable + response.asJson shouldEqual jsonContentOf("errors/content-type.json", "expected" -> "text/plain") + response.headers should not contain varyHeader + } } } } "fetch a file" in { - forAll(List(Accept(`*/*`), Accept(`text/*`))) { accept => - forAll( - List("/v1/files/org/proj/file1", "/v1/resources/org/proj/_/file1", "/v1/resources/org/proj/file/file1") - ) { endpoint => - Get(endpoint) ~> accept ~> asReader ~> routes ~> check { - status shouldEqual StatusCodes.OK - contentType.value shouldEqual `text/plain(UTF-8)`.value - val filename64 = "ZmlsZS1pZHgtMS50eHQ=" // file-idx-1.txt - header("Content-Disposition").value.value() shouldEqual - s"""attachment; filename="=?UTF-8?B?$filename64?="""" - response.asString shouldEqual content - response.headers should contain(varyHeader) + givenAFile { id => + forAll(List(Accept(`*/*`), Accept(`text/*`))) { accept => + forAll( + List(s"/v1/files/org/proj/$id", s"/v1/resources/org/proj/_/$id", s"/v1/resources/org/proj/file/$id") + ) { endpoint => + Get(endpoint) ~> accept ~> asReader ~> routes ~> check { + status shouldEqual StatusCodes.OK + contentType.value shouldEqual `text/plain(UTF-8)`.value + header("Content-Disposition").value.value() shouldEqual + s"""attachment; filename="=?UTF-8?B?${base64encode(id)}?="""" + response.asString shouldEqual content + response.headers should contain(varyHeader) + } } } } } "fetch a file by rev and tag" in { - val endpoints = List( - s"/v1/files/$uuid/$uuid/file1", - s"/v1/resources/$uuid/$uuid/_/file1", - s"/v1/resources/$uuid/$uuid/file/file1", - "/v1/files/org/proj/file1", - "/v1/resources/org/proj/_/file1", - "/v1/resources/org/proj/file/file1", - s"/v1/files/org/proj/$file1Encoded", - s"/v1/resources/org/proj/_/$file1Encoded", - s"/v1/resources/org/proj/file/$file1Encoded" - ) - forAll(endpoints) { endpoint => - forAll(List("rev=1", "tag=mytag")) { param => - Get(s"$endpoint?$param") ~> Accept(`*/*`) ~> asReader ~> routes ~> check { - status shouldEqual StatusCodes.OK - contentType.value shouldEqual `text/plain(UTF-8)`.value - val filename64 = "ZmlsZTIudHh0" // file2.txt - header("Content-Disposition").value.value() shouldEqual - s"""attachment; filename="=?UTF-8?B?$filename64?="""" - response.asString shouldEqual content - response.headers should contain(varyHeader) + givenATaggedFile(tag) { id => + val endpoints = List( + s"/v1/files/$uuid/$uuid/$id", + s"/v1/resources/$uuid/$uuid/_/$id", + s"/v1/resources/$uuid/$uuid/file/$id", + s"/v1/files/org/proj/$id", + s"/v1/resources/org/proj/_/$id", + s"/v1/resources/org/proj/file/$id", + s"/v1/files/org/proj/${encodeId(id)}", + s"/v1/resources/org/proj/_/${encodeId(id)}", + s"/v1/resources/org/proj/file/${encodeId(id)}" + ) + forAll(endpoints) { endpoint => + forAll(List("rev=1", s"tag=$tag")) { param => + Get(s"$endpoint?$param") ~> Accept(`*/*`) ~> asReader ~> routes ~> check { + status shouldEqual StatusCodes.OK + contentType.value shouldEqual `text/plain(UTF-8)`.value + header("Content-Disposition").value.value() shouldEqual + s"""attachment; filename="=?UTF-8?B?${base64encode(id)}?="""" + response.asString shouldEqual content + response.headers should contain(varyHeader) + } } } } } "fail to fetch a file metadata without resources/read permission" in { - val endpoints = - List("/v1/files/org/proj/file1", "/v1/files/org/proj/file1/tags", "/v1/resources/org/proj/_/file1/tags") - forAll(endpoints) { endpoint => - forAll(List("", "?rev=1", "?tags=mytag")) { suffix => - Get(s"$endpoint$suffix") ~> Accept(`application/ld+json`) ~> routes ~> check { - response.shouldBeForbidden - response.headers should not contain varyHeader + givenATaggedFile(tag) { id => + val endpoints = + List(s"/v1/files/org/proj/$id", s"/v1/files/org/proj/$id/tags", s"/v1/resources/org/proj/_/$id/tags") + forAll(endpoints) { endpoint => + forAll(List("", "?rev=1", s"?tags=$tag")) { suffix => + Get(s"$endpoint$suffix") ~> Accept(`application/ld+json`) ~> routes ~> check { + response.shouldBeForbidden + response.headers should not contain varyHeader + } } } } } "fetch a file metadata" in { - Get("/v1/files/org/proj/file1") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { - status shouldEqual StatusCodes.OK - val attr = attributes("file-idx-1.txt") - response.asJson shouldEqual fileMetadata(projectRef, file1, attr, diskIdRev, rev = 4, createdBy = s3writer) - response.headers should contain(varyHeader) + givenAFile { id => + Get(s"/v1/files/org/proj/$id") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { + status shouldEqual StatusCodes.OK + val attr = attributes(id) + response.asJson shouldEqual fileMetadata(projectRef, nxv + id, attr, diskIdRev) + response.headers should contain(varyHeader) + } } } "fetch a file metadata by rev and tag" in { - val attr = attributes("file2.txt") - val endpoints = List( - s"/v1/files/$uuid/$uuid/file1", - s"/v1/resources/$uuid/$uuid/_/file1", - s"/v1/resources/$uuid/$uuid/file/file1", - "/v1/files/org/proj/file1", - "/v1/resources/org/proj/_/file1", - s"/v1/files/org/proj/$file1Encoded", - s"/v1/resources/org/proj/_/$file1Encoded" - ) - forAll(endpoints) { endpoint => - forAll(List("rev=1", "tag=mytag")) { param => - Get(s"$endpoint?$param") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { - status shouldEqual StatusCodes.OK - response.asJson shouldEqual - fileMetadata(projectRef, file1, attr, s3IdRev, createdBy = s3writer, updatedBy = s3writer) - response.headers should contain(varyHeader) + givenATaggedFile(tag) { id => + val attr = attributes(id) + val endpoints = List( + s"/v1/files/$uuid/$uuid/$id", + s"/v1/resources/$uuid/$uuid/_/$id", + s"/v1/resources/$uuid/$uuid/file/$id", + s"/v1/files/org/proj/$id", + s"/v1/resources/org/proj/_/$id", + s"/v1/files/org/proj/${encodeId(id)}", + s"/v1/resources/org/proj/_/${encodeId(id)}" + ) + forAll(endpoints) { endpoint => + forAll(List("rev=1", s"tag=$tag")) { param => + Get(s"$endpoint?$param") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { + status shouldEqual StatusCodes.OK + response.asJson shouldEqual + fileMetadata(projectRef, nxv + id, attr, diskIdRev) + response.headers should contain(varyHeader) + } } } } } "fetch the file tags" in { - Get("/v1/resources/org/proj/_/file1/tags?rev=1") ~> asReader ~> routes ~> check { - status shouldEqual StatusCodes.OK - response.asJson shouldEqual json"""{"tags": []}""".addContext(contexts.tags) - } - Get("/v1/files/org/proj/file1/tags") ~> asReader ~> routes ~> check { - status shouldEqual StatusCodes.OK - response.asJson shouldEqual json"""{"tags": [{"rev": 1, "tag": "mytag"}]}""".addContext(contexts.tags) + givenAFile { id => + val taggingPayload = json"""{"tag": "$tag", "rev": 1}""" + Post(s"/v1/files/org/proj/$id/tags?rev=1", taggingPayload.toEntity) ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.Created + } + Get(s"/v1/resources/org/proj/_/$id/tags?rev=1") ~> asReader ~> routes ~> check { + status shouldEqual StatusCodes.OK + response.asJson shouldEqual json"""{"tags": []}""".addContext(contexts.tags) + } + Get(s"/v1/files/org/proj/$id/tags") ~> asReader ~> routes ~> check { + status shouldEqual StatusCodes.OK + response.asJson shouldEqual json"""{"tags": [{"rev": 1, "tag": "$tag"}]}""".addContext(contexts.tags) + } } } "return not found if tag not found" in { - Get("/v1/files/org/proj/file1?tag=myother") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { - status shouldEqual StatusCodes.NotFound - response.asJson shouldEqual jsonContentOf("/errors/tag-not-found.json", "tag" -> "myother") + givenATaggedFile("mytag") { id => + Get(s"/v1/files/org/proj/$id?tag=myother") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { + status shouldEqual StatusCodes.NotFound + response.asJson shouldEqual jsonContentOf("/errors/tag-not-found.json", "tag" -> "myother") + } } } "reject if provided rev and tag simultaneously" in { - Get("/v1/files/org/proj/file1?tag=mytag&rev=1") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { - status shouldEqual StatusCodes.BadRequest - response.asJson shouldEqual jsonContentOf("/errors/tag-and-rev-error.json") + givenATaggedFile(tag) { id => + Get(s"/v1/files/org/proj/$id?tag=$tag&rev=1") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { + status shouldEqual StatusCodes.BadRequest + response.asJson shouldEqual jsonContentOf("/errors/tag-and-rev-error.json") + } } } - "delete a tag on file" in { - Delete("/v1/files/org/proj/file1/tags/mytag?rev=4") ~> asWriter ~> routes ~> check { - val attr = attributes("file-idx-1.txt") + def deleteTag(id: String, tag: String, rev: Int) = + Delete(s"/v1/files/org/proj/$id/tags/$tag?rev=$rev") ~> asWriter ~> routes ~> check { + val attr = attributes(s"$id") status shouldEqual StatusCodes.OK - response.asJson shouldEqual fileMetadata(projectRef, file1, attr, diskIdRev, rev = 5, createdBy = s3writer) + response.asJson shouldEqual fileMetadata(projectRef, nxv + id, attr, diskIdRev, rev = rev + 1) + } + + "delete a tag on file" in { + givenATaggedFile(tag) { id => + deleteTag(id, tag, 1) } } "not return the deleted tag" in { - Get("/v1/files/org/proj/file1/tags") ~> asReader ~> routes ~> check { - status shouldEqual StatusCodes.OK - response.asJson shouldEqual json"""{"tags": []}""".addContext(contexts.tags) + givenATaggedFile(tag) { id => + deleteTag(id, tag, 1) + Get(s"/v1/files/org/proj/$id/tags") ~> asReader ~> routes ~> check { + status shouldEqual StatusCodes.OK + response.asJson shouldEqual json"""{"tags": []}""".addContext(contexts.tags) + } } } "fail to fetch file by the deleted tag" in { - Get("/v1/files/org/proj/file1?tag=mytag") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { - status shouldEqual StatusCodes.NotFound - response.asJson shouldEqual jsonContentOf("/errors/tag-not-found.json", "tag" -> "mytag") + givenATaggedFile(tag) { id => + deleteTag(id, tag, 1) + Get(s"/v1/files/org/proj/$id?tag=$tag") ~> Accept(`application/ld+json`) ~> asReader ~> routes ~> check { + status shouldEqual StatusCodes.NotFound + response.asJson shouldEqual jsonContentOf("/errors/tag-not-found.json", "tag" -> "mytag") + } } } "redirect to fusion for the latest version if the Accept header is set to text/html" in { - Get("/v1/files/org/project/file1") ~> Accept(`text/html`) ~> routes ~> check { - response.status shouldEqual StatusCodes.SeeOther - response.header[Location].value.uri shouldEqual Uri("https://bbp.epfl.ch/nexus/web/org/project/resources/file1") + givenAFile { id => + Get(s"/v1/files/org/project/$id") ~> Accept(`text/html`) ~> routes ~> check { + response.status shouldEqual StatusCodes.SeeOther + response.header[Location].value.uri shouldEqual Uri( + s"https://bbp.epfl.ch/nexus/web/org/project/resources/$id" + ) + } } } } @@ -572,6 +635,14 @@ class FilesRoutesSpec test(id) } + def givenATaggedFile(tag: String)(test: String => Assertion): Assertion = { + val id = genString() + Put(s"/v1/files/org/proj/$id?tag=$tag", entity(s"$id")) ~> asWriter ~> routes ~> check { + status shouldEqual StatusCodes.Created + } + test(id) + } + def givenADeprecatedFile(test: String => Assertion): Assertion = givenAFile { id => Delete(s"/v1/files/org/proj/$id?rev=1") ~> asWriter ~> routes ~> check { From 1f93b34aecf2ca61c3c3bd3e80a91462476edde9 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Fri, 3 Nov 2023 17:53:01 +0100 Subject: [PATCH 09/22] Fix user naming --- .../delta/routes/ResourcesRoutesSpec.scala | 20 +++++++++---------- .../resources/resource-with-metadata.json | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) 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 76fcf54d40..db8797dce4 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 @@ -29,7 +29,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.Proje 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} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject, User} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} import ch.epfl.bluebrain.nexus.testkit.bio.IOFromMap @@ -44,16 +44,16 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap with CatsIOValues private val uuid = UUID.randomUUID() implicit private val uuidF: UUIDF = UUIDF.fixed(uuid) - private val reader = alice - private val writer = bob + private val reader = User("reader", realm) + private val writer = User("writer", realm) - implicit private val callerAlice: Caller = - Caller(alice, Set(alice, Anonymous, Authenticated(realm), Group("group", realm))) - implicit private val callerBob: Caller = - Caller(bob, Set(bob, Anonymous, Authenticated(realm), Group("group", realm))) + 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))) - private val asReader = addCredentials(OAuth2BearerToken("alice")) - private val asWriter = addCredentials(OAuth2BearerToken("bob")) + private val asReader = addCredentials(OAuth2BearerToken("reader")) + private val asWriter = addCredentials(OAuth2BearerToken("writer")) private val am = ApiMappings("nxv" -> nxv.base, "Person" -> schema.Person) private val projBase = nxv.base @@ -114,7 +114,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap with CatsIOValues ( Route.seal( ResourcesRoutes( - IdentitiesDummy(callerAlice, callerBob), + IdentitiesDummy(callerReader, callerWriter), aclCheck, resources, DeltaSchemeDirectives( diff --git a/delta/sdk/src/test/resources/resources/resource-with-metadata.json b/delta/sdk/src/test/resources/resources/resource-with-metadata.json index c26cc970de..0d359f2585 100644 --- a/delta/sdk/src/test/resources/resources/resource-with-metadata.json +++ b/delta/sdk/src/test/resources/resources/resource-with-metadata.json @@ -7,7 +7,7 @@ "number": 24, "_constrainedBy": "https://bluebrain.github.io/nexus/schemas/unconstrained.json", "_createdAt": "1970-01-01T00:00:00Z", - "_createdBy": "http://localhost/v1/realms/wonderland/users/bob", + "_createdBy": "http://localhost/v1/realms/wonderland/users/writer", "_deprecated": false, "_incoming": "{{self}}/incoming", "_outgoing": "{{self}}/outgoing", @@ -16,5 +16,5 @@ "_schemaProject": "http://localhost/v1/projects/myorg/myproject", "_self": "{{self}}", "_updatedAt": "1970-01-01T00:00:00Z", - "_updatedBy": "http://localhost/v1/realms/wonderland/users/bob" + "_updatedBy": "http://localhost/v1/realms/wonderland/users/writer" } \ No newline at end of file From e40e5ed35f258fa7f85a53d0e2571405a5b09f34 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Fri, 3 Nov 2023 18:58:11 +0100 Subject: [PATCH 10/22] Add integration tests --- .../epfl/bluebrain/nexus/tests/Identity.scala | 6 +- .../bluebrain/nexus/tests/kg/FilesSpec.scala | 97 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/FilesSpec.scala diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/Identity.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/Identity.scala index b1a18e5878..73c78d863e 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/Identity.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/Identity.scala @@ -98,7 +98,11 @@ object Identity extends TestHelpers { val Mickey = UserCredentials(genString(), genString(), testRealm) } + object files { + val Writer = UserCredentials(genString(), genString(), testRealm) + } + lazy val allUsers = - userPermissions.UserWithNoPermissions :: userPermissions.UserWithPermissions :: acls.Marge :: archives.Tweety :: compositeviews.Jerry :: events.BugsBunny :: listings.Bob :: listings.Alice :: aggregations.Charlie :: aggregations.Rose :: orgs.Fry :: orgs.Leela :: projects.Bojack :: projects.PrincessCarolyn :: resources.Rick :: resources.Morty :: storages.Coyote :: views.ScoobyDoo :: mash.Radar :: supervision.Mickey :: Nil + userPermissions.UserWithNoPermissions :: userPermissions.UserWithPermissions :: acls.Marge :: archives.Tweety :: compositeviews.Jerry :: events.BugsBunny :: listings.Bob :: listings.Alice :: aggregations.Charlie :: aggregations.Rose :: orgs.Fry :: orgs.Leela :: projects.Bojack :: projects.PrincessCarolyn :: resources.Rick :: resources.Morty :: storages.Coyote :: views.ScoobyDoo :: mash.Radar :: supervision.Mickey :: files.Writer :: Nil } diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/FilesSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/FilesSpec.scala new file mode 100644 index 0000000000..f4ed740720 --- /dev/null +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/FilesSpec.scala @@ -0,0 +1,97 @@ +package ch.epfl.bluebrain.nexus.tests.kg + +import akka.http.scaladsl.model.{ContentTypes, StatusCodes} +import ch.epfl.bluebrain.nexus.tests.BaseIntegrationSpec +import ch.epfl.bluebrain.nexus.tests.Identity.files.Writer +import ch.epfl.bluebrain.nexus.tests.Optics.listing._total +import io.circe.Json +import org.scalatest.Assertion + +class FilesSpec extends BaseIntegrationSpec { + + private val org = genString() + private val project = genString() + private val projectRef = s"$org/$project" + + private val dummyJson = json"""{ "random": "content" }""" + + override def beforeAll(): Unit = { + super.beforeAll() + createProjects(Writer, org, project).accepted + } + + "File deprecation" should { + + "remove the deprecated file from the listing" in { + givenAFile { id => + eventually { assertFileIsInListing(id) } + deprecateFile(id, rev = 1) + eventually { assertFileNotInListing(id) } + } + } + + } + + "Files undeprecation" should { + + "reindex the previously deprecated file" in { + givenADeprecatedFile { id => + eventually { assertFileNotInListing(id) } + undeprecateFile(id, rev = 2) + eventually { assertFileIsInListing(id) } + } + } + + } + + private def assertListingTotal(id: String, expectedTotal: Int) = + deltaClient.get[Json](s"/files/$projectRef?locate=$id&deprecated=false", Writer) { (json, response) => + response.status shouldEqual StatusCodes.OK + _total.getOption(json) should contain(expectedTotal) + } + + private def assertFileIsInListing(id: String) = + assertListingTotal(id, 1) + + private def assertFileNotInListing(id: String) = + assertListingTotal(id, 0) + + /** Provides a file in the default storage */ + private def givenAFile(assertion: String => Assertion): Assertion = { + val id = genString() + deltaClient + .uploadFile[Json]( + s"/files/$projectRef/$id", + "file content", + ContentTypes.`text/plain(UTF-8)`, + s"$id.txt", + Writer + ) { (_, response) => + response.status shouldEqual StatusCodes.Created + } + .accepted + assertion(id) + } + + private def deprecateFile(id: String, rev: Int): Assertion = + deltaClient + .delete[Json](s"/files/$projectRef/$id?rev=$rev", Writer) { (_, response) => + response.status shouldEqual StatusCodes.OK + } + .accepted + + private def givenADeprecatedFile(assertion: String => Assertion): Assertion = { + givenAFile { id => + deprecateFile(id, 1) + assertion(id) + } + } + + private def undeprecateFile(id: String, rev: Int): Assertion = + deltaClient + .put[Json](s"/files/$projectRef/$id/undeprecate?rev=$rev", dummyJson, Writer) { (_, response) => + response.status shouldEqual StatusCodes.OK + } + .accepted + +} From f5cc6fb9584be68b2300ac7336a24244e5a7501e Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Mon, 6 Nov 2023 08:26:23 +0100 Subject: [PATCH 11/22] scalafmt --- .../epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala | 2 +- .../delta/plugins/storage/files/routes/FilesRoutesSpec.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 db8797dce4..3763dc138a 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 @@ -49,7 +49,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap with CatsIOValues implicit private val callerReader: Caller = Caller(reader, Set(reader, Anonymous, Authenticated(realm), Group("group", realm))) - implicit private val callerWriter: Caller = + implicit private val callerWriter: Caller = Caller(writer, Set(writer, Anonymous, Authenticated(realm), Group("group", realm))) private val asReader = addCredentials(OAuth2BearerToken("reader")) diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala index bd9e4240b9..e1a9887edb 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala @@ -227,7 +227,7 @@ class FilesRoutesSpec status shouldEqual StatusCodes.Created val attr = attributes("fileTagged.txt", id = uuid2) val expected = fileMetadata(projectRef, fileTagged, attr, s3IdRev, createdBy = s3writer, updatedBy = s3writer) - val userTag = UserTag.unsafe(tag) + val userTag = UserTag.unsafe(tag) val fileByTag = files.fetch(FileId(generatedId2, userTag, projectRef)).accepted response.asJson shouldEqual expected fileByTag.value.tags.tags should contain(userTag) From ce5a82526597a907b5b608350888c8a04e498804 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Mon, 6 Nov 2023 08:36:09 +0100 Subject: [PATCH 12/22] Add tests to FilesStmSpec for undeprecation --- .../plugins/storage/files/FilesStmSpec.scala | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesStmSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesStmSpec.scala index 48dae7c034..30c346e8c9 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesStmSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/FilesStmSpec.scala @@ -6,7 +6,7 @@ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.Digest.{Compute import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileAttributes.FileAttributesOrigin.Client import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileCommand._ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileEvent._ -import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection.{DigestAlreadyComputed, DigestNotComputed, FileIsDeprecated, FileNotFound, IncorrectRev, ResourceAlreadyExists, RevisionNotFound} +import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection.{DigestAlreadyComputed, DigestNotComputed, FileIsDeprecated, FileIsNotDeprecated, FileNotFound, IncorrectRev, ResourceAlreadyExists, RevisionNotFound} import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{Digest, FileAttributes} import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.StorageFixtures import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.DigestAlgorithm @@ -98,6 +98,12 @@ class FilesStmSpec extends CatsEffectSpec with FileFixtures with StorageFixtures FileDeprecated(id, projectRef, storageRef, DiskStorageType, 3, epoch, alice) } + "create a new event from a UndeprecateFile command" in { + val current = FileGen.state(id, projectRef, storageRef, attributes, rev = 2, deprecated = true) + evaluate(Some(current), UndeprecateFile(id, projectRef, 2, alice)).accepted shouldEqual + FileUndeprecated(id, projectRef, storageRef, DiskStorageType, 3, epoch, alice) + } + "reject with IncorrectRev" in { val current = FileGen.state(id, projectRef, storageRef, attributes) val commands = List( @@ -143,6 +149,11 @@ class FilesStmSpec extends CatsEffectSpec with FileFixtures with StorageFixtures } } + "reject with FileIsNotDeprecated" in { + val current = FileGen.state(id, projectRef, storageRef, attributes, deprecated = false) + evaluate(Some(current), UndeprecateFile(id, projectRef, 1, alice)).rejectedWith[FileIsNotDeprecated] + } + "reject with RevisionNotFound" in { val current = FileGen.state(id, projectRef, storageRef, attributes) evaluate(Some(current), TagFile(id, projectRef, targetRev = 3, myTag, 1, alice)).rejected shouldEqual @@ -209,6 +220,20 @@ class FilesStmSpec extends CatsEffectSpec with FileFixtures with StorageFixtures updatedBy = alice ) } + + "from a new FileUndeprecated event" in { + val event = FileUndeprecated(id, projectRef, storageRef, DiskStorageType, 2, time2, alice) + val current = FileGen.state(id, projectRef, storageRef, attributes, deprecated = true) + + next(None, event) shouldEqual None + + next(Some(current), event).value shouldEqual current.copy( + rev = 2, + deprecated = false, + updatedAt = time2, + updatedBy = alice + ) + } } } From 5cc9786189d0e3b6db3b3aa568606be16bd25c22 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Mon, 6 Nov 2023 08:36:40 +0100 Subject: [PATCH 13/22] Update version of storage used in tests --- .../nexus/testkit/remotestorage/RemoteStorageContainer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageContainer.scala b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageContainer.scala index 93a942a803..48604360bf 100644 --- a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageContainer.scala +++ b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageContainer.scala @@ -7,7 +7,7 @@ import org.testcontainers.utility.DockerImageName import java.nio.file.Path class RemoteStorageContainer(rootVolume: Path) - extends GenericContainer[RemoteStorageContainer](DockerImageName.parse("bluebrain/nexus-storage:1.7.0")) { + extends GenericContainer[RemoteStorageContainer](DockerImageName.parse("bluebrain/nexus-storage:1.8.0-M12")) { addEnv("JAVA_OPTS", "-Xmx256m -Dconfig.override_with_env_vars=true") addEnv("CONFIG_FORCE_app_subject_anonymous", "true") From 9275fb75722d9b256674e0aaed2743a5fbd76fbd Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Mon, 6 Nov 2023 09:14:27 +0100 Subject: [PATCH 14/22] Move `fileMetadata` --- .../storage/files/routes/FilesRoutesSpec.scala | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala index e1a9887edb..f4db69e41f 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala @@ -38,6 +38,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject, User} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef} +import ch.epfl.bluebrain.nexus.testkit.TestHelpers.jsonContentOf import ch.epfl.bluebrain.nexus.testkit.bio.IOFromMap import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsIOValues import io.circe.Json @@ -661,6 +662,22 @@ class FilesRoutesSpec deprecated: Boolean = false, createdBy: Subject = callerWriter.subject, updatedBy: Subject = callerWriter.subject + )(implicit baseUri: BaseUri): Json = + fileMetadata(project, id, attributes, storage, storageType, rev, deprecated, createdBy, updatedBy) + +} + +object FilesRoutesSpec { + def fileMetadata( + project: ProjectRef, + id: Iri, + attributes: FileAttributes, + storage: ResourceRef.Revision, + storageType: StorageType = StorageType.DiskStorage, + rev: Int = 1, + deprecated: Boolean = false, + createdBy: Subject, + updatedBy: Subject )(implicit baseUri: BaseUri): Json = jsonContentOf( "files/file-route-metadata-response.json", From f53d3c493e54a0d436c41fc4a2ed59073f7dd87a Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Mon, 6 Nov 2023 09:49:10 +0100 Subject: [PATCH 15/22] Fix RemoteStorageClientSpec --- .../operations/remote/client/RemoteStorageClientSpec.scala | 2 +- .../nexus/testkit/remotestorage/RemoteStorageContainer.scala | 4 ++-- .../nexus/testkit/remotestorage/RemoteStorageDocker.scala | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/remote/client/RemoteStorageClientSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/remote/client/RemoteStorageClientSpec.scala index 1c1118cbc6..01a8c82576 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/remote/client/RemoteStorageClientSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/remote/client/RemoteStorageClientSpec.scala @@ -58,7 +58,7 @@ class RemoteStorageClientSpec(docker: RemoteStorageDocker) ) "fetch the service description" in eventually { - client.serviceDescription(baseUri).accepted shouldEqual ServiceDescription(Name.unsafe("remoteStorage"), "1.7.0") + client.serviceDescription(baseUri).accepted shouldEqual ServiceDescription(Name.unsafe("remoteStorage"), docker.storageVersion) } "check if a bucket exists" in { diff --git a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageContainer.scala b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageContainer.scala index 48604360bf..fc9aa8814f 100644 --- a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageContainer.scala +++ b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageContainer.scala @@ -6,8 +6,8 @@ import org.testcontainers.utility.DockerImageName import java.nio.file.Path -class RemoteStorageContainer(rootVolume: Path) - extends GenericContainer[RemoteStorageContainer](DockerImageName.parse("bluebrain/nexus-storage:1.8.0-M12")) { +class RemoteStorageContainer(storageVersion: String, rootVolume: Path) + extends GenericContainer[RemoteStorageContainer](DockerImageName.parse(s"bluebrain/nexus-storage:$storageVersion")) { addEnv("JAVA_OPTS", "-Xmx256m -Dconfig.override_with_env_vars=true") addEnv("CONFIG_FORCE_app_subject_anonymous", "true") diff --git a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageDocker.scala b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageDocker.scala index 3547117fcb..526542614a 100644 --- a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageDocker.scala +++ b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageDocker.scala @@ -13,8 +13,10 @@ trait RemoteStorageDocker extends BeforeAndAfterAll { this: Suite => private val rwx = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxrwxrwx")) private val tmpFolder: Path = Files.createTempDirectory("root", rwx) + val storageVersion: String = "1.8.0-M12" + protected val container: RemoteStorageContainer = - new RemoteStorageContainer(tmpFolder) + new RemoteStorageContainer(storageVersion, tmpFolder) .withReuse(false) .withStartupTimeout(60.seconds.toJava) From 02a60adfd1275b9f6eb8fb4ad19e86a307a0a7b4 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Mon, 6 Nov 2023 09:52:18 +0100 Subject: [PATCH 16/22] Fix FileRoutesSpec --- .../delta/plugins/storage/files/routes/FilesRoutesSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala index f4db69e41f..6582519d6c 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala @@ -663,7 +663,7 @@ class FilesRoutesSpec createdBy: Subject = callerWriter.subject, updatedBy: Subject = callerWriter.subject )(implicit baseUri: BaseUri): Json = - fileMetadata(project, id, attributes, storage, storageType, rev, deprecated, createdBy, updatedBy) + FilesRoutesSpec.fileMetadata(project, id, attributes, storage, storageType, rev, deprecated, createdBy, updatedBy) } From 1b2b7e6b86b43c4c732bf1cf106f10dd532e0fd3 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Mon, 6 Nov 2023 09:56:38 +0100 Subject: [PATCH 17/22] scalafmt --- .../nexus/testkit/remotestorage/RemoteStorageContainer.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageContainer.scala b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageContainer.scala index fc9aa8814f..a62b054b49 100644 --- a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageContainer.scala +++ b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/remotestorage/RemoteStorageContainer.scala @@ -7,7 +7,9 @@ import org.testcontainers.utility.DockerImageName import java.nio.file.Path class RemoteStorageContainer(storageVersion: String, rootVolume: Path) - extends GenericContainer[RemoteStorageContainer](DockerImageName.parse(s"bluebrain/nexus-storage:$storageVersion")) { + extends GenericContainer[RemoteStorageContainer]( + DockerImageName.parse(s"bluebrain/nexus-storage:$storageVersion") + ) { addEnv("JAVA_OPTS", "-Xmx256m -Dconfig.override_with_env_vars=true") addEnv("CONFIG_FORCE_app_subject_anonymous", "true") From 89148d5aba55c80f16d69f0716ac662674e93a87 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Mon, 6 Nov 2023 10:01:34 +0100 Subject: [PATCH 18/22] scalafmt --- .../operations/remote/client/RemoteStorageClientSpec.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/remote/client/RemoteStorageClientSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/remote/client/RemoteStorageClientSpec.scala index 01a8c82576..f171deb18d 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/remote/client/RemoteStorageClientSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/operations/remote/client/RemoteStorageClientSpec.scala @@ -58,7 +58,10 @@ class RemoteStorageClientSpec(docker: RemoteStorageDocker) ) "fetch the service description" in eventually { - client.serviceDescription(baseUri).accepted shouldEqual ServiceDescription(Name.unsafe("remoteStorage"), docker.storageVersion) + client.serviceDescription(baseUri).accepted shouldEqual ServiceDescription( + Name.unsafe("remoteStorage"), + docker.storageVersion + ) } "check if a bucket exists" in { From 0459fa48ccdd38e95b8f8cd7c738146365783a0f Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Mon, 6 Nov 2023 12:22:56 +0100 Subject: [PATCH 19/22] Update integration test --- .../scala/ch/epfl/bluebrain/nexus/tests/kg/FilesSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/FilesSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/FilesSpec.scala index f4ed740720..80f60dce7e 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/FilesSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/FilesSpec.scala @@ -24,7 +24,6 @@ class FilesSpec extends BaseIntegrationSpec { "remove the deprecated file from the listing" in { givenAFile { id => - eventually { assertFileIsInListing(id) } deprecateFile(id, rev = 1) eventually { assertFileNotInListing(id) } } @@ -36,7 +35,6 @@ class FilesSpec extends BaseIntegrationSpec { "reindex the previously deprecated file" in { givenADeprecatedFile { id => - eventually { assertFileNotInListing(id) } undeprecateFile(id, rev = 2) eventually { assertFileIsInListing(id) } } @@ -70,6 +68,7 @@ class FilesSpec extends BaseIntegrationSpec { response.status shouldEqual StatusCodes.Created } .accepted + eventually { assertFileIsInListing(id) } assertion(id) } @@ -83,6 +82,7 @@ class FilesSpec extends BaseIntegrationSpec { private def givenADeprecatedFile(assertion: String => Assertion): Assertion = { givenAFile { id => deprecateFile(id, 1) + eventually { assertFileNotInListing(id) } assertion(id) } } From ac87f61188feead08b15b93db35b4b748fa6bffc Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:16:46 +0100 Subject: [PATCH 20/22] User other `fail` method --- .../bluebrain/nexus/testkit/scalatest/ce/CatsIOValues.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsIOValues.scala b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsIOValues.scala index 155742eb7f..e09c98095d 100644 --- a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsIOValues.scala +++ b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/scalatest/ce/CatsIOValues.scala @@ -14,7 +14,7 @@ trait CatsIOValues { implicit final class CatsIOValuesOps[A](private val io: IO[A]) { def accepted(implicit pos: source.Position): A = { io.attempt.unsafeRunTimed(45.seconds).getOrElse(fail("IO timed out during .accepted call")) match { - case Left(e) => fail(s"IO failed when it was expected to succeed. Failure details: $e") + case Left(e) => fail(s"IO failed when it was expected to succeed.", e) case Right(value) => value } } From 1396655d0475626462fb2db44134a74d1571f7d7 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:17:24 +0100 Subject: [PATCH 21/22] Inline json errors --- .../errors/file-is-not-deprecated.json | 5 ---- .../files/errors/already-exists.json | 5 ---- .../files/routes/FilesRoutesSpec.scala | 7 ++++-- .../testkit/errors/files/FileErrors.scala | 25 +++++++++++++++++++ 4 files changed, 30 insertions(+), 12 deletions(-) delete mode 100644 delta/plugins/storage/src/test/resources/errors/file-is-not-deprecated.json delete mode 100644 delta/plugins/storage/src/test/resources/files/errors/already-exists.json create mode 100644 delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/errors/files/FileErrors.scala diff --git a/delta/plugins/storage/src/test/resources/errors/file-is-not-deprecated.json b/delta/plugins/storage/src/test/resources/errors/file-is-not-deprecated.json deleted file mode 100644 index 2a749bfbc7..0000000000 --- a/delta/plugins/storage/src/test/resources/errors/file-is-not-deprecated.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "@context" : "https://bluebrain.github.io/nexus/contexts/error.json", - "@type" : "FileIsNotDeprecated", - "reason" : "File '{{id}}' is not deprecated." -} \ No newline at end of file diff --git a/delta/plugins/storage/src/test/resources/files/errors/already-exists.json b/delta/plugins/storage/src/test/resources/files/errors/already-exists.json deleted file mode 100644 index f6649ca685..0000000000 --- a/delta/plugins/storage/src/test/resources/files/errors/already-exists.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "@context": "https://bluebrain.github.io/nexus/contexts/error.json", - "@type": "ResourceAlreadyExists", - "reason": "Resource '{{id}}' already exists in project 'org/proj'." -} \ No newline at end of file diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala index 6582519d6c..39ceb150b1 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala @@ -40,6 +40,7 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef} import ch.epfl.bluebrain.nexus.testkit.TestHelpers.jsonContentOf import ch.epfl.bluebrain.nexus.testkit.bio.IOFromMap +import ch.epfl.bluebrain.nexus.testkit.errors.files.FileErrors.{fileAlreadyExistsError, fileIsNotDeprecatedError} import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsIOValues import io.circe.Json import org.scalatest._ @@ -240,7 +241,7 @@ class FilesRoutesSpec givenAFile { id => Put(s"/v1/files/org/proj/$id", entity()) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.Conflict - response.asJson shouldEqual jsonContentOf("/files/errors/already-exists.json", "id" -> (nxv + id)) + response.asJson shouldEqual fileAlreadyExistsError(nxvBase(id)) } } } @@ -411,7 +412,7 @@ class FilesRoutesSpec givenAFile { id => Put(s"/v1/files/org/proj/$id/undeprecate?rev=1") ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.BadRequest - response.asJson shouldEqual jsonContentOf("/errors/file-is-not-deprecated.json", "id" -> (nxv + id)) + response.asJson shouldEqual fileIsNotDeprecatedError(nxvBase(id)) } } } @@ -665,6 +666,8 @@ class FilesRoutesSpec )(implicit baseUri: BaseUri): Json = FilesRoutesSpec.fileMetadata(project, id, attributes, storage, storageType, rev, deprecated, createdBy, updatedBy) + private def nxvBase(id: String): String = (nxv + id).toString + } object FilesRoutesSpec { diff --git a/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/errors/files/FileErrors.scala b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/errors/files/FileErrors.scala new file mode 100644 index 0000000000..cadc9bffe5 --- /dev/null +++ b/delta/testkit/src/main/scala/ch/epfl/bluebrain/nexus/testkit/errors/files/FileErrors.scala @@ -0,0 +1,25 @@ +package ch.epfl.bluebrain.nexus.testkit.errors.files + +import ch.epfl.bluebrain.nexus.testkit.CirceLiteral.circeLiteralSyntax + +object FileErrors { + + def fileIsNotDeprecatedError(id: String) = + json""" + { + "@context" : "https://bluebrain.github.io/nexus/contexts/error.json", + "@type" : "FileIsNotDeprecated", + "reason" : "File '$id' is not deprecated." + } + """ + + def fileAlreadyExistsError(id: String) = + json""" + { + "@context": "https://bluebrain.github.io/nexus/contexts/error.json", + "@type": "ResourceAlreadyExists", + "reason": "Resource '$id' already exists in project 'org/proj'." + } + """ + +} From ab7bc755c7ce9f4068b4c89c2dbe75bac971f033 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Wed, 8 Nov 2023 09:02:23 +0100 Subject: [PATCH 22/22] Add dox --- .../delta/api/assets/files/undeprecate.sh | 2 ++ .../delta/api/assets/files/undeprecated.json | 33 +++++++++++++++++++ .../main/paradox/docs/delta/api/files-api.md | 22 ++++++++++++- .../docs/releases/v1.9-release-notes.md | 8 +++++ 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 docs/src/main/paradox/docs/delta/api/assets/files/undeprecate.sh create mode 100644 docs/src/main/paradox/docs/delta/api/assets/files/undeprecated.json diff --git a/docs/src/main/paradox/docs/delta/api/assets/files/undeprecate.sh b/docs/src/main/paradox/docs/delta/api/assets/files/undeprecate.sh new file mode 100644 index 0000000000..72d8f30ace --- /dev/null +++ b/docs/src/main/paradox/docs/delta/api/assets/files/undeprecate.sh @@ -0,0 +1,2 @@ +curl -X PUT \ + "http://localhost:8080/v1/files/myorg/myproject/myfile/undeprecate?rev=4" \ No newline at end of file diff --git a/docs/src/main/paradox/docs/delta/api/assets/files/undeprecated.json b/docs/src/main/paradox/docs/delta/api/assets/files/undeprecated.json new file mode 100644 index 0000000000..a005b28216 --- /dev/null +++ b/docs/src/main/paradox/docs/delta/api/assets/files/undeprecated.json @@ -0,0 +1,33 @@ +{ + "@context": [ + "https://bluebrain.github.io/nexus/contexts/files.json", + "https://bluebrain.github.io/nexus/contexts/metadata.json" + ], + "@id": "http://localhost:8080/v1/resources/myorg/myproject/_/myfile", + "@type": "File", + "_bytes": 13896460, + "_constrainedBy": "https://bluebrain.github.io/nexus/schemas/files.json", + "_createdAt": "2021-05-12T07:30:54.576Z", + "_createdBy": "http://localhost:8080/v1/anonymous", + "_deprecated": false, + "_digest": { + "_algorithm": "SHA-256", + "_value": "4c9f4292e3c0c5fc23cd60722adb8a1535f1dd7f0cf9203140d61fb889eef3cf" + }, + "_filename": "myfile2.pdf", + "_incoming": "http://localhost:8080/v1/files/myorg/myproject/myfile/incoming", + "_mediaType": "application/pdf", + "_origin": "Client", + "_outgoing": "http://localhost:8080/v1/files/myorg/myproject/myfile/outgoing", + "_project": "http://localhost:8080/v1/projects/myorg/myproject", + "_rev": 5, + "_self": "http://localhost:8080/v1/files/myorg/myproject/myfile", + "_storage": { + "@id": "https://bluebrain.github.io/nexus/vocabulary/diskStorageDefault", + "@type": "DiskStorage", + "_rev": 1 + }, + "_updatedAt": "2021-05-12T08:09:23.658Z", + "_updatedBy": "http://localhost:8080/v1/anonymous", + "_uuid": "3e86d93a-c196-407d-a13c-cea7168e32e3" +} diff --git a/docs/src/main/paradox/docs/delta/api/files-api.md b/docs/src/main/paradox/docs/delta/api/files-api.md index 3994456871..1d326b3a7b 100644 --- a/docs/src/main/paradox/docs/delta/api/files-api.md +++ b/docs/src/main/paradox/docs/delta/api/files-api.md @@ -237,7 +237,7 @@ Locks the file, so no further operations can be performed. Deprecating a file is considered to be an update as well. ``` -DELETE /v1/files/{org_label}/{project_label}?rev={previous_rev} +DELETE /v1/files/{org_label}/{project_label}/{file_id}?rev={previous_rev} ``` ... where `{previous_rev}` is the last known revision number for the file. @@ -250,6 +250,26 @@ Request Response : @@snip [deprecated.json](assets/files/deprecated.json) +## Undeprecate + +Unlocks a previously deprecated file. Further operations can then be performed. The file will again be found when listing/querying. + +Undeprecating a file is considered to be an update as well. + +``` +PUT /v1/file/{org_label}/{project_label}/{file_id}/undeprecate?rev={previous_rev} +``` + +... where `{previous_rev}` is the last known revision number for the resource. + +**Example** + +Request +: @@snip [undeprecate.sh](assets/files/undeprecate.sh) + +Response +: @@snip [undeprecated.json](assets/files/undeprecated.json) + ## Fetch When fetching a file, the response format can be chosen through HTTP content negotiation. 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 2424326104..f42d0c7752 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 @@ -174,6 +174,14 @@ These should instead be defined in the Delta configuration. ### Files +#### Undeprecate files + +Previously deprecated files can now be undeprecated. + +@ref:[More information](../delta/api/files-api.md#undeprecate) + +#### Automatic media type configuration + The automatic detection of the media type can now be customized at the Delta level. NB: The media type provided by the user still prevails over automatic detection.