diff --git a/build.sbt b/build.sbt index 62ffcbb6ff..0d2a651349 100644 --- a/build.sbt +++ b/build.sbt @@ -242,6 +242,76 @@ lazy val valueObjects = project testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")) ) +// Role projects + +lazy val roleInterface = project + .in(file("dsp-role/interface")) + .settings( + scalacOptions ++= Seq( + "-feature", + "-unchecked", + "-deprecation", + "-Yresolve-term-conflict:package", + "-Ymacro-annotations" + ), + name := "roleInterface", + libraryDependencies ++= Dependencies.roleInterfaceLibraryDependencies, + testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")) + ) + .dependsOn(shared, roleHandler) + +lazy val roleHandler = project + .in(file("dsp-role/handler")) + .settings( + scalacOptions ++= Seq( + "-feature", + "-unchecked", + "-deprecation", + "-Yresolve-term-conflict:package", + "-Ymacro-annotations" + ), + name := "roleHandler", + libraryDependencies ++= Dependencies.roleHandlerLibraryDependencies, + testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")) + ) + .dependsOn( + shared, + roleCore % "compile->compile;test->test", + roleRepo % "test->test" + ) + +lazy val roleRepo = project + .in(file("dsp-role/repo")) + .settings( + scalacOptions ++= Seq( + "-feature", + "-unchecked", + "-deprecation", + "-Yresolve-term-conflict:package", + "-Ymacro-annotations" + ), + name := "roleRepo", + libraryDependencies ++= Dependencies.roleRepoLibraryDependencies, + testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")) + ) + .dependsOn(shared, roleCore % "compile->compile;test->test") + +lazy val roleCore = project + .in(file("dsp-role/core")) + .settings( + scalacOptions ++= Seq( + "-feature", + "-unchecked", + "-deprecation", + "-Yresolve-term-conflict:package", + "-Ymacro-annotations" + ), + name := "roleCore", + libraryDependencies ++= Dependencies.roleCoreLibraryDependencies, + testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")) + ) + .dependsOn(shared) + // User projects lazy val userInterface = project @@ -312,6 +382,8 @@ lazy val userCore = project ) .dependsOn(shared) +// Shared project + lazy val shared = project .in(file("dsp-shared")) .settings( diff --git a/dsp-role/core/src/main/scala/dsp/role/api/RoleRepo.scala b/dsp-role/core/src/main/scala/dsp/role/api/RoleRepo.scala new file mode 100644 index 0000000000..f43a7ad67e --- /dev/null +++ b/dsp-role/core/src/main/scala/dsp/role/api/RoleRepo.scala @@ -0,0 +1,53 @@ +/* + * Copyright © 2021 - 2022 Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.api + +import dsp.role.domain.Role +import dsp.valueobjects.Id.RoleId +import zio._ +import zio.macros.accessible + +/** + * The trait (interface) for the role repository. + * The role repository is responsible for storing and retrieving roles. + * Needs to be used by the role repository implementations. + */ +@accessible +trait RoleRepo { + + /** + * Writes a role into the repository, while both creating or updating a role. + * + * @param r the [[Role]] to write + * @return the [[RoleId]] + */ + def storeRole(role: Role): UIO[RoleId] + + /** + * Gets all roles from the repository. + * + * @return a list of [[Role]] + */ + def getRoles(): UIO[List[Role]] + + /** + * Retrieves a role from the repository. + * + * @param id the role's ID + * @return the [[Role]] if found + */ + def getRoleById(id: RoleId): IO[Option[Nothing], Role] + + // should the role name be unique like username??? + + /** + * Deletes the [[Role]] from the repository by its [[RoleId]] + * + * @param id the role ID + * @return the [[RoleId]] of the deleted role, if found + */ + def deleteRole(id: RoleId): IO[Option[Nothing], RoleId] +} diff --git a/dsp-role/core/src/main/scala/dsp/role/domain/RoleDomain.scala b/dsp-role/core/src/main/scala/dsp/role/domain/RoleDomain.scala new file mode 100644 index 0000000000..da67c2ba17 --- /dev/null +++ b/dsp-role/core/src/main/scala/dsp/role/domain/RoleDomain.scala @@ -0,0 +1,122 @@ +/* + * Copyright © 2021 - 2022 Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.domain + +import dsp.errors.BadRequestException +import dsp.valueobjects.Id.RoleId +import dsp.valueobjects.Id.UserId +import dsp.valueobjects.Permission +import dsp.valueobjects.Role._ +import zio.prelude.Validation + +case class RoleUser( + id: UserId +) + +/** + * Role's domain entity. + * + * @param id the role's ID + * @param name the role's name + * @param description the role's description + * @param users the role's users + * @param permission the role's permission + */ +sealed abstract case class Role private ( + id: RoleId, + name: LangString, + description: LangString, + users: List[RoleUser], // how to reperesnt the user here? + permission: Permission +) extends Ordered[Role] { self => + + /** + * Allows to compare the [[Role]] instances. + * + * @param that [[Role]] to compare + * @return [[Boolean]] value + */ + def compare(that: Role): Int = self.id.iri.toString().compareTo(that.id.iri.toString()) + + /** + * Updates the role's name with new value. + * + * @param newValue new role's name to update + * @return updated [[Role]] + */ + def updateName(newValue: LangString): Validation[BadRequestException, Role] = + Role.make( + self.id, + newValue, + self.description, + self.users, + self.permission + ) + + /** + * Updates the role's description with new value. + * + * @param newValue new role's description to update + * @return updated [[Role]] + */ + def updateDescription(newValue: LangString): Validation[BadRequestException, Role] = + Role.make( + self.id, + self.name, + newValue, + self.users, + self.permission + ) + + /** + * Updates the role's users with new value. + * + * @param newValue new role's users to update + * @return updated [[Role]] + */ + def updateUsers(newValue: List[RoleUser]): Validation[BadRequestException, Role] = + Role.make( + self.id, + self.name, + self.description, + newValue, + self.permission + ) + + /** + * Updates the role's permission with new value. + * + * @param newValue new role's permission to update + * @return updated [[Role]] + */ + def updatePermission(newValue: Permission): Validation[BadRequestException, Role] = + Role.make( + self.id, + self.name, + self.description, + self.users, + newValue + ) +} + +object Role { + def make( + id: RoleId, + name: LangString, + description: LangString, + users: List[RoleUser], + permission: Permission + ): Validation[BadRequestException, Role] = + Validation.succeed( + new Role( + id, + name, + description, + users, + permission + ) {} + ) +} diff --git a/dsp-role/core/src/test/scala/dsp/role/api/RoleApiSpec.scala b/dsp-role/core/src/test/scala/dsp/role/api/RoleApiSpec.scala new file mode 100644 index 0000000000..4f52163360 --- /dev/null +++ b/dsp-role/core/src/test/scala/dsp/role/api/RoleApiSpec.scala @@ -0,0 +1,11 @@ +/* + * Copyright © 2021 - 2022 Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.api + +/** + * To be implemented... + */ +object placeholder {} diff --git a/dsp-role/core/src/test/scala/dsp/role/domain/RoleDomainSpec.scala b/dsp-role/core/src/test/scala/dsp/role/domain/RoleDomainSpec.scala new file mode 100644 index 0000000000..8be54029c1 --- /dev/null +++ b/dsp-role/core/src/test/scala/dsp/role/domain/RoleDomainSpec.scala @@ -0,0 +1,111 @@ +/* + * Copyright © 2021 - 2022 Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.domain + +import dsp.role.sharedtestdata.RoleTestData +import dsp.valueobjects.Id +import dsp.valueobjects.Permission +import dsp.valueobjects.Role._ +import zio.test._ + +/** + * This spec is used to test [[RoleDomain]]. + */ +object RoleDomainSpec extends ZIOSpecDefault { + def spec = (compareRolesTest + createRoleTest + updateRoleTest) + + private val compareRolesTest = suite("compareRoles")( + test("compare two roles") { + val role = RoleTestData.role1 + val equalRole = RoleTestData.role1 + val nonEqualRole = RoleTestData.role2 + + assertTrue(role == equalRole) && + assertTrue(role != nonEqualRole) + } + ) + + private val createRoleTest = suite("createRole")( + test("create a role") { + ( + for { + id <- RoleTestData.id1 + name <- RoleTestData.name1 + description <- RoleTestData.description1 + users = RoleTestData.users1 + permission <- RoleTestData.permission1 + + role <- Role.make( + id, + name, + description, + users = users, + permission + ) + } yield assertTrue(role.id == id) && + assertTrue(role.name == name) && + assertTrue(role.description == description) && + assertTrue(role.users == users) && + assertTrue(role.permission == permission) + ).toZIO + } + ) + + private val updateRoleTest = suite("updateRole")( + test("update the name") { + ( + for { + role <- RoleTestData.role1 + newValue <- LangString.make("newRoleName", "en") + updatedRole <- role.updateName(newValue) + } yield assertTrue(updatedRole.name == newValue) && + assertTrue(updatedRole.name != role.name) && + assertTrue(updatedRole.description == role.description) && + assertTrue(updatedRole.users == role.users) && + assertTrue(updatedRole.permission == role.permission) + ).toZIO + }, + test("update the description") { + ( + for { + role <- RoleTestData.role1 + newValue <- LangString.make("New Role Description", "en") + updatedRole <- role.updateDescription(newValue) + } yield assertTrue(updatedRole.name == role.name) && + assertTrue(updatedRole.description == newValue) && + assertTrue(updatedRole.description != role.description) && + assertTrue(updatedRole.users == role.users) && + assertTrue(updatedRole.permission == role.permission) + ).toZIO + }, + test("update the users") { + ( + for { + role <- RoleTestData.role1 + newValue = List(RoleUser(Id.UserId.make().fold(e => throw e.head, v => v))) + updatedRole <- role.updateUsers(newValue) + } yield assertTrue(updatedRole.name == role.name) && + assertTrue(updatedRole.description == role.description) && + assertTrue(updatedRole.users == newValue) && + assertTrue(updatedRole.users != role.users) && + assertTrue(updatedRole.permission == role.permission) + ).toZIO + }, + test("update the permission") { + ( + for { + role <- RoleTestData.role1 + newValue <- Permission.make(Permission.Create) + updatedRole <- role.updatePermission(newValue) + } yield assertTrue(updatedRole.name == role.name) && + assertTrue(updatedRole.description == role.description) && + assertTrue(updatedRole.users == role.users) && + assertTrue(updatedRole.permission == newValue) && + assertTrue(updatedRole.permission != role.permission) + ).toZIO + } + ) +} diff --git a/dsp-role/core/src/test/scala/dsp/role/sharedtestdata/RoleTestData.scala b/dsp-role/core/src/test/scala/dsp/role/sharedtestdata/RoleTestData.scala new file mode 100644 index 0000000000..900e8cf27e --- /dev/null +++ b/dsp-role/core/src/test/scala/dsp/role/sharedtestdata/RoleTestData.scala @@ -0,0 +1,59 @@ +/* + * Copyright © 2021 - 2022 Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.sharedtestdata + +import dsp.role.domain.Role +import dsp.role.domain.RoleUser +import dsp.valueobjects.Id +import dsp.valueobjects.Permission +import dsp.valueobjects.Role._ + +/** + * Contains shared role test data. + */ +object RoleTestData { + val id1 = Id.RoleId.make() + val name1 = LangString.make("Name", "en") + val description1 = LangString.make("Description", "en") + val users1 = List(RoleUser(Id.UserId.make().fold(e => throw e.head, v => v))) + val permission1 = Permission.make(Permission.View) + + val role1 = for { + id <- id1 + name <- name1 + description <- description1 + permission <- permission1 + + role <- Role.make( + id, + name, + description, + users = users1, + permission + ) + } yield role + + val id2 = Id.RoleId.make() + val name2 = LangString.make("Name 2", "en") + val description2 = LangString.make("Description 2", "en") + val users2 = List(RoleUser(Id.UserId.make().fold(e => throw e.head, v => v))) + val permission2 = Permission.make(Permission.Admin) + + val role2 = for { + id <- id2 + name <- name2 + description <- description2 + permission <- permission2 + + role <- Role.make( + id, + name, + description, + users = users2, + permission + ) + } yield role +} diff --git a/dsp-role/handler/src/main/scala/dsp/role/handler/RoleHandler.scala b/dsp-role/handler/src/main/scala/dsp/role/handler/RoleHandler.scala new file mode 100644 index 0000000000..981c24f794 --- /dev/null +++ b/dsp-role/handler/src/main/scala/dsp/role/handler/RoleHandler.scala @@ -0,0 +1,148 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.handler + +import dsp.errors.NotFoundException +import dsp.errors.RequestRejectedException +import dsp.role.api.RoleRepo +import dsp.role.domain.Role +import dsp.role.domain.RoleUser +import dsp.valueobjects.Id.RoleId +import dsp.valueobjects.Permission +import dsp.valueobjects.Role._ +import zio.ZIO +import zio._ + +/** + * The role handler. + * + * @param repo the role repository + */ +final case class RoleHandler(repo: RoleRepo) { + + /** + * Retrieves all roles (sorted by IRI). + */ + def getRoles(): UIO[List[Role]] = + repo + .getRoles() + .map(_.sorted) + .tap(_ => ZIO.logInfo("Retrieved all roles.")) + + /** + * Retrieves the role by ID. + * + * @param id of the role to be retrieved + */ + def getRoleById(id: RoleId): IO[NotFoundException, Role] = + for { + role <- repo + .getRoleById(id) + .mapError(_ => NotFoundException(s"Not found the role with ID: $id")) + .tap(_ => ZIO.logInfo(s"Found the role by ID: $id")) + } yield role + + /** + * Creates a new role. + * + * @param name the role's name + * @param description the role's description + * @param users the role's users + * @param permission the role's permission + * @return the [[RoleId]] if succeed, [[Throwable]] if fail + */ + def createRole( + name: LangString, + description: LangString, + users: List[RoleUser], + permission: Permission + ): IO[Throwable, RoleId] = + (for { + id <- RoleId.make().toZIO + role <- Role.make(id, name, description, users, permission).toZIO + storedRole <- repo.storeRole(role) + } yield storedRole).tap(id => ZIO.logInfo(s"Created role with ID: $id")) + + /** + * Updates the name of a role. + * + * @param id the roles's ID + * @param newValue the new name value + * @return the [[RoleId]] if succeed, [[RequestRejectedException]] if fail + */ + def updateName(id: RoleId, newValue: LangString): IO[RequestRejectedException, RoleId] = + (for { + role <- getRoleById(id) + updatedRole <- role.updateName(newValue).toZIO + _ <- repo.storeRole(updatedRole) + } yield id).tap(_ => ZIO.logInfo(s"Updated name with new value: ${newValue.value}")) + + /** + * Updates the description of a role. + * + * @param id the roles's ID + * @param newValue the new description value + * @return the [[RoleId]] if succeed, [[RequestRejectedException]] if fail + */ + def updateDescription(id: RoleId, newValue: LangString): IO[RequestRejectedException, RoleId] = + (for { + role <- getRoleById(id) + updatedRole <- role.updateDescription(newValue).toZIO + _ <- repo.storeRole(updatedRole) + } yield id).tap(_ => ZIO.logInfo(s"Updated description with new value: $newValue")) + + /** + * Updates the users of a role. + * + * @param id the roles's ID + * @param newValue the new users value + * @return the [[RoleId]] if succeed, [[RequestRejectedException]] if fail + */ + def updateUsers(id: RoleId, newValue: List[RoleUser]): IO[RequestRejectedException, RoleId] = + (for { + role <- getRoleById(id) + updatedRole <- role.updateUsers(newValue).toZIO + _ <- repo.storeRole(updatedRole) + } yield id).tap(_ => ZIO.logInfo(s"Updated users with new value: $newValue")) + + /** + * Updates the permission of a role. + * + * @param id the roles's ID + * @param newValue the new permission value + * @return the [[RoleId]] if succeed, [[RequestRejectedException]] if fail + */ + def updatePermission(id: RoleId, newValue: Permission): IO[RequestRejectedException, RoleId] = + (for { + role <- getRoleById(id) + updatedRole <- role.updatePermission(newValue).toZIO + _ <- repo.storeRole(updatedRole) + } yield id).tap(_ => ZIO.logInfo(s"Updated permission with new value: $newValue")) + + /** + * Deletes the role. + * + * @param id the roles's ID + * @return the [[RoleId]] if succeed, [[NotFoundException]] if fail + */ + def deleteRole(id: RoleId): IO[NotFoundException, RoleId] = + (for { + _ <- repo.deleteRole(id).mapError(_ => NotFoundException(s"Not found the role with ID: $id")) + } yield id).tap(_ => ZIO.logInfo(s"Deleted role with ID: $id")) +} + +/** + * Companion object providing the layer with an initialized implementation + */ +object RoleHandler { + val layer: ZLayer[RoleRepo, Nothing, RoleHandler] = { + ZLayer { + for { + repo <- ZIO.service[RoleRepo] + } yield RoleHandler(repo) + }.tap(_ => ZIO.logInfo(">>> Role Handler initilaized <<<")) + } +} diff --git a/dsp-role/handler/src/test/scala/dsp/role/handler/RoleHandlerSpec.scala b/dsp-role/handler/src/test/scala/dsp/role/handler/RoleHandlerSpec.scala new file mode 100644 index 0000000000..86e42cca83 --- /dev/null +++ b/dsp-role/handler/src/test/scala/dsp/role/handler/RoleHandlerSpec.scala @@ -0,0 +1,193 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.handler + +import dsp.errors.NotFoundException +import dsp.errors.RequestRejectedException +import dsp.role.api.RoleRepo +import dsp.role.domain.Role +import dsp.role.domain.RoleUser +import dsp.role.repo.impl.RoleRepoMock +import dsp.role.sharedtestdata.RoleTestData +import dsp.valueobjects.Id.RoleId +import dsp.valueobjects.Permission +import dsp.valueobjects.Role._ +import zio.ZIO +import zio._ +import zio.test._ + +/** + * This spec is used to test [[RoleHandler]] + */ +object RoleHandlerSpec extends ZIOSpecDefault { + def spec = (getRolesTest + updateRoleTest + deleteRoleTest) + + private val getRolesTest = suite("getRoles")( + test("return an empty map while trying to get all roles but there are none") { + for { + handler <- ZIO.service[RoleHandler] + roles <- handler.getRoles() + } yield assertTrue(roles.size == 0) + }, + test("store some roles and retrive them all") { + for { + handler <- ZIO.service[RoleHandler] + + name1 <- RoleTestData.name1.toZIO + description1 <- RoleTestData.description1.toZIO + users1 = RoleTestData.users1 + permission1 <- RoleTestData.permission1.toZIO + + name2 <- RoleTestData.name2.toZIO + description2 <- RoleTestData.description2.toZIO + users2 = RoleTestData.users2 + permission2 <- RoleTestData.permission2.toZIO + + _ <- handler.createRole( + name1, + description1, + users1, + permission1 + ) + + _ <- handler.createRole( + name2, + description2, + users2, + permission2 + ) + + roles <- handler.getRoles() + } yield assertTrue(roles.size == 2) + } + ).provide(RoleRepoMock.layer, RoleHandler.layer) + + private val updateRoleTest = suite("updateRole")( + test("store a role and update its name") { + for { + handler <- ZIO.service[RoleHandler] + + newValue <- LangString.make("New Role Name", "en").toZIO + + name1 <- RoleTestData.name1.toZIO + description1 <- RoleTestData.description1.toZIO + users1 = RoleTestData.users1 + permission1 <- RoleTestData.permission1.toZIO + + roleId <- handler.createRole( + name1, + description1, + users1, + permission1 + ) + + updatedRoleId <- handler.updateName(roleId, newValue) + retrievedRole <- handler.getRoleById(updatedRoleId) + } yield assertTrue(retrievedRole.name == newValue) && + assertTrue(retrievedRole.description == description1) && + assertTrue(retrievedRole.users == users1) && + assertTrue(retrievedRole.permission == permission1) + }, + test("store a role and update its description") { + for { + handler <- ZIO.service[RoleHandler] + + newValue <- LangString.make("New Role Description", "en").toZIO + + name1 <- RoleTestData.name1.toZIO + description1 <- RoleTestData.description1.toZIO + users1 = RoleTestData.users1 + permission1 <- RoleTestData.permission1.toZIO + + roleId <- handler.createRole( + name1, + description1, + users1, + permission1 + ) + + updatedRoleId <- handler.updateDescription(roleId, newValue) + retrievedRole <- handler.getRoleById(updatedRoleId) + } yield assertTrue(retrievedRole.name == name1) && + assertTrue(retrievedRole.description == newValue) && + assertTrue(retrievedRole.users == users1) && + assertTrue(retrievedRole.permission == permission1) + }, + test("store a role and update its name") { + for { + handler <- ZIO.service[RoleHandler] + + newValue <- LangString.make("New Role Name", "en").toZIO + + name1 <- RoleTestData.name1.toZIO + description1 <- RoleTestData.description1.toZIO + users1 = RoleTestData.users1 + permission1 <- RoleTestData.permission1.toZIO + + roleId <- handler.createRole( + name1, + description1, + users1, + permission1 + ) + + updatedRoleId <- handler.updateName(roleId, newValue) + retrievedRole <- handler.getRoleById(updatedRoleId) + } yield assertTrue(retrievedRole.name == newValue) && + assertTrue(retrievedRole.description == description1) && + assertTrue(retrievedRole.users == users1) && + assertTrue(retrievedRole.permission == permission1) + }, + test("store a role and update its permission") { + for { + handler <- ZIO.service[RoleHandler] + + newValue <- Permission.make(Permission.Create).toZIO + + name1 <- RoleTestData.name1.toZIO + description1 <- RoleTestData.description1.toZIO + users1 = RoleTestData.users1 + permission1 <- RoleTestData.permission1.toZIO + + roleId <- handler.createRole( + name1, + description1, + users1, + permission1 + ) + + updatedRoleId <- handler.updatePermission(roleId, newValue) + retrievedRole <- handler.getRoleById(updatedRoleId) + } yield assertTrue(retrievedRole.name == name1) && + assertTrue(retrievedRole.description == description1) && + assertTrue(retrievedRole.users == users1) && + assertTrue(retrievedRole.permission == newValue) + } + ).provide(RoleRepoMock.layer, RoleHandler.layer) + + private val deleteRoleTest = suite("deleteRole") { + test("store and delete a role") { + for { + handler <- ZIO.service[RoleHandler] + + name1 <- RoleTestData.name1.toZIO + description1 <- RoleTestData.description1.toZIO + users1 = RoleTestData.users1 + permission1 <- RoleTestData.permission1.toZIO + + roleId <- handler.createRole( + name1, + description1, + users1, + permission1 + ) + + deletedRoleId <- handler.deleteRole(roleId) + roles <- handler.getRoles() + } yield assertTrue(roles.size == 0) + } + }.provide(RoleRepoMock.layer, RoleHandler.layer) +} diff --git a/dsp-role/interface/src/main/scala/dsp/role/listener/external/RoleListenerExternal.scala b/dsp-role/interface/src/main/scala/dsp/role/listener/external/RoleListenerExternal.scala new file mode 100644 index 0000000000..034906962c --- /dev/null +++ b/dsp-role/interface/src/main/scala/dsp/role/listener/external/RoleListenerExternal.scala @@ -0,0 +1,11 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.listener.external + +/** + * To be implemented... + */ +object placeholder {} diff --git a/dsp-role/interface/src/main/scala/dsp/role/listener/internal/RoleListenerInternal.scala b/dsp-role/interface/src/main/scala/dsp/role/listener/internal/RoleListenerInternal.scala new file mode 100644 index 0000000000..7cae25efca --- /dev/null +++ b/dsp-role/interface/src/main/scala/dsp/role/listener/internal/RoleListenerInternal.scala @@ -0,0 +1,11 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.listener.internal + +/** + * To be implemented... + */ +object placeholder {} diff --git a/dsp-role/interface/src/main/scala/dsp/role/route/RoleRoute.scala b/dsp-role/interface/src/main/scala/dsp/role/route/RoleRoute.scala new file mode 100644 index 0000000000..66c2294cc2 --- /dev/null +++ b/dsp-role/interface/src/main/scala/dsp/role/route/RoleRoute.scala @@ -0,0 +1,11 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.route + +/** + * To be implemented... + */ +object placeholder {} diff --git a/dsp-role/interface/src/test/scala/dsp/role/listener/external/RoleListenerExternalSpec.scala b/dsp-role/interface/src/test/scala/dsp/role/listener/external/RoleListenerExternalSpec.scala new file mode 100644 index 0000000000..034906962c --- /dev/null +++ b/dsp-role/interface/src/test/scala/dsp/role/listener/external/RoleListenerExternalSpec.scala @@ -0,0 +1,11 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.listener.external + +/** + * To be implemented... + */ +object placeholder {} diff --git a/dsp-role/interface/src/test/scala/dsp/role/listener/internal/RoleListenerInternalSpec.scala b/dsp-role/interface/src/test/scala/dsp/role/listener/internal/RoleListenerInternalSpec.scala new file mode 100644 index 0000000000..7cae25efca --- /dev/null +++ b/dsp-role/interface/src/test/scala/dsp/role/listener/internal/RoleListenerInternalSpec.scala @@ -0,0 +1,11 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.listener.internal + +/** + * To be implemented... + */ +object placeholder {} diff --git a/dsp-role/interface/src/test/scala/dsp/role/route/RoleRouteSpec.scala b/dsp-role/interface/src/test/scala/dsp/role/route/RoleRouteSpec.scala new file mode 100644 index 0000000000..66c2294cc2 --- /dev/null +++ b/dsp-role/interface/src/test/scala/dsp/role/route/RoleRouteSpec.scala @@ -0,0 +1,11 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.route + +/** + * To be implemented... + */ +object placeholder {} diff --git a/dsp-role/repo/src/main/scala/dsp/role/repo/impl/RoleRepoLive.scala b/dsp-role/repo/src/main/scala/dsp/role/repo/impl/RoleRepoLive.scala new file mode 100644 index 0000000000..b325faa455 --- /dev/null +++ b/dsp-role/repo/src/main/scala/dsp/role/repo/impl/RoleRepoLive.scala @@ -0,0 +1,73 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.repo.impl + +import dsp.role.api.RoleRepo +import dsp.role.domain.Role +import dsp.valueobjects.Id.RoleId +import zio._ +import zio.stm.TMap + +import java.util.UUID + +/** + * Role repo live implementation. + * + * @param roles a map of roles + */ +final case class RoleRepoLive( + roles: TMap[UUID, Role] +) extends RoleRepo { + + /** + * @inheritDoc + */ + override def storeRole(r: Role): UIO[RoleId] = + (for { + _ <- roles.put(r.id.uuid, r) + } yield r.id).commit.tap(_ => ZIO.logInfo(s"Stored role with ID: ${r.id.uuid}")) + + /** + * @inheritDoc + */ + override def getRoles(): UIO[List[Role]] = + roles.values.commit.tap(rolesList => ZIO.logInfo(s"Number of roles found: ${rolesList.size}")) + + /** + * @inheritDoc + */ + override def getRoleById(id: RoleId): IO[Option[Nothing], Role] = + roles + .get(id.uuid) + .commit + .some + .tapBoth( + _ => ZIO.logInfo(s"Not found the role with ID: ${id.uuid}"), + _ => ZIO.logInfo(s"Found role by ID: ${id.uuid}") + ) + + /** + * @inheritDoc + */ + override def deleteRole(id: RoleId): IO[Option[Nothing], RoleId] = + (for { + role <- roles.get(id.uuid).some + _ <- roles.delete(id.uuid) + } yield id).commit.tap(_ => ZIO.logInfo(s"Deleted role: ${id.uuid}")) +} + +/** + * Companion object providing the layer with an initialized implementation of [[RoleRepo]] + */ +object RoleRepoLive { + val layer: ZLayer[Any, Nothing, RoleRepo] = { + ZLayer { + for { + roles <- TMap.empty[UUID, Role].commit + } yield RoleRepoLive(roles) + }.tap(_ => ZIO.logInfo(">>> Role repository initialized <<<")) + } +} diff --git a/dsp-role/repo/src/test/scala/dsp/role/repo/impl/RoleRepoImplSpec.scala b/dsp-role/repo/src/test/scala/dsp/role/repo/impl/RoleRepoImplSpec.scala new file mode 100644 index 0000000000..b7e2d4f658 --- /dev/null +++ b/dsp-role/repo/src/test/scala/dsp/role/repo/impl/RoleRepoImplSpec.scala @@ -0,0 +1,60 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.repo.impl + +import dsp.role.api.RoleRepo +import dsp.role.repo.impl.RoleRepoLive +import dsp.role.repo.impl.RoleRepoMock +import dsp.role.sharedtestdata.RoleTestData +import zio._ +import zio.test.Assertion._ +import zio.test._ + +/** + * This spec is used to test all [[RoleRepo]] implementations. + */ +object RoleRepoImplSpec extends ZIOSpecDefault { + + def spec = (roleRepoMockTests + roleRepoLiveTests) + + private val role1 = RoleTestData.role1 + private val role2 = RoleTestData.role2 + + val roleTests = + test("store two roles and retrieve them all") { + for { + role1 <- role1.toZIO + role2 <- role2.toZIO + _ <- RoleRepo.storeRole(role1) + _ <- RoleRepo.storeRole(role2) + roles <- RoleRepo.getRoles() + } yield assertTrue(roles.size == 2) + } + + test("store a role and get it by ID") { + for { + role1 <- role1.toZIO + _ <- RoleRepo.storeRole(role1) + role <- RoleRepo.getRoleById(role1.id) + } yield assertTrue(role == role1) + } + + test("store and delete a role") { + for { + role1 <- role1.toZIO + roleId <- RoleRepo.storeRole(role1) + idOfDeletedRole <- RoleRepo.deleteRole(roleId) + isIdDeleted <- RoleRepo.getRoleById(idOfDeletedRole).exit + } yield assertTrue(roleId == role1.id) && + assert(isIdDeleted)(fails(equalTo(None))) + } + + val roleRepoMockTests = suite("RoleRepoMock")( + roleTests + ).provide(RoleRepoMock.layer) + + val roleRepoLiveTests = suite("RoleRepoLive")( + roleTests + ).provide(RoleRepoLive.layer) +} diff --git a/dsp-role/repo/src/test/scala/dsp/role/repo/impl/RoleRepoMock.scala b/dsp-role/repo/src/test/scala/dsp/role/repo/impl/RoleRepoMock.scala new file mode 100644 index 0000000000..a814717551 --- /dev/null +++ b/dsp-role/repo/src/test/scala/dsp/role/repo/impl/RoleRepoMock.scala @@ -0,0 +1,73 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.role.repo.impl + +import dsp.role.api.RoleRepo +import dsp.role.domain.Role +import dsp.valueobjects.Id.RoleId +import zio._ +import zio.stm.TMap + +import java.util.UUID + +/** + * Role repo test implementation. Mocks the [[RoleRepo]] for tests. + * + * @param roles a map of roles + */ +final case class RoleRepoMock( + roles: TMap[UUID, Role] +) extends RoleRepo { + + /** + * @inheritDoc + */ + override def storeRole(r: Role): UIO[RoleId] = + (for { + _ <- roles.put(r.id.uuid, r) + } yield r.id).commit.tap(_ => ZIO.logInfo(s"Stored role with ID: ${r.id.uuid}")) + + /** + * @inheritDoc + */ + override def getRoles(): UIO[List[Role]] = + roles.values.commit.tap(rolesList => ZIO.logInfo(s"Number of roles found: ${rolesList.size}")) + + /** + * @inheritDoc + */ + override def getRoleById(id: RoleId): IO[Option[Nothing], Role] = + roles + .get(id.uuid) + .commit + .some + .tapBoth( + _ => ZIO.logInfo(s"Not found the role with ID: ${id.uuid}"), + _ => ZIO.logInfo(s"Found role by ID: ${id.uuid}") + ) + + /** + * inheritDoc + */ + override def deleteRole(id: RoleId): IO[Option[Nothing], RoleId] = + (for { + role <- roles.get(id.uuid).some + _ <- roles.delete(id.uuid) + } yield id).commit.tap(_ => ZIO.logInfo(s"Deleted role: ${id.uuid}")) +} + +/** + * Companion object providing the layer with an initialized implementation of [[RoleRepo]] + */ +object RoleRepoMock { + val layer: ZLayer[Any, Nothing, RoleRepo] = { + ZLayer { + for { + roles <- TMap.empty[UUID, Role].commit + } yield RoleRepoMock(roles) + }.tap(_ => ZIO.logInfo(">>> In-memory role repository initialized <<<")) + } +} diff --git a/dsp-shared/src/main/scala/dsp/valueobjects/Id.scala b/dsp-shared/src/main/scala/dsp/valueobjects/Id.scala index 1e9e6da3f5..6e0998ece2 100644 --- a/dsp-shared/src/main/scala/dsp/valueobjects/Id.scala +++ b/dsp-shared/src/main/scala/dsp/valueobjects/Id.scala @@ -13,6 +13,54 @@ import java.util.UUID sealed trait Id object Id { + /** + * Stores the role's ID. + * + * @param uuid the role's UUID + * @param iri the role's IRI + */ + sealed abstract case class RoleId private ( + uuid: UUID, + iri: Iri.RoleIri + ) extends Id + + object RoleId { + private val roleIriPrefix = "http://rdfh.ch/roles/" + + /** + * Generates a RoleId instance with a UUID and a given IRI. + * + * @param iri the role's IRI used to create RoleId + * @return new RoleId instance + */ + def fromIri(iri: Iri.RoleIri): Validation[Throwable, RoleId] = { + val uuid: UUID = UUID.fromString(iri.value.split("/").last) + Validation.succeed(new RoleId(uuid, iri) {}) + } + + /** + * Generates a RoleId instance with a UUID and a given IRI. + * + * @param uuid the UUID used to create RoleId + * @return new RoleId instance + */ + def fromUuid(uuid: UUID): Validation[Throwable, RoleId] = { + val iri = Iri.RoleIri.make(roleIriPrefix + uuid.toString).fold(e => throw e.head, v => v) + Validation.succeed(new RoleId(uuid, iri) {}) + } + + /** + * Generates a RoleId instance with new random UUID and a new IRI. + * + * @return new RoleId instance + */ + def make(): Validation[Throwable, RoleId] = { + val uuid = UUID.randomUUID() + val iri = Iri.RoleIri.make(roleIriPrefix + uuid.toString).fold(e => throw e.head, v => v) + Validation.succeed(new RoleId(uuid, iri) {}) + } + } + /** * Stores the user ID, i.e. UUID and IRI of the user * diff --git a/dsp-shared/src/main/scala/dsp/valueobjects/Iri.scala b/dsp-shared/src/main/scala/dsp/valueobjects/Iri.scala index 98a6a8535e..3fa760fe61 100644 --- a/dsp-shared/src/main/scala/dsp/valueobjects/Iri.scala +++ b/dsp-shared/src/main/scala/dsp/valueobjects/Iri.scala @@ -127,6 +127,34 @@ object Iri { } } + /** + * RoleIri value object. + */ + sealed abstract case class RoleIri private (value: String) extends Iri + object RoleIri { + def make(value: String): Validation[Throwable, RoleIri] = + if (value.isEmpty) { + Validation.fail(BadRequestException(IriErrorMessages.RoleIriMissing)) + } else { + val isUuid: Boolean = V2UuidValidation.hasUuidLength(value.split("/").last) + + if (!V2IriValidation.isKnoraRoleIriStr(value)) { + Validation.fail(BadRequestException(IriErrorMessages.RoleIriInvalid(value))) + } else if (isUuid && !V2UuidValidation.isUuidVersion4Or5(value)) { + Validation.fail(BadRequestException(IriErrorMessages.UuidVersionInvalid)) + } else { + val validatedValue = Validation( + V2IriValidation.validateAndEscapeIri( + value, + throw BadRequestException(IriErrorMessages.RoleIriInvalid(value)) + ) + ) + + validatedValue.map(new RoleIri(_) {}) + } + } + } + /** * UserIri value object. */ @@ -139,14 +167,14 @@ object Iri { val isUuid: Boolean = V2UuidValidation.hasUuidLength(value.split("/").last) if (!V2IriValidation.isKnoraUserIriStr(value)) { - Validation.fail(BadRequestException(IriErrorMessages.UserIriInvalid)) + Validation.fail(BadRequestException(IriErrorMessages.UserIriInvalid(value))) } else if (isUuid && !V2UuidValidation.isUuidVersion4Or5(value)) { Validation.fail(BadRequestException(IriErrorMessages.UuidVersionInvalid)) } else { val validatedValue = Validation( V2IriValidation.validateAndEscapeUserIri( value, - throw BadRequestException(IriErrorMessages.UserIriInvalid) + throw BadRequestException(IriErrorMessages.UserIriInvalid(value)) ) ) @@ -184,8 +212,10 @@ object IriErrorMessages { val ListIriInvalid = "List IRI is invalid" val ProjectIriMissing = "Project IRI cannot be empty." val ProjectIriInvalid = "Project IRI is invalid." + val RoleIriMissing = "Role IRI cannot be empty." + val RoleIriInvalid = (iri: String) => s"Role IRI: $iri is invalid." val UserIriMissing = "User IRI cannot be empty." - val UserIriInvalid = "User IRI is invalid." + val UserIriInvalid = (iri: String) => s"User IRI: $iri is invalid." val UuidVersionInvalid = "Invalid UUID used to create IRI. Only versions 4 and 5 are supported." val PropertyIriMissing = "Property IRI cannot be empty." } diff --git a/dsp-shared/src/main/scala/dsp/valueobjects/Role.scala b/dsp-shared/src/main/scala/dsp/valueobjects/Role.scala new file mode 100644 index 0000000000..2b3bfea427 --- /dev/null +++ b/dsp-shared/src/main/scala/dsp/valueobjects/Role.scala @@ -0,0 +1,94 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.valueobjects + +import dsp.errors.BadRequestException +import zio.prelude.Validation + +object Role { + + /** + * LangString value object. + * + * @param value the [[String]] the value to be validated + * @param isoCode the language ISO code to be validated + */ + sealed abstract case class LangString private (value: String, isoCode: String) + object LangString { + def isIsoCodeSupported(isoCode: String): Boolean = + V2.SupportedLanguageCodes.contains(isoCode.toLowerCase) // should only lower case be supported? + + def make(value: String, isoCode: String): Validation[Throwable, LangString] = + if (value.isEmpty) { + Validation.fail( + BadRequestException(RoleErrorMessages.LangStringValueMissing) + ) + } else if (isoCode.isEmpty) { + Validation.fail( + BadRequestException(RoleErrorMessages.LangStringIsoCodeMissing) + ) + } else if (!isIsoCodeSupported(isoCode)) { + Validation.fail( + BadRequestException(RoleErrorMessages.LangStringIsoCodeInvalid(isoCode)) + ) + } else { + val validatedValue = Validation( + V2IriValidation.toSparqlEncodedString( + value, + throw BadRequestException(RoleErrorMessages.LangStringValueInvalid(value)) + ) + ) + + validatedValue.map(new LangString(_, isoCode) {}) + } + } +} + +/** + * Permission value object. + * + * @param value the value to be validated + */ +sealed abstract case class Permission private (value: String) +object Permission { + val View: String = "view" + val Create: String = "create" + val Modify: String = "modify" + val Delete: String = "delete" + val Admin: String = "admin" + val availablePermissions: Set[String] = Set( + View, + Create, + Modify, + Delete, + Admin + ) + + def isPermissionAvailable(permission: String): Boolean = + availablePermissions.contains(permission.toLowerCase) + + def make(value: String): Validation[Throwable, Permission] = + if (value.isEmpty) { + Validation.fail( + BadRequestException(RoleErrorMessages.PermissionMissing) + ) + } else if (!isPermissionAvailable(value)) { + Validation.fail( + BadRequestException(RoleErrorMessages.PermissionInvalid(value)) + ) + } else { + Validation.succeed(new Permission(value) {}) + } +} + +object RoleErrorMessages { + val LangStringValueMissing = "Value cannot be empty." + val LangStringValueInvalid = (value: String) => s"String value: $value is invalid." + val LangStringIsoCodeMissing = "Language ISO code cannot be empty." + val LangStringIsoCodeInvalid = (isoCode: String) => s"Language ISO code: $isoCode is not suporrted." + val PermissionMissing = "Permission cannot be empty." + val PermissionInvalid = (value: String) => s"Permission: $value is invalid." +} diff --git a/dsp-shared/src/main/scala/dsp/valueobjects/V2.scala b/dsp-shared/src/main/scala/dsp/valueobjects/V2.scala index 26d1dc55aa..8dea3c274b 100644 --- a/dsp-shared/src/main/scala/dsp/valueobjects/V2.scala +++ b/dsp-shared/src/main/scala/dsp/valueobjects/V2.scala @@ -5,14 +5,15 @@ package dsp.valueobjects -import java.util.UUID -import java.util.Base64 -import java.nio.ByteBuffer -import scala.util.matching.Regex -import org.apache.commons.validator.routines.UrlValidator -import org.apache.commons.lang3.StringUtils import com.google.gwt.safehtml.shared.UriUtils.encodeAllowEscapes import dsp.errors.BadRequestException +import org.apache.commons.lang3.StringUtils +import org.apache.commons.validator.routines.UrlValidator + +import java.nio.ByteBuffer +import java.util.Base64 +import java.util.UUID +import scala.util.matching.Regex // TODO-mpro: don't forget to remove all occurances and additional "helper" // implementations in webapi project which needed to be added temporary in order @@ -73,6 +74,12 @@ object V2IriValidation { * @return the same string, escaped or unescaped as requested. */ def toSparqlEncodedString(s: String, errorFun: => Nothing): String = { +// Findings about this method: +// - there is more cases to handle according to docs (https://www.w3.org/TR/rdf-sparql-query#grammarEscapes) - we use 1.1 version right? +// - `'` doesn't appear on that list, but this method escapes it +// - why `\r` throws error instead of being escaped? +// - fun fact is that if I remove StringUtils.replaceEach, for example `\t` passes unescaped, why? + if (s.isEmpty || s.contains("\r")) errorFun // http://www.morelab.deusto.es/code_injection/ @@ -85,17 +92,20 @@ object V2IriValidation { } /** - * The domain name used to construct IRIs. + * Returns `true` if an IRI string looks like a Knora list IRI. + * + * @param iri the IRI to be checked. */ - val IriDomain: String = "rdfh.ch" + def isKnoraListIriStr(iri: IRI): Boolean = + Iri.isIri(iri) && iri.startsWith("http://rdfh.ch/lists/") /** - * Returns `true` if an IRI string looks like a Knora list IRI. + * Returns `true` if an IRI string looks like a Knora role IRI. * * @param iri the IRI to be checked. */ - def isKnoraListIriStr(iri: IRI): Boolean = - Iri.isIri(iri) && iri.startsWith("http://" + IriDomain + "/lists/") + def isKnoraRoleIriStr(iri: IRI): Boolean = + Iri.isIri(iri) && iri.startsWith("http://rdfh.ch/roles/") /** * Returns `true` if an IRI string looks like a Knora user IRI. @@ -103,7 +113,7 @@ object V2IriValidation { * @param iri the IRI to be checked. */ def isKnoraUserIriStr(iri: IRI): Boolean = - Iri.isIri(iri) && iri.startsWith("http://" + IriDomain + "/users/") + Iri.isIri(iri) && iri.startsWith("http://rdfh.ch/users/") /** * Returns `true` if an IRI string looks like a Knora group IRI. @@ -111,7 +121,7 @@ object V2IriValidation { * @param iri the IRI to be checked. */ def isKnoraGroupIriStr(iri: IRI): Boolean = - Iri.isIri(iri) && iri.startsWith("http://" + IriDomain + "/groups/") + Iri.isIri(iri) && iri.startsWith("http://rdfh.ch/groups/") /** * Returns `true` if an IRI string looks like a Knora project IRI @@ -119,7 +129,7 @@ object V2IriValidation { * @param iri the IRI to be checked. */ def isKnoraProjectIriStr(iri: IRI): Boolean = - Iri.isIri(iri) && (iri.startsWith("http://" + IriDomain + "/projects/") || isKnoraBuiltInProjectIriStr(iri)) + Iri.isIri(iri) && (iri.startsWith("http://rdfh.ch/projects/") || isKnoraBuiltInProjectIriStr(iri)) /** * Returns `true` if an IRI string looks like a Knora built-in IRI: diff --git a/dsp-shared/src/test/scala/dsp/valueobjects/IriSpec.scala b/dsp-shared/src/test/scala/dsp/valueobjects/IriSpec.scala index 530ec4c60c..71f37e2cee 100644 --- a/dsp-shared/src/test/scala/dsp/valueobjects/IriSpec.scala +++ b/dsp-shared/src/test/scala/dsp/valueobjects/IriSpec.scala @@ -21,6 +21,8 @@ object IriSpec extends ZIOSpecDefault { val listIriWithUUIDVersion3 = "http://rdfh.ch/lists/0803/6_xROK_UN1S2ZVNSzLlSXQ" val validProjectIri = "http://rdfh.ch/projects/0001" val projectIriWithUUIDVersion3 = "http://rdfh.ch/projects/tZjZhGSZMeCLA5VeUmwAmg" + val validRoleIri = "http://rdfh.ch/roles/ZPKPVh8yQs6F7Oyukb8WIQ" + val roleIriWithUUIDVersion3 = "http://rdfh.ch/roles/Ul3IYhDMOQ2fyoVY0ePz0w" val validUserIri = "http://rdfh.ch/users/jDEEitJESRi3pDaDjjQ1WQ" val userIriWithUUIDVersion3 = "http://rdfh.ch/users/cCmdcpn2MO211YYOplR1hQ" @@ -152,6 +154,29 @@ object IriSpec extends ZIOSpecDefault { } ) + private val RoleIriTest = suite("IriSpec - roleIri")( + test("pass an empty value and return an error") { + assertTrue(RoleIri.make("") == Validation.fail(BadRequestException(IriErrorMessages.RoleIriMissing))) + }, + test("pass an invalid value and return an error") { + assertTrue( + RoleIri.make(invalidIri) == Validation.fail( + BadRequestException(IriErrorMessages.RoleIriInvalid(invalidIri)) + ) + ) + }, + test("pass an invalid IRI containing unsupported UUID version and return an error") { + assertTrue( + RoleIri.make(roleIriWithUUIDVersion3) == Validation.fail( + BadRequestException(IriErrorMessages.UuidVersionInvalid) + ) + ) + }, + test("pass a valid value and successfully create value object") { + assertTrue(RoleIri.make(validRoleIri).toOption.get.value == validRoleIri) + } + ) + private val UserIriTest = suite("IriSpec - UserIri")( test("pass an empty value and return an error") { assertTrue(UserIri.make("") == Validation.fail(BadRequestException(IriErrorMessages.UserIriMissing))) @@ -159,7 +184,7 @@ object IriSpec extends ZIOSpecDefault { test("pass an invalid value and return an error") { assertTrue( UserIri.make(invalidIri) == Validation.fail( - BadRequestException(IriErrorMessages.UserIriInvalid) + BadRequestException(IriErrorMessages.UserIriInvalid(invalidIri)) ) ) }, diff --git a/dsp-shared/src/test/scala/dsp/valueobjects/RoleSpec.scala b/dsp-shared/src/test/scala/dsp/valueobjects/RoleSpec.scala new file mode 100644 index 0000000000..70f3f4ab7c --- /dev/null +++ b/dsp-shared/src/test/scala/dsp/valueobjects/RoleSpec.scala @@ -0,0 +1,78 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package dsp.valueobjects + +import dsp.errors.BadRequestException +import dsp.valueobjects.Role._ +import zio.prelude.Validation +import zio.test.ZIOSpecDefault +import zio.test._ + +/** + * This spec is used to test the [[Role]] value objects creation. + */ +object RoleSpec extends ZIOSpecDefault { + private val validLangStringValue = "I'm valid LangString value" + private val invalidLangStringValue = "Invalid \r" + private val validPermission = Permission.View + private val invalidPermission = "mod" + private val validLangStringIsoCode = V2.EN + private val invalidLangStringIsoCode = "iso" + + def spec = + (langStringTest + permissionTest) + + private val langStringTest = suite("LangString")( + test("pass an empty value and return an error") { + assertTrue( + LangString.make("", validLangStringIsoCode) == Validation.fail( + BadRequestException(RoleErrorMessages.LangStringValueMissing) + ) + ) + }, + test("pass an invalid value and return an error") { + assertTrue( + LangString.make(invalidLangStringValue, validLangStringIsoCode) == Validation.fail( + BadRequestException(RoleErrorMessages.LangStringValueInvalid(invalidLangStringValue)) + ) + ) + }, + test("pass an empty ISO code and return an error") { + assertTrue( + LangString.make(validLangStringValue, "") == Validation.fail( + BadRequestException(RoleErrorMessages.LangStringIsoCodeMissing) + ) + ) + }, + test("pass an invalid ISO code and return an error") { + assertTrue( + LangString.make(validLangStringValue, invalidLangStringIsoCode) == Validation.fail( + BadRequestException(RoleErrorMessages.LangStringIsoCodeInvalid(invalidLangStringIsoCode)) + ) + ) + } + ) + + private val permissionTest = suite("Permission")( + test("pass an empty value and return an error") { + assertTrue( + Permission.make("") == Validation.fail( + BadRequestException(RoleErrorMessages.PermissionMissing) + ) + ) + }, + test("pass an invalid value and return an error") { + assertTrue( + Permission.make(invalidPermission) == Validation.fail( + BadRequestException(RoleErrorMessages.PermissionInvalid(invalidPermission)) + ) + ) + }, + test("pass a valid value and successfully create value object") { + assertTrue(Permission.make(validPermission).toOption.get.value == validPermission) + } + ) +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 373552163a..8914b24c22 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -188,49 +188,85 @@ object Dependencies { val schemaRepoEventStoreServiceLibraryDependencies = Seq() val schemaRepoSearchServiceLibraryDependencies = Seq() - // user slice dependencies + // user projects dependencies val userInterfaceLibraryDependencies = Seq( + slf4j % Test, zio, zioMacros, zioTest % Test, - zioTestSbt % Test, - slf4j % Test + zioTestSbt % Test ) val userHandlerLibraryDependencies = Seq( - springSecurityCore, bouncyCastle, + slf4j % Test, + springSecurityCore, zio, zioMacros, zioTest % Test, - zioTestSbt % Test, - slf4j % Test + zioTestSbt % Test ) val userCoreLibraryDependencies = Seq( - springSecurityCore, bouncyCastle, + slf4j % Test, + springSecurityCore, zio, zioMacros, zioTest % Test, - zioTestSbt % Test, - slf4j % Test + zioTestSbt % Test ) val userRepoLibraryDependencies = Seq( + slf4j % Test, zio, zioMacros, zioTest % Test, - zioTestSbt % Test, - slf4j % Test + zioTestSbt % Test ) - val sharedLibraryDependencies = Seq( + + // role projects dependencies + val roleInterfaceLibraryDependencies = Seq( + slf4j % Test, + zio, + zioMacros, + zioTest % Test, + zioTestSbt % Test + ) + val roleHandlerLibraryDependencies = Seq( + bouncyCastle, + slf4j % Test, springSecurityCore, + zio, + zioMacros, + zioTest % Test, + zioTestSbt % Test + ) + val roleCoreLibraryDependencies = Seq( + bouncyCastle, + slf4j % Test, + springSecurityCore, + zio, + zioMacros, + zioTest % Test, + zioTestSbt % Test + ) + val roleRepoLibraryDependencies = Seq( + slf4j % Test, + zio, + zioMacros, + zioTest % Test, + zioTestSbt % Test + ) + + // shared project dependencies + val sharedLibraryDependencies = Seq( bouncyCastle, commonsLang3, commonsValidator, gwtServlet, - zioPrelude, scalaLogging, + slf4j % Test, + springSecurityCore, + zioPrelude, zioTest % Test, - zioTestSbt % Test, - slf4j % Test + zioTestSbt % Test ) }