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))
+    )
+  }
 }

Reply via email to