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/08/14 06:32:01 UTC
(texera) 01/01: test(amber): add specs for GuestAuthFilter and UserResource (#7652)
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 <me...@uci.edu>
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"joining_reason_$testUid@example.com")
+ 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
+ }
+}