mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 15:27:01 +00:00
password reset tests
This commit is contained in:
parent
634d583105
commit
b461724299
@ -24,7 +24,7 @@ import code.api.v4_0_0.JSONFactory400.createCallsLimitJson
|
||||
import code.api.v5_0_0.JSONFactory500
|
||||
import code.api.v5_0_0.{ViewJsonV500, ViewsJsonV500}
|
||||
import code.api.v5_1_0.{JSONFactory510, PostCustomerLegalNameJsonV510}
|
||||
import code.api.v6_0_0.JSONFactory600.{DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupJsonV600, GroupMembershipJsonV600, GroupMembershipsJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson}
|
||||
import code.api.v6_0_0.JSONFactory600.{DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupJsonV600, GroupMembershipJsonV600, GroupMembershipsJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson}
|
||||
import code.api.v6_0_0.OBPAPI6_0_0
|
||||
import code.metrics.APIMetrics
|
||||
import code.bankconnectors.LocalMappedConnectorInternal
|
||||
@ -3212,6 +3212,133 @@ trait APIMethods600 {
|
||||
}
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
resetPasswordUrl,
|
||||
implementedInApiVersion,
|
||||
nameOf(resetPasswordUrl),
|
||||
"POST",
|
||||
"/management/user/reset-password-url",
|
||||
"Create Password Reset URL and Send Email",
|
||||
s"""Create a password reset URL for a user and automatically send it via email.
|
||||
|
|
||||
|This endpoint generates a password reset URL and sends it to the user's email address.
|
||||
|
|
||||
|${userAuthenticationMessage(true)}
|
||||
|
|
||||
|Behavior:
|
||||
|- Generates a unique password reset token
|
||||
|- Creates a reset URL using the portal_external_url property (falls back to API hostname)
|
||||
|- Sends an email to the user with the reset link
|
||||
|- Returns the reset URL in the response for logging/tracking purposes
|
||||
|
|
||||
|Required fields:
|
||||
|- username: The user's username (typically email)
|
||||
|- email: The user's email address (must match username)
|
||||
|- user_id: The user's UUID
|
||||
|
|
||||
|The user must exist and be validated before a reset URL can be generated.
|
||||
|
|
||||
|Email configuration must be set up correctly for email delivery to work.
|
||||
|
|
||||
|""".stripMargin,
|
||||
PostResetPasswordUrlJsonV600(
|
||||
"user@example.com",
|
||||
"user@example.com",
|
||||
"74a8ebcc-10e4-4036-bef3-9835922246bf"
|
||||
),
|
||||
ResetPasswordUrlJsonV600(
|
||||
"https://api.example.com/user_mgt/reset_password/QOL1CPNJPCZ4BRMPX3Z01DPOX1HMGU3L"
|
||||
),
|
||||
List(
|
||||
$UserNotLoggedIn,
|
||||
UserHasMissingRoles,
|
||||
InvalidJsonFormat,
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagUser),
|
||||
Some(List(canCreateResetPasswordUrl))
|
||||
)
|
||||
|
||||
lazy val resetPasswordUrl: OBPEndpoint = {
|
||||
case "management" :: "user" :: "reset-password-url" :: Nil JsonPost json -> _ => {
|
||||
cc => implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
(Full(u), callContext) <- authenticatedAccess(cc)
|
||||
_ <- Helper.booleanToFuture(
|
||||
failMsg = ErrorMessages.NotAllowedEndpoint,
|
||||
cc = callContext
|
||||
) {
|
||||
APIUtil.getPropsAsBoolValue("ResetPasswordUrlEnabled", false)
|
||||
}
|
||||
postedData <- NewStyle.function.tryons(
|
||||
s"$InvalidJsonFormat The Json body should be the ${classOf[PostResetPasswordUrlJsonV600]}",
|
||||
400,
|
||||
callContext
|
||||
) {
|
||||
json.extract[PostResetPasswordUrlJsonV600]
|
||||
}
|
||||
// Find the AuthUser
|
||||
authUserBox <- Future {
|
||||
code.model.dataAccess.AuthUser.find(
|
||||
net.liftweb.mapper.By(code.model.dataAccess.AuthUser.username, postedData.username)
|
||||
)
|
||||
}
|
||||
authUser <- NewStyle.function.tryons(
|
||||
s"$UnknownError User not found or validation failed",
|
||||
400,
|
||||
callContext
|
||||
) {
|
||||
authUserBox match {
|
||||
case Full(user) if user.validated.get && user.email.get == postedData.email =>
|
||||
// Verify user_id matches
|
||||
Users.users.vend.getUserByUserId(postedData.user_id) match {
|
||||
case Full(resourceUser) if resourceUser.name == postedData.username &&
|
||||
resourceUser.emailAddress == postedData.email =>
|
||||
user
|
||||
case _ => throw new Exception("User ID does not match username and email")
|
||||
}
|
||||
case _ => throw new Exception("User not found, not validated, or email mismatch")
|
||||
}
|
||||
}
|
||||
} yield {
|
||||
// Explicitly type the user to ensure proper method resolution
|
||||
val user: code.model.dataAccess.AuthUser = authUser
|
||||
|
||||
// Generate new reset token
|
||||
// Reset the unique ID token by generating a new random value (32 chars, no hyphens)
|
||||
user.uniqueId.set(java.util.UUID.randomUUID().toString.replace("-", ""))
|
||||
user.save
|
||||
|
||||
// Construct reset URL using portal_hostname
|
||||
// Get the unique ID value for the reset token URL
|
||||
val resetPasswordLink = APIUtil.getPropsValue("portal_external_url", Constant.HostName) +
|
||||
"/user_mgt/reset_password/" +
|
||||
java.net.URLEncoder.encode(user.uniqueId.get, "UTF-8")
|
||||
|
||||
// Send email using CommonsEmailWrapper (like createUser does)
|
||||
val textContent = Some(s"Please use the following link to reset your password: $resetPasswordLink")
|
||||
val htmlContent = Some(s"<p>Please use the following link to reset your password:</p><p><a href='$resetPasswordLink'>$resetPasswordLink</a></p>")
|
||||
val subjectContent = "Reset your password - " + user.username.get
|
||||
|
||||
val emailContent = code.api.util.CommonsEmailWrapper.EmailContent(
|
||||
from = code.model.dataAccess.AuthUser.emailFrom,
|
||||
to = List(user.email.get),
|
||||
bcc = code.model.dataAccess.AuthUser.bccEmail.toList,
|
||||
subject = subjectContent,
|
||||
textContent = textContent,
|
||||
htmlContent = htmlContent
|
||||
)
|
||||
|
||||
code.api.util.CommonsEmailWrapper.sendHtmlEmail(emailContent)
|
||||
|
||||
(
|
||||
ResetPasswordUrlJsonV600(resetPasswordLink),
|
||||
HttpCode.`201`(callContext)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -642,4 +642,8 @@ case class RoleWithEntitlementCountJsonV600(
|
||||
|
||||
case class RolesWithEntitlementCountsJsonV600(roles: List[RoleWithEntitlementCountJsonV600])
|
||||
|
||||
case class PostResetPasswordUrlJsonV600(username: String, email: String, user_id: String)
|
||||
|
||||
case class ResetPasswordUrlJsonV600(reset_password_url: String)
|
||||
|
||||
}
|
||||
|
||||
165
obp-api/src/test/scala/code/api/v6_0_0/PasswordResetTest.scala
Normal file
165
obp-api/src/test/scala/code/api/v6_0_0/PasswordResetTest.scala
Normal file
@ -0,0 +1,165 @@
|
||||
/**
|
||||
Open Bank Project - API
|
||||
Copyright (C) 2011-2019, TESOBE GmbH
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Email: contact@tesobe.com
|
||||
TESOBE GmbH
|
||||
Osloerstrasse 16/17
|
||||
Berlin 13359, Germany
|
||||
|
||||
This product includes software developed at
|
||||
TESOBE (http://www.tesobe.com/)
|
||||
*/
|
||||
package code.api.v6_0_0
|
||||
|
||||
import java.util.UUID
|
||||
import com.openbankproject.commons.model.ErrorMessage
|
||||
import code.api.util.APIUtil.OAuth._
|
||||
import code.api.util.ApiRole._
|
||||
import com.openbankproject.commons.util.ApiVersion
|
||||
import code.api.util.ErrorMessages._
|
||||
import code.api.v6_0_0.APIMethods600
|
||||
|
||||
import code.entitlement.Entitlement
|
||||
import code.model.dataAccess.{AuthUser, ResourceUser}
|
||||
import code.users.Users
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.model.User
|
||||
import net.liftweb.common.Box
|
||||
import net.liftweb.json.Serialization.write
|
||||
import net.liftweb.mapper.By
|
||||
import org.scalatest.Tag
|
||||
|
||||
/**
|
||||
* Test suite for Password Reset URL endpoint (POST /obp/v6.0.0/management/user/reset-password-url)
|
||||
*
|
||||
* Tests cover:
|
||||
* - Unauthorized access (no authentication)
|
||||
* - Missing role (authenticated but no CanCreateResetPasswordUrl)
|
||||
* - Successful password reset URL creation (with proper role)
|
||||
* - User validation requirements
|
||||
* - Email sending functionality
|
||||
*/
|
||||
class PasswordResetTest extends V600ServerSetup {
|
||||
|
||||
override def beforeEach() = {
|
||||
wipeTestData()
|
||||
super.beforeEach()
|
||||
AuthUser.bulkDelete_!!(By(AuthUser.username, postJson.username))
|
||||
ResourceUser.bulkDelete_!!(By(ResourceUser.providerId, postJson.username))
|
||||
}
|
||||
|
||||
/**
|
||||
* Test tags
|
||||
* Example: To run tests with tag "getPermissions":
|
||||
* mvn test -D tagsToInclude
|
||||
*
|
||||
* This is made possible by the scalatest maven plugin
|
||||
*/
|
||||
object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString)
|
||||
object ApiEndpoint1 extends Tag(nameOf(APIMethods600.Implementations6_0_0.resetPasswordUrl))
|
||||
lazy val postUserId = UUID.randomUUID.toString
|
||||
lazy val postJson = JSONFactory600.PostResetPasswordUrlJsonV600("marko", "marko@tesobe.com", postUserId)
|
||||
|
||||
feature("Reset password url v6.0.0 - Unauthorized access") {
|
||||
scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) {
|
||||
When("We make a request v6.0.0")
|
||||
val request600 = (v6_0_0_Request / "management" / "user" / "reset-password-url").POST
|
||||
val response600 = makePostRequest(request600, write(postJson))
|
||||
Then("We should get a 401")
|
||||
response600.code should equal(401)
|
||||
And("error should be " + UserNotLoggedIn)
|
||||
response600.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
|
||||
}
|
||||
}
|
||||
|
||||
feature("Reset password url v6.0.0 - Authorized access") {
|
||||
scenario("We will call the endpoint without the proper Role " + canCreateResetPasswordUrl, ApiEndpoint1, VersionOfApi) {
|
||||
When("We make a request v6.0.0 without a Role " + canCreateResetPasswordUrl)
|
||||
val request600 = (v6_0_0_Request / "management" / "user" / "reset-password-url").POST <@(user1)
|
||||
val response600 = makePostRequest(request600, write(postJson))
|
||||
Then("We should get a 400")
|
||||
response600.code should equal(400)
|
||||
And("error should be " + UserHasMissingRoles + CanCreateResetPasswordUrl)
|
||||
response600.body.extract[ErrorMessage].message should equal((UserHasMissingRoles + CanCreateResetPasswordUrl))
|
||||
}
|
||||
|
||||
scenario("We will call the endpoint with the proper Role " + canCreateResetPasswordUrl, ApiEndpoint1, VersionOfApi) {
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateResetPasswordUrl.toString)
|
||||
val authUser: AuthUser = AuthUser.create.email(postJson.email).username(postJson.username).validated(true).saveMe()
|
||||
val resourceUser: Box[User] = Users.users.vend.getUserByResourceUserId(authUser.user.get)
|
||||
When("We make a request v6.0.0")
|
||||
val request600 = (v6_0_0_Request / "management" / "user" / "reset-password-url").POST <@(user1)
|
||||
val response600 = makePostRequest(request600, write(postJson.copy(user_id = resourceUser.map(_.userId).getOrElse(""))))
|
||||
Then("We should get a 201")
|
||||
response600.code should equal(201)
|
||||
response600.body.extractOpt[JSONFactory600.ResetPasswordUrlJsonV600].isDefined should equal(true)
|
||||
And("The response should contain a valid reset URL")
|
||||
val resetUrl = (response600.body \ "reset_password_url").extract[String]
|
||||
resetUrl should include("/user_mgt/reset_password/")
|
||||
resetUrl.split("/user_mgt/reset_password/").last.length should be > 0
|
||||
}
|
||||
|
||||
scenario("We will call the endpoint with unvalidated user", ApiEndpoint1, VersionOfApi) {
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateResetPasswordUrl.toString)
|
||||
val testUsername = "unvalidated@tesobe.com"
|
||||
val testEmail = "unvalidated@tesobe.com"
|
||||
val authUser: AuthUser = AuthUser.create.email(testEmail).username(testUsername).validated(false).saveMe()
|
||||
val resourceUser: Box[User] = Users.users.vend.getUserByResourceUserId(authUser.user.get)
|
||||
When("We make a request v6.0.0 with unvalidated user")
|
||||
val request600 = (v6_0_0_Request / "management" / "user" / "reset-password-url").POST <@(user1)
|
||||
val testJson = JSONFactory600.PostResetPasswordUrlJsonV600(testUsername, testEmail, resourceUser.map(_.userId).getOrElse(""))
|
||||
val response600 = makePostRequest(request600, write(testJson))
|
||||
Then("We should get a 400")
|
||||
response600.code should equal(400)
|
||||
And("error should indicate user validation issue")
|
||||
response600.body.extract[ErrorMessage].message should include("not validated")
|
||||
// Clean up
|
||||
authUser.delete_!
|
||||
}
|
||||
|
||||
scenario("We will call the endpoint with mismatched email", ApiEndpoint1, VersionOfApi) {
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateResetPasswordUrl.toString)
|
||||
val testUsername = "mismatch@tesobe.com"
|
||||
val testEmail = "correct@tesobe.com"
|
||||
val wrongEmail = "wrong@tesobe.com"
|
||||
val authUser: AuthUser = AuthUser.create.email(testEmail).username(testUsername).validated(true).saveMe()
|
||||
val resourceUser: Box[User] = Users.users.vend.getUserByResourceUserId(authUser.user.get)
|
||||
When("We make a request v6.0.0 with mismatched email")
|
||||
val request600 = (v6_0_0_Request / "management" / "user" / "reset-password-url").POST <@(user1)
|
||||
val testJson = JSONFactory600.PostResetPasswordUrlJsonV600(testUsername, wrongEmail, resourceUser.map(_.userId).getOrElse(""))
|
||||
val response600 = makePostRequest(request600, write(testJson))
|
||||
Then("We should get a 400")
|
||||
response600.code should equal(400)
|
||||
And("error should indicate email mismatch")
|
||||
response600.body.extract[ErrorMessage].message should include("email mismatch")
|
||||
// Clean up
|
||||
authUser.delete_!
|
||||
}
|
||||
|
||||
scenario("We will call the endpoint with non-existent user", ApiEndpoint1, VersionOfApi) {
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateResetPasswordUrl.toString)
|
||||
When("We make a request v6.0.0 with non-existent user")
|
||||
val request600 = (v6_0_0_Request / "management" / "user" / "reset-password-url").POST <@(user1)
|
||||
val nonExistentJson = JSONFactory600.PostResetPasswordUrlJsonV600("nonexistent@tesobe.com", "nonexistent@tesobe.com", UUID.randomUUID.toString)
|
||||
val response600 = makePostRequest(request600, write(nonExistentJson))
|
||||
Then("We should get a 400")
|
||||
response600.code should equal(400)
|
||||
And("error should indicate user not found")
|
||||
response600.body.extract[ErrorMessage].message should include("User not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user