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-7399-a38bc6659d314b5cbbc32a2233934bb3e0d4ce37 in repository https://gitbox.apache.org/repos/asf/texera.git
commit c5c2c6f8c2aeb746929a5f56b9d36eaa0373848e Author: Eugene Gu <[email protected]> AuthorDate: Sat Aug 8 00:06:27 2026 -0700 test(amber): add unit tests cover ProjectSearchQueryBuilder (#7399) ### What changes were proposed in this PR? Adds `ProjectSearchQueryBuilderSpec`, a connection-free unit spec for the project arm of the unified dashboard search. `ProjectSearchQueryBuilder` was the only search query builder without tests — `WorkflowSearchQueryBuilderSpec` already exists, and `DatasetSearchQueryBuilder` is driven from `DatasetResourceSpec`. The spec follows the same conventions as the sibling `WorkflowSearchQueryBuilderSpec` (in-memory jOOQ records rendered with the Postgres dialect, no database connection) and covers the two members the object widens from the trait's `protected` to public: **`toEntryImpl` — the record-to-DTO mapping (4 tests):** copies every `PROJECT` column into the `Project` POJO with distinct fixture values so a wrong-column read cannot pass; passes NULL description/color through (the only two nullable project columns); tags the entry `resourceType == "project"` with the `workflow`/`dataset` payload slots `None` (the dashboard dispatch matches on this value with no default branch, so drift is a runtime `MatchError`); and produces an identical entry regardless of the caller's uid (the project arm computes no ownership flag). **`mappedResourceSchema` — the projection the union and dispatch depend on (5 tests):** pins the inline `'project'` resourceType literal; pins that `PROJECT.CREATION_TIME` is aliased as both the creation and last-modified time — a deliberate alias (the project table has no last-modified column) that keeps projects sortable by edit time instead of NULL-sinking; and pins the color, pid/ownerId, and name/description projection slots. `constructFromClause`, `constructWhereClause`, and `getGroupByFields` stay `override protected` (Scala `protected` grants no same-package access), so they are intentionally out of scope. Every test was mutation-checked: 8 distinct hand-applied mutations of the production object (literal changes, column swaps, dropped alias, empty-POJO copy, `project = None`) each fail the intended test, with production sources restored afterwards. No production file is touched. ### Any related issues, documentation, discussions? Closes #7398 ### How was this PR tested? Nine new tests; the spec is the only change: ``` sbt "WorkflowExecutionService/testOnly org.apache.texera.web.resource.dashboard.ProjectSearchQueryBuilderSpec" ``` ``` [info] Tests: succeeded 9, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. ``` The file is formatted with the project's scalafmt (no diff on re-run). ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Fable 5) --- .../dashboard/ProjectSearchQueryBuilderSpec.scala | 141 +++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilderSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilderSpec.scala new file mode 100644 index 0000000000..0a05846dda --- /dev/null +++ b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilderSpec.scala @@ -0,0 +1,141 @@ +/* + * 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 + +import org.apache.texera.dao.jooq.generated.Tables.PROJECT +import org.apache.texera.dao.jooq.generated.tables.pojos.Project +import org.jooq.impl.{DSL => JDSL} +import org.jooq.{Record, SQLDialect} + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import java.sql.Timestamp + +class ProjectSearchQueryBuilderSpec extends AnyFlatSpec with Matchers { + + private val ctx = JDSL.using(SQLDialect.POSTGRES) + + private val ownerUid: Integer = Integer.valueOf(42) + private val callerUid: Integer = Integer.valueOf(43) + private val pid: Integer = Integer.valueOf(7) + private val createdAt = new Timestamp(1700000000123L) + + // In-memory record shaped like the one toEntryImpl receives (keyed by the + // original PROJECT fields). Values are distinct so a wrong-column read fails. + private def translatedRecord( + description: String = "proj-description", + color: String = "aabbcc" + ): Record = { + val record = ctx.newRecord( + PROJECT.PID, + PROJECT.NAME, + PROJECT.DESCRIPTION, + PROJECT.OWNER_ID, + PROJECT.CREATION_TIME, + PROJECT.COLOR + ) + record.set(PROJECT.PID, pid) + record.set(PROJECT.NAME, "proj-name") + record.set(PROJECT.DESCRIPTION, description) + record.set(PROJECT.OWNER_ID, ownerUid) + record.set(PROJECT.CREATION_TIME, createdAt) + record.set(PROJECT.COLOR, color) + record + } + + private def projectOf(record: Record, uid: Integer): Project = + ProjectSearchQueryBuilder.toEntryImpl(uid, record).project.get + + "toEntryImpl" should "copy every project column into the POJO" in { + val p = projectOf(translatedRecord(), ownerUid) + p.getPid shouldBe pid + p.getName shouldBe "proj-name" + p.getDescription shouldBe "proj-description" + p.getOwnerId shouldBe ownerUid + p.getCreationTime shouldBe createdAt + p.getColor shouldBe "aabbcc" + } + + it should "pass a NULL description and a NULL color through as null" in { + // description and color are the only nullable project columns. + val p = projectOf(translatedRecord(description = null, color = null), ownerUid) + p.getDescription shouldBe null + p.getColor shouldBe null + p.getPid shouldBe pid + } + + it should "tag the entry as a project and leave the other payload slots empty" in { + // searchAllResources matches on resourceType with no default branch, so a + // wrong tag is a runtime MatchError. + val entry = ProjectSearchQueryBuilder.toEntryImpl(ownerUid, translatedRecord()) + entry.resourceType shouldBe "project" + entry.project should not be None + entry.workflow shouldBe None + entry.dataset shouldBe None + } + + it should "produce the same entry regardless of the caller's uid" in { + // Unlike the workflow arm there is no ownership flag to compute. + val record = translatedRecord() + ProjectSearchQueryBuilder.toEntryImpl(ownerUid, record) shouldBe + ProjectSearchQueryBuilder.toEntryImpl(callerUid, record) + } + + // An aliased field renders as the bare alias on its own, so render a SELECT. + private lazy val renderedSchema: String = ctx.renderInlined( + JDSL.select(ProjectSearchQueryBuilder.mappedResourceSchema.allFields: _*) + ) + + "mappedResourceSchema" should "project the literal 'project' as the resourceType column" in { + renderedSchema should include("'project' as \"resourceType\"") + } + + it should "alias PROJECT.CREATION_TIME as both the creation and last-modified time" in { + // Deliberate: the project table has no last-modified column. Without this + // alias projects would NULL-sink in every sort by edit time. + val creationTime = ctx.renderInlined(PROJECT.CREATION_TIME) + renderedSchema should include(s"""$creationTime as "resourceCreationTime"""") + renderedSchema should include(s"""$creationTime as "resourceLastModifiedTime"""") + } + + it should "project PROJECT.COLOR as the color column" in { + // The frontend reads the project colour swatch from this alias. + val color = ctx.renderInlined(PROJECT.COLOR) + renderedSchema should include(s"""$color as "color"""") + } + + it should "project the project id and owner through the shared slots" in { + // The owner rides the generic resourceOwnerId slot; the project-specific + // "owner_uid" slot stays NULL and is deliberately not pinned here. + renderedSchema should include(s"""${ctx.renderInlined(PROJECT.PID)} as "pid"""") + renderedSchema should include( + s"""${ctx.renderInlined(PROJECT.OWNER_ID)} as "resourceOwnerId"""" + ) + } + + it should "project the name and description through the shared full-text slots" in { + // Also the two columns the keyword filter targets. + renderedSchema should include(s"""${ctx.renderInlined(PROJECT.NAME)} as "resourceName"""") + renderedSchema should include( + s"""${ctx.renderInlined(PROJECT.DESCRIPTION)} as "resourceDescription"""" + ) + } +}
