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-7230-390e2543278f8eb5acf863122805305f2f62525b in repository https://gitbox.apache.org/repos/asf/texera.git
commit 31d5536bf785529fefbe66a58331a0543d9052c8 Author: Meng Wang <[email protected]> AuthorDate: Sat Aug 1 01:23:41 2026 -0700 test(amber): add unit test coverage for HubResource (#7230) ### What changes were proposed in this PR? Adds a `MockTexeraDB`-backed spec for `HubResource` (previously ~0% coverage), following the sibling embedded-Postgres specs. Seeds an owner + a liker user and a public workflow (with ownership + access), mocks `HttpServletRequest`, and covers the non-LakeFS surface with 11 tests: - **Actions (object helpers):** `recordUserAction` writes a `user_action` row; `recordLikeAction` inserts a like the first time / removes it on unlike / returns `false` when already liked; `recordCloneAction` writes a clone row and an action row. - **Endpoints:** `postLike` / `postUnlike` / `isLiked` toggle the like; `getCount` counts public workflows; `postView` increments the view count (1 → 2) and records a view action; `getCounts` reports the like count; `getTops` lists the liked public entity under the `"like"` key; `userAccess` returns the granted user ids; `fetchDashboardWorkflowsByWids` returns the seeded workflow. All tests use `EntityType.Workflow` (the workflow tables), which keeps them off the LakeFS-backed dataset-size path. No production code was changed. ### Any related issues, documentation, discussions? Closes #7225. ### How was this PR tested? `sbt "WorkflowExecutionService/testOnly *HubResourceSpec"` — 11 passed, run twice in full plus the `getTops` / `getCounts` tests once in isolation to rule out order-dependence, against the embedded Postgres. The failure path was verified by breaking an assertion (1 failed, non-zero exit), and `Test/scalafmtCheck` + `Test/scalafix --check` are clean. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8 [1M context]) --- .../resource/dashboard/hub/HubResourceSpec.scala | 269 +++++++++++++++++++++ 1 file changed, 269 insertions(+) diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/hub/HubResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/hub/HubResourceSpec.scala new file mode 100644 index 0000000000..215f9865c5 --- /dev/null +++ b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/hub/HubResourceSpec.scala @@ -0,0 +1,269 @@ +/* + * 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.hub + +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.{ + UserDao, + WorkflowDao, + WorkflowOfUserDao, + WorkflowUserAccessDao +} +import org.apache.texera.dao.jooq.generated.tables.pojos.{ + User, + Workflow, + WorkflowOfUser, + WorkflowUserAccess +} +import org.apache.texera.web.resource.dashboard.hub.HubResource._ +import org.scalamock.scalatest.MockFactory +import org.scalatest.BeforeAndAfterAll +import org.scalatest.BeforeAndAfterEach +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import java.sql.Timestamp +import java.util.UUID +import javax.servlet.http.HttpServletRequest +import scala.jdk.CollectionConverters._ + +class HubResourceSpec + extends AnyFlatSpec + with Matchers + with MockFactory + with BeforeAndAfterAll + with BeforeAndAfterEach + with MockTexeraDB { + + // MockTexeraDB gives this suite its own UUID-named database, so fixed ids are + // safe and keep failures reproducible. + private val ownerUid = 9001 + private val likerUid = 9002 + private val wid = 7001 + private val Wf: EntityType = EntityType.Workflow + + private var userDao: UserDao = _ + private var workflowDao: WorkflowDao = _ + private var workflowOfUserDao: WorkflowOfUserDao = _ + private var workflowUserAccessDao: WorkflowUserAccessDao = _ + + // A request whose remote address is a valid IPv4 so recordUserAction stores it. + private def req: HttpServletRequest = { + val r = stub[HttpServletRequest] + (r.getRemoteAddr _).when().returns("127.0.0.1") + r + } + + private def session(uid: Int): SessionUser = { + val u = new User + u.setUid(Integer.valueOf(uid)) + new SessionUser(u) + } + + private def makeUser(uid: Int, name: String): User = { + val u = new User + u.setUid(Integer.valueOf(uid)) + u.setName(name) + u.setEmail(s"[email protected]") + u.setPassword("password") + u + } + + private def ids(values: Int*): java.util.List[Integer] = values.map(Integer.valueOf).asJava + private def types(values: EntityType*): java.util.List[EntityType] = values.toList.asJava + private def actions(values: ActionType*): java.util.List[ActionType] = values.toList.asJava + + private def likeRows: Int = + getDSLContext + .selectCount() + .from(WORKFLOW_USER_LIKES) + .where(WORKFLOW_USER_LIKES.WID.eq(wid).and(WORKFLOW_USER_LIKES.UID.eq(likerUid))) + .fetchOne(0, classOf[Integer]) + .intValue() + + private def cloneRows: Int = + getDSLContext + .selectCount() + .from(WORKFLOW_USER_CLONES) + .where(WORKFLOW_USER_CLONES.WID.eq(wid).and(WORKFLOW_USER_CLONES.UID.eq(likerUid))) + .fetchOne(0, classOf[Integer]) + .intValue() + + private def userActionRows: Int = + getDSLContext + .selectCount() + .from(USER_ACTION) + .where(USER_ACTION.RESOURCE_ID.eq(wid)) + .fetchOne(0, classOf[Integer]) + .intValue() + + private def isLiked(uid: Int): Boolean = + isLikedHelper(Integer.valueOf(uid), ids(wid), types(Wf)).asScala.head.isLiked + + override protected def beforeAll(): Unit = { + initializeDBAndReplaceDSLContext() + userDao = new UserDao(getDSLContext.configuration()) + workflowDao = new WorkflowDao(getDSLContext.configuration()) + workflowOfUserDao = new WorkflowOfUserDao(getDSLContext.configuration()) + workflowUserAccessDao = new WorkflowUserAccessDao(getDSLContext.configuration()) + + userDao.insert(makeUser(ownerUid, "hub_owner")) + userDao.insert(makeUser(likerUid, "hub_liker")) + + val wf = new Workflow + wf.setWid(Integer.valueOf(wid)) + wf.setName("hub_wf_" + UUID.randomUUID().toString.substring(0, 8)) + wf.setContent("{}") + wf.setDescription("hub test") + wf.setIsPublic(true) + wf.setCreationTime(new Timestamp(System.currentTimeMillis())) + wf.setLastModifiedTime(new Timestamp(System.currentTimeMillis())) + workflowDao.insert(wf) + + val ownership = new WorkflowOfUser + ownership.setUid(Integer.valueOf(ownerUid)) + ownership.setWid(Integer.valueOf(wid)) + workflowOfUserDao.insert(ownership) + + val access = new WorkflowUserAccess + access.setUid(Integer.valueOf(ownerUid)) + access.setWid(Integer.valueOf(wid)) + access.setPrivilege(PrivilegeEnum.WRITE) + workflowUserAccessDao.insert(access) + } + + override protected def afterAll(): Unit = closeConnectionPool() + + // Reset the per-entity hub rows so every test starts from a clean slate. + override protected def beforeEach(): Unit = { + val ctx = getDSLContext + ctx.deleteFrom(WORKFLOW_USER_LIKES).where(WORKFLOW_USER_LIKES.WID.eq(wid)).execute() + ctx.deleteFrom(WORKFLOW_USER_CLONES).where(WORKFLOW_USER_CLONES.WID.eq(wid)).execute() + ctx.deleteFrom(WORKFLOW_VIEW_COUNT).where(WORKFLOW_VIEW_COUNT.WID.eq(wid)).execute() + ctx.deleteFrom(USER_ACTION).where(USER_ACTION.RESOURCE_ID.eq(wid)).execute() + } + + "recordUserAction" should "insert a user_action row for the entity" in { + recordUserAction(req, Integer.valueOf(likerUid), Integer.valueOf(wid), Wf, ActionType.Like) + userActionRows shouldBe 1 + } + + "recordLikeAction" should "insert a like the first time and remove it on unlike" in { + recordLikeAction( + req, + Integer.valueOf(likerUid), + UserRequest(wid, Wf), + isLike = true + ) shouldBe true + likeRows shouldBe 1 + isLiked(likerUid) shouldBe true + + recordLikeAction( + req, + Integer.valueOf(likerUid), + UserRequest(wid, Wf), + isLike = false + ) shouldBe true + likeRows shouldBe 0 + isLiked(likerUid) shouldBe false + } + + it should "return false when liking something already liked" in { + recordLikeAction( + req, + Integer.valueOf(likerUid), + UserRequest(wid, Wf), + isLike = true + ) shouldBe true + recordLikeAction( + req, + Integer.valueOf(likerUid), + UserRequest(wid, Wf), + isLike = true + ) shouldBe false + likeRows shouldBe 1 + } + + "recordCloneAction" should "record a clone row and a user_action row" in { + recordCloneAction(req, Integer.valueOf(likerUid), Integer.valueOf(wid), Wf) + cloneRows shouldBe 1 + userActionRows shouldBe 1 + } + + "postLike / postUnlike / isLiked" should "toggle the like through the resource endpoints" in { + val resource = new HubResource() + resource.postLike(session(likerUid), req, UserRequest(wid, Wf)) shouldBe true + resource.isLiked(session(likerUid), ids(wid), types(Wf)).asScala.head.isLiked shouldBe true + + resource.postUnlike(session(likerUid), req, UserRequest(wid, Wf)) shouldBe true + resource.isLiked(session(likerUid), ids(wid), types(Wf)).asScala.head.isLiked shouldBe false + } + + "getCount" should "count the public workflows" in { + // this suite's isolated database holds exactly one public workflow + new HubResource().getCount(Wf).intValue() shouldBe 1 + } + + "postView" should "increment the view count and record a view action" in { + val resource = new HubResource() + resource.postView( + req, + ViewRequest(Integer.valueOf(wid), Integer.valueOf(likerUid), Wf) + ) shouldBe 1 + resource.postView( + req, + ViewRequest(Integer.valueOf(wid), Integer.valueOf(likerUid), Wf) + ) shouldBe 2 + userActionRows shouldBe 2 + } + + "getCounts" should "report the like count for the entity" in { + recordLikeAction(req, Integer.valueOf(likerUid), UserRequest(wid, Wf), isLike = true) + + val response = + new HubResource().getCounts(types(Wf), ids(wid), actions(ActionType.Like)).asScala.head + response.entityId shouldBe Integer.valueOf(wid) + response.counts.get(ActionType.Like) shouldBe 1 + } + + "getTops" should "list the liked public entity under the 'like' key" in { + recordLikeAction(req, Integer.valueOf(likerUid), UserRequest(wid, Wf), isLike = true) + + val tops = new HubResource().getTops(Wf, actions(ActionType.Like), null, null) + tops.keySet.asScala should contain("like") + val likedWids = tops.get("like").asScala.flatMap(_.workflow.map(_.workflow.getWid.intValue())) + likedWids should contain(wid) + } + + "userAccess" should "return the user ids granted access to the entity" in { + val response = new HubResource().userAccess(types(Wf), ids(wid)).asScala.head + response.entityId shouldBe Integer.valueOf(wid) + response.userIds.asScala should contain(Integer.valueOf(ownerUid)) + } + + "fetchDashboardWorkflowsByWids" should "return the seeded workflow" in { + val results = + fetchDashboardWorkflowsByWids(Seq(Integer.valueOf(wid)), Integer.valueOf(ownerUid)) + results.map(_.workflow.getWid.intValue()) should contain(wid) + } +}
