This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-7222-58179a08a73203bae5c3cd6e615b592a235713ea in repository https://gitbox.apache.org/repos/asf/texera.git
commit e970abb8eed36c8dc3a6f002ccb846a61287b47e Author: Meng Wang <[email protected]> AuthorDate: Sat Aug 1 00:02:56 2026 -0700 test(amber): extend ProjectAccessResource coverage to read/query and grant/revoke (#7222) ### What changes were proposed in this PR? Extends the existing `MockTexeraDB`-backed `ProjectAccessResourceSpec` to cover the read/query and grant/revoke endpoints (`amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResource.scala`). The prior spec only exercised `getProjectAccessPrivilege` / `userHasWriteAccess` for the owner/READ/NONE cases. No production code was changed. A third seeded user (a WRITE grantee) is added; +8 tests cover: - `getProjectAccessPrivilege` / `userHasWriteAccess` — the WRITE-grantee case (a non-owner with WRITE), which the existing tests didn't reach. - `getOwner` — returns the owning user's email. - `getAccessList` — empty when only the owner has access (the owner is excluded), and lists every grantee with their email, name and privilege (READ and WRITE). - `grantAccess` — a writer grants READ to another user (access row created), and a user without write access is rejected with `ForbiddenException`. - `revokeAccess` — a grantee's access is removed, and a user without write access is rejected with `ForbiddenException`. ### Any related issues, documentation, discussions? Closes #7178 ### How was this PR tested? Extended unit tests, run locally against embedded Postgres (`MockTexeraDB`). All pass, and the failure path was verified by breaking an assertion to confirm the suite goes red: ``` sbt "WorkflowExecutionService/testOnly *ProjectAccessResourceSpec" # Tests: succeeded 11, failed 0 sbt "WorkflowExecutionService/Test/scalafmtCheck" # clean sbt "WorkflowExecutionService/Test/scalafix --check" # clean ``` ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8 [1M context]) --- .../user/project/ProjectAccessResourceSpec.scala | 115 ++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResourceSpec.scala index 2100be4b42..cf886e4a8e 100644 --- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResourceSpec.scala +++ b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResourceSpec.scala @@ -25,9 +25,13 @@ import org.apache.texera.dao.jooq.generated.Tables.{PROJECT, PROJECT_USER_ACCESS import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, UserRoleEnum} import org.apache.texera.dao.jooq.generated.tables.daos.{ProjectUserAccessDao, UserDao} import org.apache.texera.dao.jooq.generated.tables.pojos.{ProjectUserAccess, User} +import org.apache.texera.web.model.common.AccessEntry import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} +import javax.ws.rs.ForbiddenException +import scala.jdk.CollectionConverters._ + class ProjectAccessResourceSpec extends AnyFlatSpec with BeforeAndAfterAll @@ -36,12 +40,15 @@ class ProjectAccessResourceSpec private val ownerUid = 7101 private val readerUid = 7102 + private val writerUid = 7103 private var owner: User = _ private var reader: User = _ + private var writer: User = _ private var userDao: UserDao = _ private var projectUserAccessDao: ProjectUserAccessDao = _ private var projectResource: ProjectResource = _ + private var projectAccessResource: ProjectAccessResource = _ override protected def beforeAll(): Unit = { initializeDBAndReplaceDSLContext() @@ -51,14 +58,17 @@ class ProjectAccessResourceSpec userDao = new UserDao(getDSLContext.configuration()) projectUserAccessDao = new ProjectUserAccessDao(getDSLContext.configuration()) projectResource = new ProjectResource() + projectAccessResource = new ProjectAccessResource() owner = createUser(ownerUid, "project_owner", "[email protected]") reader = createUser(readerUid, "project_reader", "[email protected]") + writer = createUser(writerUid, "project_writer", "[email protected]") cleanupTestData() userDao.insert(owner) userDao.insert(reader) + userDao.insert(writer) } override protected def afterEach(): Unit = { @@ -82,7 +92,7 @@ class ProjectAccessResourceSpec private def cleanupTestData(): Unit = { getDSLContext .deleteFrom(PROJECT_USER_ACCESS) - .where(PROJECT_USER_ACCESS.UID.in(ownerUid, readerUid)) + .where(PROJECT_USER_ACCESS.UID.in(ownerUid, readerUid, writerUid)) .execute() getDSLContext @@ -92,7 +102,7 @@ class ProjectAccessResourceSpec getDSLContext .deleteFrom(USER) - .where(USER.UID.in(ownerUid, readerUid)) + .where(USER.UID.in(ownerUid, readerUid, writerUid)) .execute() } @@ -129,4 +139,105 @@ class ProjectAccessResourceSpec assert(privilege == PrivilegeEnum.NONE) assert(!ProjectAccessResource.userHasWriteAccess(privateProject.getPid, readerUid)) } + + it should "return WRITE and grant write access for a WRITE grantee" in { + val project = projectResource.createProject(new SessionUser(owner), "writer-project") + projectUserAccessDao.merge( + new ProjectUserAccess(writerUid, project.getPid, PrivilegeEnum.WRITE) + ) + + assert( + ProjectAccessResource.getProjectAccessPrivilege( + project.getPid, + writerUid + ) == PrivilegeEnum.WRITE + ) + assert(ProjectAccessResource.userHasWriteAccess(project.getPid, writerUid)) + } + + "ProjectAccessResource.getOwner" should "return the owning user's email" in { + val project = projectResource.createProject(new SessionUser(owner), "owned-project") + assert(projectAccessResource.getOwner(project.getPid) == owner.getEmail) + } + + "ProjectAccessResource.getAccessList" should "be empty when only the owner has access" in { + val project = projectResource.createProject(new SessionUser(owner), "solo-project") + // createProject grants the owner WRITE, but getAccessList excludes the owner. + assert(projectAccessResource.getAccessList(project.getPid).asScala.isEmpty) + } + + it should "list every grantee (excluding the owner) with their email, name and privilege" in { + val project = projectResource.createProject(new SessionUser(owner), "shared-list-project") + projectUserAccessDao.merge(new ProjectUserAccess(readerUid, project.getPid, PrivilegeEnum.READ)) + projectUserAccessDao.merge( + new ProjectUserAccess(writerUid, project.getPid, PrivilegeEnum.WRITE) + ) + + val entries = projectAccessResource.getAccessList(project.getPid).asScala.toList + assert(entries.size == 2) + assert(!entries.map(_.email).contains(owner.getEmail)) // owner is excluded + assert(entries.contains(AccessEntry(reader.getEmail, reader.getName, PrivilegeEnum.READ))) + assert(entries.contains(AccessEntry(writer.getEmail, writer.getName, PrivilegeEnum.WRITE))) + } + + "ProjectAccessResource.grantAccess" should "let a WRITE grantee grant READ access to another user" in { + val project = projectResource.createProject(new SessionUser(owner), "grant-project") + // writer is a non-owner WRITE grantee, so it is allowed to grant access. + projectUserAccessDao.merge( + new ProjectUserAccess(writerUid, project.getPid, PrivilegeEnum.WRITE) + ) + + projectAccessResource.grantAccess( + project.getPid, + reader.getEmail, + "READ", + new SessionUser(writer) + ) + + assert( + ProjectAccessResource.getProjectAccessPrivilege( + project.getPid, + readerUid + ) == PrivilegeEnum.READ + ) + } + + it should "reject a user without write access with ForbiddenException" in { + val project = projectResource.createProject(new SessionUser(owner), "grant-forbidden-project") + // reader has no access to the project, so cannot grant. + assertThrows[ForbiddenException]( + projectAccessResource.grantAccess( + project.getPid, + writer.getEmail, + "READ", + new SessionUser(reader) + ) + ) + } + + "ProjectAccessResource.revokeAccess" should "remove a grantee's access" in { + val project = projectResource.createProject(new SessionUser(owner), "revoke-project") + projectUserAccessDao.merge(new ProjectUserAccess(readerUid, project.getPid, PrivilegeEnum.READ)) + + projectAccessResource.revokeAccess(project.getPid, reader.getEmail, new SessionUser(owner)) + + assert( + ProjectAccessResource.getProjectAccessPrivilege( + project.getPid, + readerUid + ) == PrivilegeEnum.NONE + ) + } + + it should "reject a user without write access with ForbiddenException" in { + val project = projectResource.createProject(new SessionUser(owner), "revoke-forbidden-project") + projectUserAccessDao.merge( + new ProjectUserAccess(writerUid, project.getPid, PrivilegeEnum.WRITE) + ) + + // reader has no write access, so cannot revoke the writer's access. + assertThrows[ForbiddenException]( + projectAccessResource.revokeAccess(project.getPid, writer.getEmail, new SessionUser(reader)) + ) + } }
