This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new 348fa88444 test(amber): add unit test coverage for AuthResource (#7180)
348fa88444 is described below
commit 348fa884442990307b842fd7c0dd6ca005dd320c
Author: Meng Wang <[email protected]>
AuthorDate: Fri Jul 31 15:23:50 2026 -0700
test(amber): add unit test coverage for AuthResource (#7180)
### What changes were proposed in this PR?
Adds a spec for `AuthResource`
(`amber/src/main/scala/org/apache/texera/web/resource/auth/AuthResource.scala`),
which had no test coverage. Authentication is against the user table
(jOOQ) and
JWTs are issued via `JwtAuth`, so the spec runs on embedded Postgres via
`MockTexeraDB` (the established unit pattern, mirroring
`AdminExecutionResourceSpec` / `WorkflowExecutionsResourceSpec`). No
production
code was changed.
15 tests:
- `retrieveUserByUsernameAndPassword` — returns the user for correct
credentials;
`None` for a wrong password, an unknown user, and a null
username/password.
- `login` — issues a JWT whose subject (decoded through
`JwtAuth.jwtConsumer`) is
the username for valid credentials, and rejects bad credentials with
`NotAuthorizedException`.
- `register` — persists a `RESTRICTED` user with a hashed password
(verified with
the same `StrongPasswordEncryptor`) and issues a token, and rejects each
guard
branch (`empty username` / `empty email` / `malformed email` / `empty
password`
/ `duplicate username` / `duplicate email`) with
`NotAcceptableException`.
- `createAdminUser` — inserts the configured admin with the `ADMIN` role
and a
hashed password, and is idempotent when an admin already exists.
Tokens are verified by decoding through `JwtAuth.jwtConsumer`
(subject/claims)
rather than asserting raw strings, so there are no time-sensitive
assertions.
### Any related issues, documentation, discussions?
Closes #7164
### How was this PR tested?
Unit tests, run locally against embedded Postgres (`MockTexeraDB`). All
pass, and
the failure path was verified by breaking an assertion to confirm the
suite goes
red:
```
sbt "WorkflowExecutionService/testOnly *AuthResourceSpec"
# Tests: succeeded 15, failed 0
sbt "WorkflowExecutionService/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])
---
.../web/resource/auth/AuthResourceSpec.scala | 209 +++++++++++++++++++++
1 file changed, 209 insertions(+)
diff --git
a/amber/src/test/scala/org/apache/texera/web/resource/auth/AuthResourceSpec.scala
b/amber/src/test/scala/org/apache/texera/web/resource/auth/AuthResourceSpec.scala
new file mode 100644
index 0000000000..c832011baf
--- /dev/null
+++
b/amber/src/test/scala/org/apache/texera/web/resource/auth/AuthResourceSpec.scala
@@ -0,0 +1,209 @@
+/*
+ * 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.auth
+
+import org.apache.texera.auth.JwtAuth
+import org.apache.texera.common.config.UserSystemConfig
+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.apache.texera.web.model.http.request.auth.{UserLoginRequest,
UserRegistrationRequest}
+import org.jasypt.util.password.StrongPasswordEncryptor
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
+
+import java.util.UUID
+import javax.ws.rs.{NotAcceptableException, NotAuthorizedException}
+
+class AuthResourceSpec
+ extends AnyFlatSpec
+ with Matchers
+ with BeforeAndAfterAll
+ with BeforeAndAfterEach
+ with MockTexeraDB {
+
+ // Random suffix keeps this suite's rows from colliding with data other
suites may
+ // have left in the shared (singleton) embedded DB; suites run sequentially.
+ private val runId = UUID.randomUUID().toString.substring(0, 8)
+ // jasypt verifies a plain password against a stored hash; a fresh instance
works fine.
+ private val encryptor = new StrongPasswordEncryptor()
+
+ private var userDao: UserDao = _
+ private var resource: AuthResource = _
+
+ private def uname(tag: String): String = s"authspec_${tag}_$runId"
+
+ private def uemail(tag: String): String =
s"authspec_${tag}[email protected]"
+
+ override protected def beforeAll(): Unit = initializeDBAndReplaceDSLContext()
+
+ override protected def afterAll(): Unit = shutdownDB()
+
+ override protected def beforeEach(): Unit = {
+ userDao = new UserDao(getDSLContext.configuration())
+ resource = new AuthResource()
+ cleanup()
+ }
+
+ override protected def afterEach(): Unit = cleanup()
+
+ private def cleanup(): Unit = {
+ // startsWith escapes SQL LIKE wildcards, so the literal "authspec_"
prefix is matched exactly.
+
getDSLContext.deleteFrom(USER).where(USER.NAME.startsWith("authspec_")).execute()
+ // createAdminUser() seeds the configured admin — remove it too so the
test starts clean.
+
getDSLContext.deleteFrom(USER).where(USER.NAME.eq(UserSystemConfig.adminUsername)).execute()
+ }
+
+ private def seedUser(
+ name: String,
+ password: String,
+ role: UserRoleEnum = UserRoleEnum.REGULAR
+ ): User = {
+ val user = new User
+ user.setName(name)
+ user.setEmail(s"[email protected]")
+ user.setPassword(encryptor.encryptPassword(password))
+ user.setRole(role)
+ userDao.insert(user)
+ user
+ }
+
+ private def subjectOf(token: String): String =
+ JwtAuth.jwtConsumer.processToClaims(token).getSubject
+
+ // ─── retrieveUserByUsernameAndPassword
──────────────────────────────────────
+
+ "retrieveUserByUsernameAndPassword" should "return the user for correct
credentials" in {
+ seedUser(uname("ok"), "secret")
+ AuthResource
+ .retrieveUserByUsernameAndPassword(uname("ok"), "secret")
+ .map(_.getName) shouldBe Some(uname("ok"))
+ }
+
+ it should "return None for a wrong password" in {
+ seedUser(uname("wp"), "secret")
+ AuthResource.retrieveUserByUsernameAndPassword(uname("wp"), "wrong")
shouldBe None
+ }
+
+ it should "return None for an unknown user" in {
+ AuthResource.retrieveUserByUsernameAndPassword(uname("nobody"), "secret")
shouldBe None
+ }
+
+ it should "return None when the username or password is null" in {
+ seedUser(uname("nul"), "secret")
+ AuthResource.retrieveUserByUsernameAndPassword(null, "secret") shouldBe
None
+ AuthResource.retrieveUserByUsernameAndPassword(uname("nul"), null)
shouldBe None
+ }
+
+ // ─── login
──────────────────────────────────────────────────────────────────
+
+ "login" should "issue a JWT whose subject is the username for valid
credentials" in {
+ seedUser(uname("login"), "pw")
+ val response = resource.login(UserLoginRequest(uname("login"), "pw"))
+ response.accessToken should not be empty
+ subjectOf(response.accessToken) shouldBe uname("login")
+ }
+
+ it should "reject invalid credentials with NotAuthorizedException" in {
+ seedUser(uname("bad"), "pw")
+
assertThrows[NotAuthorizedException](resource.login(UserLoginRequest(uname("bad"),
"nope")))
+ }
+
+ // ─── register ─────────────────────────────────────────────────────────────
+
+ "register" should "persist a RESTRICTED user with a hashed password and
issue a token" in {
+ val response = resource.register(UserRegistrationRequest(uname("reg"),
uemail("reg"), "pw"))
+
+ subjectOf(response.accessToken) shouldBe uname("reg")
+ val persisted = userDao.fetchByName(uname("reg"))
+ persisted.size() shouldBe 1
+ val stored = persisted.get(0)
+ stored.getRole shouldBe UserRoleEnum.RESTRICTED
+ stored.getEmail shouldBe uemail("reg")
+ // stored hashed, not in plain text, but verifies against the plain
password
+ stored.getPassword should not be "pw"
+ encryptor.checkPassword("pw", stored.getPassword) shouldBe true
+ }
+
+ it should "reject an empty username" in {
+ val ex = intercept[NotAcceptableException](
+ resource.register(UserRegistrationRequest(" ", uemail("eu"), "pw"))
+ )
+ ex.getMessage should include("Username")
+ }
+
+ it should "reject an empty email" in {
+ val ex = intercept[NotAcceptableException](
+ resource.register(UserRegistrationRequest(uname("ee"), " ", "pw"))
+ )
+ ex.getMessage should include("Email cannot be empty")
+ }
+
+ it should "reject a malformed email" in {
+ val ex =
+ intercept[NotAcceptableException](
+ resource.register(UserRegistrationRequest(uname("mf"), "not-an-email",
"pw"))
+ )
+ ex.getMessage should include("Email format is invalid")
+ }
+
+ it should "reject an empty password" in {
+ val ex = intercept[NotAcceptableException](
+ resource.register(UserRegistrationRequest(uname("ep"), uemail("ep"), ""))
+ )
+ ex.getMessage should include("Password")
+ }
+
+ it should "reject a duplicate username" in {
+ seedUser(uname("dupu"), "pw")
+ val ex = intercept[NotAcceptableException](
+ resource.register(UserRegistrationRequest(uname("dupu"),
uemail("dupu_other"), "pw2"))
+ )
+ ex.getMessage should include("Username exists")
+ }
+
+ it should "reject a duplicate email" in {
+ val existing = seedUser(uname("dupeowner"), "pw")
+ val ex = intercept[NotAcceptableException](
+ resource.register(UserRegistrationRequest(uname("dupenew"),
existing.getEmail, "pw2"))
+ )
+ ex.getMessage should include("Email exists")
+ }
+
+ // ─── createAdminUser
────────────────────────────────────────────────────────
+
+ "createAdminUser" should "insert the configured admin with the ADMIN role
and a hashed password" in {
+ AuthResource.createAdminUser()
+
+ val admins = userDao.fetchByName(UserSystemConfig.adminUsername)
+ admins.size() shouldBe 1
+ admins.get(0).getRole shouldBe UserRoleEnum.ADMIN
+ encryptor.checkPassword(UserSystemConfig.adminPassword,
admins.get(0).getPassword) shouldBe true
+ }
+
+ it should "not create a second admin when one already exists" in {
+ AuthResource.createAdminUser()
+ AuthResource.createAdminUser()
+ userDao.fetchByName(UserSystemConfig.adminUsername).size() shouldBe 1
+ }
+}