This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/texera.git
commit a5cdabbf4eacb4599e116a9806ff70e8a604a0c8 Author: Meng Wang <[email protected]> AuthorDate: Sat Aug 1 01:23:44 2026 -0700 test(amber): extend WorkflowExecutionsResource coverage for access-controlled metadata endpoints (#7231) ### What changes were proposed in this PR? Extends the existing `MockTexeraDB`-backed `WorkflowExecutionsResourceSpec` to cover the previously-untested access-controlled execution-metadata endpoints. The result/log-URI and replay paths (`DocumentFactory` / `ReplayLogRecord`) are out of scope per the issue — these tests cover the jOOQ-metadata portion of each endpoint only. 12 added tests (each seeds only what it needs, so they stay order-independent): - `retrieveExecutionsOfWorkflow` — empty list when the user lacks read access; the workflow's executions for an authorized user; a `BadRequestException` for an invalid `status` filter. - `retrieveLatestExecutionEntry` — `ForbiddenException` when the workflow has no executions; the most-recent entry otherwise. - `retrieveInteractionHistory` — empty list when the user lacks read access (the replay branch is out of scope). - `setExecutionAreBookmarked` — `WebApplicationException` for a user without access; the bookmark flag toggles both directions. - `updateWorkflowExecutionsName` — renames the execution. - `groupDeleteExecutionsOfWorkflow` — removes the execution rows (seeded with no URIs so `removeAllExecutionFiles` touches no storage). - `retrieveWorkflowRuntimeStatistics` — `NoSuchElementException` when the execution has no runtime-stats URI (the `DocumentFactory` read is out of scope). Read access is granted by seeding a `WORKFLOW_USER_ACCESS(READ)` row; that row is cleaned up in `cleanupTestData` before the `WORKFLOW` row (FK / leak safety). No production code was changed. ### Any related issues, documentation, discussions? Closes #7226 ### How was this PR tested? Extended unit tests, run locally against embedded Postgres: ``` sbt "WorkflowExecutionService/testOnly org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResourceSpec" # Tests: succeeded 42, failed 0 (30 existing + 12 new; re-ran for stability) sbt "WorkflowExecutionService/Test/scalafmtCheck" "WorkflowExecutionService/Test/scalafix --check" # clean ``` The failure path was verified by deliberately breaking a new assertion and confirming the suite goes red (41 succeeded, 1 failed), then restored. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8 [1M context]) --- .../workflow/WorkflowExecutionsResourceSpec.scala | 147 ++++++++++++++++++++- 1 file changed, 146 insertions(+), 1 deletion(-) diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowExecutionsResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowExecutionsResourceSpec.scala index 97bbe34abc..7a3afc03f3 100644 --- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowExecutionsResourceSpec.scala +++ b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowExecutionsResourceSpec.scala @@ -28,9 +28,10 @@ import org.apache.texera.amber.core.virtualidentity.{ } import org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PortIdentity} import org.apache.texera.amber.util.serde.GlobalPortIdentitySerde.SerdeOps +import org.apache.texera.auth.SessionUser import org.apache.texera.dao.MockTexeraDB import org.apache.texera.dao.jooq.generated.Tables._ -import org.apache.texera.dao.jooq.generated.enums.WorkflowComputingUnitTypeEnum +import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, WorkflowComputingUnitTypeEnum} import org.apache.texera.dao.jooq.generated.tables.daos.{ DatasetDao, UserDao, @@ -52,6 +53,7 @@ import org.apache.texera.web.service.ExecutionResultService import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, PrivateMethodTester} +import javax.ws.rs.{BadRequestException, ForbiddenException, WebApplicationException} import java.net.URI import java.sql.Timestamp import java.util.UUID @@ -162,6 +164,12 @@ class WorkflowExecutionsResourceSpec .where(WORKFLOW_VERSION.WID.eq(testWorkflowWid)) .execute() + // Access grants seeded by the endpoint tests must go before the workflow row. + getDSLContext + .deleteFrom(WORKFLOW_USER_ACCESS) + .where(WORKFLOW_USER_ACCESS.WID.eq(testWorkflowWid)) + .execute() + getDSLContext .deleteFrom(WORKFLOW) .where(WORKFLOW.WID.eq(testWorkflowWid)) @@ -900,4 +908,141 @@ class WorkflowExecutionsResourceSpec ) } + // ─── access-controlled instance endpoints (jOOQ metadata only) ───────────── + // The result/log-URI and replay paths (DocumentFactory / ReplayLogRecord) are + // out of scope; these cover the DB-metadata portion of each endpoint. + + private val resource = new WorkflowExecutionsResource + + private def session(user: User): SessionUser = new SessionUser(user) + + private def grantReadAccess(uid: Integer = testUserId): Unit = + getDSLContext + .insertInto(WORKFLOW_USER_ACCESS) + .set(WORKFLOW_USER_ACCESS.WID, Integer.valueOf(testWorkflowWid)) + .set(WORKFLOW_USER_ACCESS.UID, uid) + .set(WORKFLOW_USER_ACCESS.PRIVILEGE, PrivilegeEnum.READ) + .execute() + + private def userWithoutAccess(): User = { + val u = new User + u.setUid(testUserId + 5000) + u.setName("no_access_user") + u.setEmail("[email protected]") + u.setPassword("password") + u + } + + "retrieveExecutionsOfWorkflow" should "return an empty list when the user lacks read access" in { + val result = + resource.retrieveExecutionsOfWorkflow(testWorkflowWid, session(userWithoutAccess()), null) + assert(result.isEmpty) + } + + it should "return the workflow's executions for an authorized user" in { + grantReadAccess() + insertExecution() + insertExecution() + val result = resource.retrieveExecutionsOfWorkflow(testWorkflowWid, session(testUser), null) + assert(result.size == 2) + } + + it should "reject an invalid status filter with a BadRequestException" in { + grantReadAccess() + assertThrows[BadRequestException]( + resource.retrieveExecutionsOfWorkflow( + testWorkflowWid, + session(testUser), + "definitely-not-a-status" + ) + ) + } + + "retrieveLatestExecutionEntry" should "throw ForbiddenException when the workflow has no executions" in { + grantReadAccess() + assertThrows[ForbiddenException]( + resource.retrieveLatestExecutionEntry(testWorkflowWid, session(testUser)) + ) + } + + it should "return the most recently created execution entry" in { + grantReadAccess() + insertExecution(name = "first") + val latest = insertExecution(name = "second") + val entry = resource.retrieveLatestExecutionEntry(testWorkflowWid, session(testUser)) + // same VID, so the highest EID is the latest + assert(entry.eId == latest.getEid) + assert(entry.name == "second") + } + + "retrieveInteractionHistory" should "return an empty list when the user lacks read access" in { + val result = + resource.retrieveInteractionHistory( + testWorkflowWid, + Integer.valueOf(1), + session(userWithoutAccess()) + ) + assert(result.isEmpty) + } + + "setExecutionAreBookmarked" should "reject a user without access" in { + val exec = insertExecution() + assertThrows[WebApplicationException]( + resource.setExecutionAreBookmarked( + ExecutionGroupBookmarkRequest(testWorkflowWid, Array(exec.getEid), isBookmarked = false), + session(userWithoutAccess()) + ) + ) + } + + it should "bookmark executions that are currently un-bookmarked" in { + grantReadAccess() + val exec = insertExecution() // bookmarked = false + resource.setExecutionAreBookmarked( + ExecutionGroupBookmarkRequest(testWorkflowWid, Array(exec.getEid), isBookmarked = false), + session(testUser) + ) + assert(workflowExecutionsDao.fetchOneByEid(exec.getEid).getBookmarked == true) + } + + it should "un-bookmark executions that are currently bookmarked" in { + grantReadAccess() + val exec = insertExecution() + resource.setExecutionAreBookmarked( + ExecutionGroupBookmarkRequest(testWorkflowWid, Array(exec.getEid), isBookmarked = true), + session(testUser) + ) + assert(workflowExecutionsDao.fetchOneByEid(exec.getEid).getBookmarked == false) + } + + "updateWorkflowExecutionsName" should "rename the execution" in { + grantReadAccess() + val exec = insertExecution(name = "old-name") + resource.updateWorkflowExecutionsName( + ExecutionRenameRequest(testWorkflowWid, exec.getEid, "new-name"), + session(testUser) + ) + assert(workflowExecutionsDao.fetchOneByEid(exec.getEid).getName == "new-name") + } + + "groupDeleteExecutionsOfWorkflow" should "delete the execution rows" in { + grantReadAccess() + val e1 = insertExecution() + val e2 = insertExecution() + resource.groupDeleteExecutionsOfWorkflow( + ExecutionGroupDeleteRequest(testWorkflowWid, Array(e1.getEid, e2.getEid)), + session(testUser) + ) + assert(workflowExecutionsDao.fetchOneByEid(e1.getEid) == null) + assert(workflowExecutionsDao.fetchOneByEid(e2.getEid) == null) + } + + "retrieveWorkflowRuntimeStatistics" should "throw when the execution has no runtime-stats URI" in { + grantReadAccess() + val exec = insertExecution() // runtimeStatsUri = null + assertThrows[java.util.NoSuchElementException]( + resource.retrieveWorkflowRuntimeStatistics(testWorkflowWid, exec.getEid, session(testUser)) + ) + } + }
