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-7168-d31406eaed568cdd4be5274f73de7683bbfdd586 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 9fa612401790f866d1e58a94f70744c996a2f696 Author: Meng Wang <[email protected]> AuthorDate: Fri Jul 31 11:48:58 2026 -0700 test(amber): add unit test coverage for UserQuotaResource (#7168) ### What changes were proposed in this PR? Adds `UserQuotaResourceSpec` for `UserQuotaResource` (previously ~0% coverage, no spec). The suite runs against embedded Postgres via `MockTexeraDB` and seeds users / workflows / versions / executions / datasets through the generated DAOs. 13 tests, each starting from a clean slate (so they are order-independent): - `getUserCreatedWorkflow` — empty list when the user owns no workflows; the owned workflows with their id / name / creation time when `WORKFLOW_OF_USER` is seeded. - `getUserAccessedWorkflow` — empty list with no grants; the accessible workflow ids from `WORKFLOW_USER_ACCESS`. - `getUserQuotaSize` — empty array with no executions; the assembled per-execution `resultBytes` / `runTimeStatsBytes` / `logBytes` (seeded across `WORKFLOW_EXECUTIONS.RUNTIME_STATS_SIZE`, `OPERATOR_PORT_EXECUTIONS.RESULT_SIZE`, and `OPERATOR_EXECUTIONS.CONSOLE_MESSAGES_SIZE`). - The JAX-RS endpoints (`getCreatedWorkflow`, `getAccessedWorkflow`, `getUserQuota`, `getCreatedDatasets`) via a `new SessionUser`, covering the thin delegation wrappers; `getCreatedDatasets` covers the empty case and the created-datasets case (size is deterministically 0 — the size calculation is disabled in the current dataset implementation). - `deleteExecutionCollection` and the `deleteCollection` endpoint — assert the execution row is removed from `WORKFLOW_EXECUTIONS`. No production code was changed. ### Any related issues, documentation, discussions? Closes #7162 ### How was this PR tested? New unit tests, run locally against embedded Postgres: ``` sbt "WorkflowExecutionService/testOnly org.apache.texera.web.resource.dashboard.user.quota.UserQuotaResourceSpec" # Tests: succeeded 13, failed 0 sbt "WorkflowExecutionService/Test/scalafmtCheck" "WorkflowExecutionService/Test/scalafix --check" # clean ``` The failure path was verified by deliberately breaking an assertion and confirming the suite goes red. To keep the suite deterministic, the `creationTime` assertion compares against the DB round-tripped value rather than the in-memory timestamp (Postgres rounds the stored value, so an exact in-memory comparison would be flaky). ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8 [1M context]) --- .../user/quota/UserQuotaResourceSpec.scala | 353 +++++++++++++++++++++ 1 file changed, 353 insertions(+) diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/quota/UserQuotaResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/quota/UserQuotaResourceSpec.scala new file mode 100644 index 0000000000..0a0212c2b2 --- /dev/null +++ b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/quota/UserQuotaResourceSpec.scala @@ -0,0 +1,353 @@ +/* + * 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.user.quota + +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.PrivilegeEnum +import org.apache.texera.dao.jooq.generated.tables.daos.{ + DatasetDao, + UserDao, + WorkflowDao, + WorkflowExecutionsDao, + WorkflowOfUserDao, + WorkflowUserAccessDao, + WorkflowVersionDao +} +import org.apache.texera.dao.jooq.generated.tables.pojos.{ + Dataset, + User, + Workflow, + WorkflowExecutions, + WorkflowOfUser, + WorkflowUserAccess, + WorkflowVersion +} +import org.scalatest.BeforeAndAfterAll +import org.scalatest.flatspec.AnyFlatSpec + +import java.sql.Timestamp +import java.util.UUID + +class UserQuotaResourceSpec extends AnyFlatSpec with BeforeAndAfterAll with MockTexeraDB { + + private val testUid = 4000 + scala.util.Random.nextInt(1000) + private val testWid = 5000 + scala.util.Random.nextInt(1000) + + private var userDao: UserDao = _ + private var workflowDao: WorkflowDao = _ + private var workflowVersionDao: WorkflowVersionDao = _ + private var workflowOfUserDao: WorkflowOfUserDao = _ + private var workflowUserAccessDao: WorkflowUserAccessDao = _ + private var workflowExecutionsDao: WorkflowExecutionsDao = _ + private var datasetDao: DatasetDao = _ + + private var testUser: User = _ + private var testWorkflow: Workflow = _ + private var testVersion: WorkflowVersion = _ + private val resource = new UserQuotaResource + + override protected def beforeAll(): Unit = { + initializeDBAndReplaceDSLContext() + + userDao = new UserDao(getDSLContext.configuration()) + workflowDao = new WorkflowDao(getDSLContext.configuration()) + workflowVersionDao = new WorkflowVersionDao(getDSLContext.configuration()) + workflowOfUserDao = new WorkflowOfUserDao(getDSLContext.configuration()) + workflowUserAccessDao = new WorkflowUserAccessDao(getDSLContext.configuration()) + workflowExecutionsDao = new WorkflowExecutionsDao(getDSLContext.configuration()) + datasetDao = new DatasetDao(getDSLContext.configuration()) + } + + override protected def afterAll(): Unit = shutdownDB() + + // Each test starts from a clean slate for the ids under test, then seeds + // only what it needs, so tests are order-independent. + private def resetFixtures(): Unit = { + cleanupTestData() + + testUser = new User + testUser.setUid(testUid) + testUser.setName("quota_user") + testUser.setEmail("[email protected]") + testUser.setPassword("password") + userDao.insert(testUser) + + testWorkflow = new Workflow + testWorkflow.setWid(testWid) + testWorkflow.setName("quota_workflow_" + UUID.randomUUID().toString.substring(0, 8)) + testWorkflow.setContent("{}") + testWorkflow.setDescription("desc") + testWorkflow.setCreationTime(new Timestamp(System.currentTimeMillis())) + testWorkflow.setLastModifiedTime(new Timestamp(System.currentTimeMillis())) + workflowDao.insert(testWorkflow) + + testVersion = new WorkflowVersion + testVersion.setWid(testWid) + testVersion.setContent("{}") + testVersion.setCreationTime(new Timestamp(System.currentTimeMillis())) + workflowVersionDao.insert(testVersion) + } + + private def cleanupTestData(): Unit = { + val vidSubquery = getDSLContext + .select(WORKFLOW_VERSION.VID) + .from(WORKFLOW_VERSION) + .where(WORKFLOW_VERSION.WID.eq(testWid)) + + getDSLContext + .deleteFrom(OPERATOR_PORT_EXECUTIONS) + .where( + OPERATOR_PORT_EXECUTIONS.WORKFLOW_EXECUTION_ID.in( + getDSLContext + .select(WORKFLOW_EXECUTIONS.EID) + .from(WORKFLOW_EXECUTIONS) + .where(WORKFLOW_EXECUTIONS.UID.eq(testUid)) + ) + ) + .execute() + getDSLContext + .deleteFrom(OPERATOR_EXECUTIONS) + .where( + OPERATOR_EXECUTIONS.WORKFLOW_EXECUTION_ID.in( + getDSLContext + .select(WORKFLOW_EXECUTIONS.EID) + .from(WORKFLOW_EXECUTIONS) + .where(WORKFLOW_EXECUTIONS.UID.eq(testUid)) + ) + ) + .execute() + getDSLContext + .deleteFrom(WORKFLOW_EXECUTIONS) + .where(WORKFLOW_EXECUTIONS.VID.in(vidSubquery)) + .execute() + getDSLContext.deleteFrom(WORKFLOW_VERSION).where(WORKFLOW_VERSION.WID.eq(testWid)).execute() + getDSLContext + .deleteFrom(WORKFLOW_USER_ACCESS) + .where(WORKFLOW_USER_ACCESS.UID.eq(testUid)) + .execute() + getDSLContext.deleteFrom(WORKFLOW_OF_USER).where(WORKFLOW_OF_USER.UID.eq(testUid)).execute() + getDSLContext.deleteFrom(WORKFLOW).where(WORKFLOW.WID.eq(testWid)).execute() + getDSLContext.deleteFrom(DATASET).where(DATASET.OWNER_UID.eq(testUid)).execute() + getDSLContext.deleteFrom(USER).where(USER.UID.eq(testUid)).execute() + } + + // ─── helpers ────────────────────────────────────────────────────────────── + + private def insertOwnership(): Unit = { + val ownership = new WorkflowOfUser + ownership.setUid(testUid) + ownership.setWid(testWid) + workflowOfUserDao.insert(ownership) + } + + private def insertAccess(): Unit = { + val access = new WorkflowUserAccess + access.setUid(testUid) + access.setWid(testWid) + access.setPrivilege(PrivilegeEnum.READ) + workflowUserAccessDao.insert(access) + } + + private def insertExecution(): WorkflowExecutions = { + val execution = new WorkflowExecutions + execution.setVid(testVersion.getVid) + execution.setUid(testUid) + execution.setStatus(0.toByte) + execution.setResult("") + execution.setLogLocation("") + execution.setStartingTime(new Timestamp(System.currentTimeMillis())) + execution.setBookmarked(false) + execution.setName("exec-" + UUID.randomUUID().toString.substring(0, 8)) + execution.setEnvironmentVersion("test-env-1.0") + workflowExecutionsDao.insert(execution) + execution + } + + private def insertDataset(): Dataset = { + val dataset = new Dataset + dataset.setOwnerUid(testUid) + dataset.setName("quota_ds_" + UUID.randomUUID().toString.substring(0, 8)) + dataset.setRepositoryName("repo-" + UUID.randomUUID().toString.substring(0, 8)) + dataset.setIsPublic(false) + dataset.setIsDownloadable(true) + dataset.setDescription("") + dataset.setCreationTime(new Timestamp(System.currentTimeMillis())) + datasetDao.insert(dataset) + dataset + } + + // ─── getUserCreatedWorkflow ──────────────────────────────────────────────── + + "getUserCreatedWorkflow" should "return an empty list when the user owns no workflows" in { + resetFixtures() + assert(UserQuotaResource.getUserCreatedWorkflow(testUid).isEmpty) + } + + it should "return the workflows owned by the user with their names and timestamps" in { + resetFixtures() + insertOwnership() + + val workflows = UserQuotaResource.getUserCreatedWorkflow(testUid) + + assert(workflows.size == 1) + val w = workflows.head + assert(w.userId == testUid) + assert(w.workflowId == testWid) + assert(w.workflowName == testWorkflow.getName) + // Compare against the DB round-tripped value: Postgres rounds the stored + // timestamp, so the in-memory pojo's millis would not match exactly. + assert(w.creationTime == workflowDao.fetchOneByWid(testWid).getCreationTime.getTime) + } + + // ─── getUserAccessedWorkflow ─────────────────────────────────────────────── + + "getUserAccessedWorkflow" should "return an empty list when the user has no access grants" in { + resetFixtures() + assert(UserQuotaResource.getUserAccessedWorkflow(testUid).isEmpty) + } + + it should "return the workflow ids the user can access" in { + resetFixtures() + insertAccess() + + val accessed = UserQuotaResource.getUserAccessedWorkflow(testUid) + + assert(accessed.size == 1) + assert(accessed.get(0) == testWid) + } + + // ─── getUserQuotaSize ────────────────────────────────────────────────────── + + "getUserQuotaSize" should "return an empty array when the user has no executions" in { + resetFixtures() + assert(UserQuotaResource.getUserQuotaSize(testUid).isEmpty) + } + + it should "assemble per-execution result / runtime-stats / log sizes" in { + resetFixtures() + val execution = insertExecution() + getDSLContext + .update(WORKFLOW_EXECUTIONS) + .set(WORKFLOW_EXECUTIONS.RUNTIME_STATS_SIZE, java.lang.Long.valueOf(200L)) + .where(WORKFLOW_EXECUTIONS.EID.eq(execution.getEid)) + .execute() + getDSLContext + .insertInto(OPERATOR_PORT_EXECUTIONS) + .columns( + OPERATOR_PORT_EXECUTIONS.WORKFLOW_EXECUTION_ID, + OPERATOR_PORT_EXECUTIONS.GLOBAL_PORT_ID, + OPERATOR_PORT_EXECUTIONS.RESULT_URI, + OPERATOR_PORT_EXECUTIONS.RESULT_SIZE + ) + .values(execution.getEid, "gp-1", "vfs:///r", java.lang.Long.valueOf(100L)) + .execute() + getDSLContext + .insertInto(OPERATOR_EXECUTIONS) + .columns( + OPERATOR_EXECUTIONS.WORKFLOW_EXECUTION_ID, + OPERATOR_EXECUTIONS.OPERATOR_ID, + OPERATOR_EXECUTIONS.CONSOLE_MESSAGES_URI, + OPERATOR_EXECUTIONS.CONSOLE_MESSAGES_SIZE + ) + .values(execution.getEid, "op-1", "vfs:///c", java.lang.Long.valueOf(50L)) + .execute() + + val quota = UserQuotaResource.getUserQuotaSize(testUid) + + assert(quota.length == 1) + val q = quota.head + assert(q.eid == execution.getEid) + assert(q.workflowId == testWid) + assert(q.workflowName == testWorkflow.getName) + assert(q.resultBytes == 100L) + assert(q.runTimeStatsBytes == 200L) + assert(q.logBytes == 50L) + } + + // ─── class endpoints (delegate to the object using the SessionUser's uid) ── + + "getCreatedWorkflow (endpoint)" should "return the current user's created workflows" in { + resetFixtures() + insertOwnership() + + val workflows = resource.getCreatedWorkflow(new SessionUser(testUser)) + + assert(workflows.size == 1) + assert(workflows.head.workflowId == testWid) + } + + "getAccessedWorkflow (endpoint)" should "return the current user's accessible workflow ids" in { + resetFixtures() + insertAccess() + + val accessed = resource.getAccessedWorkflow(new SessionUser(testUser)) + + assert(accessed.size == 1) + assert(accessed.get(0) == testWid) + } + + "getUserQuota (endpoint)" should "return the current user's quota storage array" in { + resetFixtures() + insertExecution() + + val quota = resource.getUserQuota(new SessionUser(testUser)) + + assert(quota.length == 1) + assert(quota.head.workflowId == testWid) + } + + "getCreatedDatasets (endpoint)" should "return an empty list when the user owns no datasets" in { + resetFixtures() + assert(resource.getCreatedDatasets(new SessionUser(testUser)).isEmpty) + } + + it should "return the datasets the user created (with size disabled to 0)" in { + resetFixtures() + val dataset = insertDataset() + + val datasets = resource.getCreatedDatasets(new SessionUser(testUser)) + + assert(datasets.size == 1) + assert(datasets.head.name == dataset.getName) + assert(datasets.head.size == 0L) + } + + // ─── deleteExecutionCollection / deleteCollection ────────────────────────── + + "deleteExecutionCollection" should "delete the execution row from WORKFLOW_EXECUTIONS" in { + resetFixtures() + val execution = insertExecution() + + UserQuotaResource.deleteExecutionCollection(execution.getEid) + + assert(workflowExecutionsDao.fetchOneByEid(execution.getEid) == null) + } + + "deleteCollection (endpoint)" should "delete the execution row for the given eid" in { + resetFixtures() + val execution = insertExecution() + + resource.deleteCollection(execution.getEid) + + assert(workflowExecutionsDao.fetchOneByEid(execution.getEid) == null) + } +}
