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-7167-d9c443f38e2f4ac730c840a9fbb0ed8ed19cde63
in repository https://gitbox.apache.org/repos/asf/texera.git

commit d31406eaed568cdd4be5274f73de7683bbfdd586
Author: Meng Wang <[email protected]>
AuthorDate: Fri Jul 31 11:48:54 2026 -0700

    test(amber): add unit test coverage for AdminUserResource (#7167)
    
    ### What changes were proposed in this PR?
    
    Adds `AdminUserResourceSpec`, the first spec for `AdminUserResource`
    (previously
    ~0% coverage). It runs against embedded Postgres via `MockTexeraDB` and
    seeds
    rows through the jOOQ DAOs, with 12 tests covering the admin
    user-management
    methods, empty and populated:
    
    - `list` — returns the seeded users with name / email / role.
    - `addUser` — persists a new `INACTIVE` user.
    - `updateUser` — round-trips an editable-field change; rejects an email
    that
      already belongs to another user (`WebApplicationException`).
    - `getCreatedWorkflow` / `getAccessedWorkflow` — the per-user created /
    accessible
      workflow lists.
    - `getUserQuota` — the per-user quota view.
    - `deleteCollection` — removes the target execution row.
    
    Two side-effecting branches are intentionally left to their owning code
    paths so
    this stays a hermetic unit test: `updateUser`'s role-change branch
    (sends an
    e-mail via `GmailResource`) is not exercised — only the no-role-change
    and
    conflict paths are; and `deleteCollection` is asserted on its DB delete
    (the
    Iceberg document cleanup is a best-effort, exception-swallowed step).
    
    ### Any related issues, documentation, discussions?
    
    Closes #7163.
    
    ### How was this PR tested?
    
    `sbt "WorkflowExecutionService/testOnly *AdminUserResourceSpec"` — 12
    succeeded, 0 failed. `Test/scalafmtCheck` and `Test/scalafix --check`
    clean. Failure path verified by breaking an assertion (→ 1 failed,
    non-zero exit) and restoring.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../admin/user/AdminUserResourceSpec.scala         | 291 +++++++++++++++++++++
 1 file changed, 291 insertions(+)

diff --git 
a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/admin/user/AdminUserResourceSpec.scala
 
b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/admin/user/AdminUserResourceSpec.scala
new file mode 100644
index 0000000000..741dbba464
--- /dev/null
+++ 
b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/admin/user/AdminUserResourceSpec.scala
@@ -0,0 +1,291 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.web.resource.dashboard.admin.user
+
+import org.apache.texera.dao.MockTexeraDB
+import org.apache.texera.dao.jooq.generated.Tables._
+import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, UserRoleEnum}
+import org.apache.texera.dao.jooq.generated.tables.daos.{
+  UserDao,
+  WorkflowDao,
+  WorkflowExecutionsDao,
+  WorkflowOfUserDao,
+  WorkflowUserAccessDao,
+  WorkflowVersionDao
+}
+import org.apache.texera.dao.jooq.generated.tables.pojos.{
+  User,
+  Workflow,
+  WorkflowExecutions,
+  WorkflowOfUser,
+  WorkflowUserAccess,
+  WorkflowVersion
+}
+import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+import java.sql.Timestamp
+import java.util.UUID
+import javax.ws.rs.WebApplicationException
+import scala.jdk.CollectionConverters._
+
+class AdminUserResourceSpec
+    extends AnyFlatSpec
+    with Matchers
+    with BeforeAndAfterAll
+    with BeforeAndAfterEach
+    with MockTexeraDB {
+
+  private val primaryUid = 90000 + scala.util.Random.nextInt(5000)
+  private val secondaryUid = primaryUid + 1
+  private val testWid = 90000 + scala.util.Random.nextInt(5000)
+
+  private var userDao: UserDao = _
+  private var workflowDao: WorkflowDao = _
+  private var workflowVersionDao: WorkflowVersionDao = _
+  private var workflowExecutionsDao: WorkflowExecutionsDao = _
+  private var workflowOfUserDao: WorkflowOfUserDao = _
+  private var workflowUserAccessDao: WorkflowUserAccessDao = _
+
+  private val resource = new AdminUserResource
+
+  override protected def beforeAll(): Unit = {
+    initializeDBAndReplaceDSLContext()
+    userDao = new UserDao(getDSLContext.configuration())
+    workflowDao = new WorkflowDao(getDSLContext.configuration())
+    workflowVersionDao = new WorkflowVersionDao(getDSLContext.configuration())
+    workflowExecutionsDao = new 
WorkflowExecutionsDao(getDSLContext.configuration())
+    workflowOfUserDao = new WorkflowOfUserDao(getDSLContext.configuration())
+    workflowUserAccessDao = new 
WorkflowUserAccessDao(getDSLContext.configuration())
+  }
+
+  override protected def afterAll(): Unit = closeConnectionPool()
+
+  // Wipe everything this spec seeds, children before parents, after each test.
+  private def cleanup(): Unit = {
+    getDSLContext
+      .deleteFrom(WORKFLOW_EXECUTIONS)
+      .where(WORKFLOW_EXECUTIONS.UID.in(primaryUid, secondaryUid))
+      .execute()
+    
getDSLContext.deleteFrom(WORKFLOW_VERSION).where(WORKFLOW_VERSION.WID.eq(testWid)).execute()
+    getDSLContext
+      .deleteFrom(WORKFLOW_USER_ACCESS)
+      .where(WORKFLOW_USER_ACCESS.WID.eq(testWid))
+      .execute()
+    
getDSLContext.deleteFrom(WORKFLOW_OF_USER).where(WORKFLOW_OF_USER.WID.eq(testWid)).execute()
+    
getDSLContext.deleteFrom(WORKFLOW).where(WORKFLOW.WID.eq(testWid)).execute()
+    getDSLContext.deleteFrom(USER).where(USER.UID.in(primaryUid, 
secondaryUid)).execute()
+    // addUser() inserts an INACTIVE user with an auto-generated uid and a 
"User<millis>" name.
+    getDSLContext
+      .deleteFrom(USER)
+      .where(USER.ROLE.eq(UserRoleEnum.INACTIVE).and(USER.NAME.like("User%")))
+      .execute()
+  }
+
+  private def makeUser(uid: Int, name: String, role: UserRoleEnum = 
UserRoleEnum.REGULAR): User = {
+    val user = new User
+    user.setUid(uid)
+    user.setName(name)
+    user.setEmail(
+      s"admin_user_spec_${uid}_${UUID.randomUUID().toString.substring(0, 
8)}@example.com"
+    )
+    user.setPassword("password")
+    user.setRole(role)
+    user
+  }
+
+  private def seedWorkflow(): Workflow = {
+    val workflow = new Workflow
+    workflow.setWid(testWid)
+    workflow.setName("admin_user_spec_wf_" + 
UUID.randomUUID().toString.substring(0, 8))
+    workflow.setContent("{}")
+    workflow.setDescription("desc")
+    workflow.setCreationTime(new Timestamp(System.currentTimeMillis()))
+    workflow.setLastModifiedTime(new Timestamp(System.currentTimeMillis()))
+    workflowDao.insert(workflow)
+    workflow
+  }
+
+  private def seedExecution(uid: Int): WorkflowExecutions = {
+    seedWorkflow()
+    val version = new WorkflowVersion
+    version.setWid(testWid)
+    version.setContent("{}")
+    version.setCreationTime(new Timestamp(System.currentTimeMillis()))
+    workflowVersionDao.insert(version)
+
+    val execution = new WorkflowExecutions
+    execution.setVid(version.getVid)
+    execution.setUid(uid)
+    execution.setStatus(0.toByte)
+    execution.setResult("")
+    execution.setLogLocation("")
+    execution.setStartingTime(new Timestamp(System.currentTimeMillis()))
+    execution.setBookmarked(false)
+    execution.setName("admin_user_spec_exec")
+    execution.setEnvironmentVersion("test-env-1.0")
+    workflowExecutionsDao.insert(execution)
+    execution
+  }
+
+  override protected def afterEach(): Unit = cleanup()
+
+  // ─── list ───────────────────────────────────────────────────────────────
+
+  "list" should "return the seeded users with their name, email, and role" in {
+    val a = makeUser(primaryUid, "alice", UserRoleEnum.ADMIN)
+    val b = makeUser(secondaryUid, "bob", UserRoleEnum.REGULAR)
+    userDao.insert(a)
+    userDao.insert(b)
+
+    val listed = resource.list().asScala
+    val alice = listed.find(_.uid == primaryUid)
+    val bob = listed.find(_.uid == secondaryUid)
+
+    alice.map(u => (u.name, u.email, u.role)) shouldBe Some(
+      ("alice", a.getEmail, UserRoleEnum.ADMIN)
+    )
+    bob.map(u => (u.name, u.email, u.role)) shouldBe Some(("bob", b.getEmail, 
UserRoleEnum.REGULAR))
+  }
+
+  it should "not return a user that has not been seeded" in {
+    resource.list().asScala.exists(_.uid == primaryUid) shouldBe false
+  }
+
+  // ─── addUser ────────────────────────────────────────────────────────────
+
+  "addUser" should "persist a new INACTIVE user" in {
+    val before = userDao.fetchByRole(UserRoleEnum.INACTIVE).size()
+
+    resource.addUser()
+
+    val after = userDao.fetchByRole(UserRoleEnum.INACTIVE)
+    after.size() shouldBe before + 1
+    // The newly added user has a generated non-empty name and no password 
left blank.
+    after.asScala.exists(u => u.getName.startsWith("User") && u.getPassword != 
null) shouldBe true
+  }
+
+  // ─── updateUser ─────────────────────────────────────────────────────────
+
+  "updateUser" should "update a user's editable fields and round-trip via a 
re-read" in {
+    val user = makeUser(primaryUid, "original", UserRoleEnum.REGULAR)
+    userDao.insert(user)
+
+    val edit = new User
+    edit.setUid(primaryUid)
+    edit.setName("renamed")
+    edit.setEmail(user.getEmail) // unchanged email → no conflict
+    edit.setRole(UserRoleEnum.REGULAR) // unchanged role → no e-mail side 
effect
+    edit.setComment("a new comment")
+    resource.updateUser(edit)
+
+    val reread = userDao.fetchOneByUid(primaryUid)
+    reread.getName shouldBe "renamed"
+    reread.getComment shouldBe "a new comment"
+    reread.getRole shouldBe UserRoleEnum.REGULAR
+  }
+
+  it should "reject an update whose email already belongs to another user" in {
+    val alice = makeUser(primaryUid, "alice", UserRoleEnum.REGULAR)
+    val bob = makeUser(secondaryUid, "bob", UserRoleEnum.REGULAR)
+    userDao.insert(alice)
+    userDao.insert(bob)
+
+    val edit = new User
+    edit.setUid(secondaryUid) // updating bob…
+    edit.setName("bob")
+    edit.setEmail(alice.getEmail) // …to alice's email → conflict
+    edit.setRole(UserRoleEnum.REGULAR)
+
+    a[WebApplicationException] should be thrownBy resource.updateUser(edit)
+  }
+
+  // ─── getCreatedWorkflow ───────────────────────────────────────────────────
+
+  "getCreatedWorkflow" should "return an empty list for a user with no created 
workflows" in {
+    userDao.insert(makeUser(primaryUid, "creator"))
+    resource.getCreatedWorkflow(primaryUid) shouldBe empty
+  }
+
+  it should "return the workflows the user created" in {
+    userDao.insert(makeUser(primaryUid, "creator"))
+    val workflow = seedWorkflow()
+    val ownership = new WorkflowOfUser
+    ownership.setUid(primaryUid)
+    ownership.setWid(testWid)
+    workflowOfUserDao.insert(ownership)
+
+    val created = resource.getCreatedWorkflow(primaryUid)
+    created.map(_.workflowId) shouldBe List(Integer.valueOf(testWid))
+    created.head.workflowName shouldBe workflow.getName
+  }
+
+  // ─── getAccessedWorkflow ──────────────────────────────────────────────────
+
+  "getAccessedWorkflow" should "return an empty list for a user with no 
workflow access" in {
+    userDao.insert(makeUser(primaryUid, "viewer"))
+    resource.getAccessedWorkflow(primaryUid).asScala shouldBe empty
+  }
+
+  it should "return the workflow ids the user can access" in {
+    userDao.insert(makeUser(primaryUid, "viewer"))
+    seedWorkflow()
+    val access = new WorkflowUserAccess
+    access.setUid(primaryUid)
+    access.setWid(testWid)
+    access.setPrivilege(PrivilegeEnum.READ)
+    workflowUserAccessDao.insert(access)
+
+    resource.getAccessedWorkflow(primaryUid).asScala should 
contain(Integer.valueOf(testWid))
+  }
+
+  // ─── getUserQuota ─────────────────────────────────────────────────────────
+
+  "getUserQuota" should "return an empty array for a user with no executions" 
in {
+    userDao.insert(makeUser(primaryUid, "quota_user"))
+    resource.getUserQuota(primaryUid) shouldBe empty
+  }
+
+  it should "return one quota entry per execution the user owns" in {
+    userDao.insert(makeUser(primaryUid, "quota_user"))
+    val execution = seedExecution(primaryUid)
+
+    val quota = resource.getUserQuota(primaryUid)
+    quota should have length 1
+    quota.head.eid shouldBe execution.getEid
+    quota.head.workflowId shouldBe Integer.valueOf(testWid)
+    // No operator_port/operator rows seeded → result and log sizes are zero.
+    quota.head.resultBytes shouldBe 0L
+    quota.head.logBytes shouldBe 0L
+  }
+
+  // ─── deleteCollection ─────────────────────────────────────────────────────
+
+  "deleteCollection" should "delete the target execution row" in {
+    userDao.insert(makeUser(primaryUid, "collection_user"))
+    val execution = seedExecution(primaryUid)
+    workflowExecutionsDao.fetchOneByEid(execution.getEid) should not be null
+
+    resource.deleteCollection(execution.getEid)
+
+    workflowExecutionsDao.fetchOneByEid(execution.getEid) shouldBe null
+  }
+}

Reply via email to