You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/07/31 19:16:07 UTC
(texera) branch gh-readonly-queue/main/pr-7167-d9c443f38e2f4ac730c840a9fbb0ed8ed19cde63 created (now d31406eaed)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-7167-d9c443f38e2f4ac730c840a9fbb0ed8ed19cde63
in repository https://gitbox.apache.org/repos/asf/texera.git
at d31406eaed test(amber): add unit test coverage for AdminUserResource (#7167)
This branch includes the following new commits:
new d31406eaed test(amber): add unit test coverage for AdminUserResource (#7167)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(texera) 01/01: test(amber): add unit test coverage for AdminUserResource (#7167)
Posted by gi...@apache.org.
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 <me...@uci.edu>
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
+ }
+}