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

commit 938342afb31a8b51faeec75e5d98116a694b11e2
Author: Meng Wang <[email protected]>
AuthorDate: Fri Aug 14 06:04:06 2026 +0000

    test(amber): add specs for GuestAuthFilter and UserResource (#7652)
    
    ### What changes were proposed in this PR?
    
    Adds specs for two amber classes that had none. No production code was
    changed.
    
    **`GuestAuthFilterSpec`** (+8 tests) — pure, no database. The filter is
    built
    through its `Builder` with a small test authorizer, run over a stubbed
    `ContainerRequestContext`, and the `SecurityContext` it installs is
    captured and
    asserted:
    
    - `getUserPrincipal` is a `SessionUser` wrapping
    `GuestAuthFilter.GUEST`;
    - `isUserInRole` delegates to the authorizer — both an allowed and a
    denied role;
    - `isSecure` inherits from the incoming context — secure, insecure, and
    the
      **null** incoming context the code guards for;
    - `getAuthenticationScheme` reflects the scheme `filter()` authenticates
    with.
    
    Plus the `GUEST` preset's fields and `Builder` returning a fresh filter
    each time.
    
    **`UserResourceSpec`** (+7 tests) — jOOQ against embedded Postgres via
    `MockTexeraDB`:
    
    - `isJoiningReasonRequired` — true while unset, false once stored, and a
    404
      `WebApplicationException` for an unknown uid;
    - `updateJoiningReason` — persists affiliation/reason and flips the
    prompt off,
      trims both values, and defaults a null affiliation to an empty string.
    
    Two branches the issue does not mention are covered as well, since they
    are the
    guard `updateJoiningReason` opens with: a blank/whitespace reason and a
    null
    reason each raise 400, and the blank case is asserted to leave the
    stored user
    untouched.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7650
    
    ### How was this PR tested?
    
    Unit tests, run locally (the `UserResource` half against embedded
    Postgres). All
    pass, and the failure path was verified by breaking an assertion to
    confirm the
    suite goes red:
    
    ```
    sbt "WorkflowExecutionService/testOnly *GuestAuthFilterSpec 
*UserResourceSpec"
    # Tests: succeeded 33, failed 0
    sbt "WorkflowExecutionService/Test/scalafmtCheck"      # clean
    sbt "WorkflowExecutionService/Test/scalafix --check"   # clean
    ```
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../texera/web/auth/GuestAuthFilterSpec.scala      | 112 +++++++++++++++++
 .../resource/dashboard/user/UserResourceSpec.scala | 138 +++++++++++++++++++++
 2 files changed, 250 insertions(+)

diff --git 
a/amber/src/test/scala/org/apache/texera/web/auth/GuestAuthFilterSpec.scala 
b/amber/src/test/scala/org/apache/texera/web/auth/GuestAuthFilterSpec.scala
new file mode 100644
index 0000000000..5dfacbf9e5
--- /dev/null
+++ b/amber/src/test/scala/org/apache/texera/web/auth/GuestAuthFilterSpec.scala
@@ -0,0 +1,112 @@
+/*
+ * 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.auth
+
+import io.dropwizard.auth.Authorizer
+import org.apache.texera.auth.SessionUser
+import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum
+import org.scalamock.scalatest.MockFactory
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+import javax.ws.rs.container.ContainerRequestContext
+import javax.ws.rs.core.SecurityContext
+
+/**
+  * Covers the guest identity [[GuestAuthFilter]] installs on an 
unauthenticated request.
+  * The filter is pure — it only rewrites the request's SecurityContext — so 
no database or
+  * Jersey runtime is involved here.
+  */
+class GuestAuthFilterSpec extends AnyFlatSpec with Matchers with MockFactory {
+
+  /** Authorizes exactly the "REGULAR" role, so both arms of isUserInRole are 
observable. */
+  private val roleAuthorizer: Authorizer[SessionUser] =
+    (_: SessionUser, role: String) => role == UserRoleEnum.REGULAR.getLiteral
+
+  /**
+    * Runs the filter over a request whose incoming SecurityContext is 
`incoming` and returns the
+    * SecurityContext the filter installed in its place.
+    */
+  private def installedContext(incoming: SecurityContext): SecurityContext = {
+    val filter = new 
GuestAuthFilter.Builder().setAuthorizer(roleAuthorizer).buildAuthFilter()
+    var installed: SecurityContext = null
+
+    val requestContext = stub[ContainerRequestContext]
+    (() => requestContext.getSecurityContext).when().returns(incoming)
+    (requestContext.setSecurityContext _)
+      .when(*)
+      .onCall((ctx: SecurityContext) => installed = ctx)
+
+    filter.filter(requestContext)
+    installed should not be null
+    installed
+  }
+
+  private def secureContext(secure: Boolean): SecurityContext = {
+    val ctx = stub[SecurityContext]
+    (() => ctx.isSecure).when().returns(secure)
+    ctx
+  }
+
+  "GuestAuthFilter.GUEST" should "be a REGULAR user named guest" in {
+    GuestAuthFilter.GUEST.getName shouldBe "guest"
+    GuestAuthFilter.GUEST.getRole shouldBe UserRoleEnum.REGULAR
+  }
+
+  "GuestAuthFilter.Builder" should "build a fresh filter each time" in {
+    val builder = new GuestAuthFilter.Builder().setAuthorizer(roleAuthorizer)
+    val first = builder.buildAuthFilter()
+    val second = builder.buildAuthFilter()
+
+    first should not be theSameInstanceAs(second)
+  }
+
+  "the installed SecurityContext" should "carry the guest user as its 
principal" in {
+    val principal = installedContext(secureContext(secure = 
false)).getUserPrincipal
+
+    principal shouldBe a[SessionUser]
+    principal.asInstanceOf[SessionUser].getUser shouldBe GuestAuthFilter.GUEST
+  }
+
+  it should "delegate isUserInRole to the authorizer" in {
+    val context = installedContext(secureContext(secure = false))
+
+    context.isUserInRole(UserRoleEnum.REGULAR.getLiteral) shouldBe true
+    context.isUserInRole(UserRoleEnum.ADMIN.getLiteral) shouldBe false
+  }
+
+  it should "inherit isSecure from a secure incoming context" in {
+    installedContext(secureContext(secure = true)).isSecure shouldBe true
+  }
+
+  it should "inherit isSecure from an insecure incoming context" in {
+    installedContext(secureContext(secure = false)).isSecure shouldBe false
+  }
+
+  it should "report an insecure request when there is no incoming context" in {
+    // filter() guards with `securityContext != null` before reading isSecure
+    installedContext(null).isSecure shouldBe false
+  }
+
+  it should "expose the scheme the filter authenticated with" in {
+    // GuestAuthFilter.filter passes an empty scheme to authenticate()
+    installedContext(secureContext(secure = false)).getAuthenticationScheme 
shouldBe ""
+  }
+}
diff --git 
a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/UserResourceSpec.scala
 
b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/UserResourceSpec.scala
new file mode 100644
index 0000000000..90258cc499
--- /dev/null
+++ 
b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/UserResourceSpec.scala
@@ -0,0 +1,138 @@
+/*
+ * 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
+
+import org.apache.texera.dao.MockTexeraDB
+import org.apache.texera.dao.jooq.generated.Tables.USER
+import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum
+import org.apache.texera.dao.jooq.generated.tables.daos.UserDao
+import org.apache.texera.dao.jooq.generated.tables.pojos.User
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
+
+import javax.ws.rs.WebApplicationException
+import javax.ws.rs.core.Response
+
+/**
+  * Covers the joining-reason endpoints against embedded Postgres: whether a 
user still has to be
+  * prompted, and that submitting a reason persists and flips that answer.
+  */
+class UserResourceSpec
+    extends AnyFlatSpec
+    with Matchers
+    with BeforeAndAfterAll
+    with BeforeAndAfterEach
+    with MockTexeraDB {
+
+  private val testUid = 8000 + scala.util.Random.nextInt(1000)
+  private val unknownUid = testUid + 1
+
+  private var userDao: UserDao = _
+  private var resource: UserResource = _
+
+  override protected def beforeAll(): Unit = initializeDBAndReplaceDSLContext()
+
+  override protected def afterAll(): Unit = {
+    cleanup()
+    shutdownDB()
+  }
+
+  override protected def beforeEach(): Unit = {
+    userDao = new UserDao(getDSLContext.configuration())
+    resource = new UserResource()
+    cleanup()
+    userDao.insert(seedUser())
+  }
+
+  override protected def afterEach(): Unit = cleanup()
+
+  private def cleanup(): Unit =
+    getDSLContext.deleteFrom(USER).where(USER.UID.in(testUid, 
unknownUid)).execute()
+
+  /** A freshly registered user: joining reason not yet supplied. */
+  private def seedUser(): User = {
+    val user = new User
+    user.setUid(testUid)
+    user.setName(s"joining_reason_user_$testUid")
+    user.setEmail(s"[email protected]")
+    user.setRole(UserRoleEnum.REGULAR)
+    user
+  }
+
+  "isJoiningReasonRequired" should "be true while the user has not supplied 
one" in {
+    resource.isJoiningReasonRequired(testUid) shouldBe true
+  }
+
+  it should "be false once a joining reason is stored" in {
+    val user = userDao.fetchOneByUid(testUid)
+    user.setJoiningReason("research")
+    userDao.update(user)
+
+    resource.isJoiningReasonRequired(testUid) shouldBe false
+  }
+
+  it should "report 404 for a user that does not exist" in {
+    val ex = 
intercept[WebApplicationException](resource.isJoiningReasonRequired(unknownUid))
+    ex.getResponse.getStatus shouldBe Response.Status.NOT_FOUND.getStatusCode
+  }
+
+  "updateJoiningReason" should "persist the affiliation and reason, and stop 
the prompt" in {
+    resource.updateJoiningReason(RegistrationUpdateRequest(testUid, "UC 
Irvine", "research"))
+
+    val stored = userDao.fetchOneByUid(testUid)
+    stored.getAffiliation shouldBe "UC Irvine"
+    stored.getJoiningReason shouldBe "research"
+    resource.isJoiningReasonRequired(testUid) shouldBe false
+  }
+
+  it should "trim the submitted values" in {
+    resource.updateJoiningReason(
+      RegistrationUpdateRequest(testUid, "  UC Irvine  ", "  research  ")
+    )
+
+    val stored = userDao.fetchOneByUid(testUid)
+    stored.getAffiliation shouldBe "UC Irvine"
+    stored.getJoiningReason shouldBe "research"
+  }
+
+  it should "default a null affiliation to an empty string" in {
+    resource.updateJoiningReason(RegistrationUpdateRequest(testUid, null, 
"research"))
+
+    userDao.fetchOneByUid(testUid).getAffiliation shouldBe ""
+  }
+
+  it should "reject a blank reason with 400 and leave the user untouched" in {
+    val ex = intercept[WebApplicationException](
+      resource.updateJoiningReason(RegistrationUpdateRequest(testUid, "UC 
Irvine", "   "))
+    )
+
+    ex.getResponse.getStatus shouldBe Response.Status.BAD_REQUEST.getStatusCode
+    userDao.fetchOneByUid(testUid).getJoiningReason shouldBe null
+  }
+
+  it should "reject a null reason with 400" in {
+    val ex = intercept[WebApplicationException](
+      resource.updateJoiningReason(RegistrationUpdateRequest(testUid, "UC 
Irvine", null))
+    )
+
+    ex.getResponse.getStatus shouldBe Response.Status.BAD_REQUEST.getStatusCode
+  }
+}

Reply via email to