Skip to content

Commit b5f45ca

Browse files
feature: GET /users/user-id/USER_IDgetUserByUserId migrated to v7
1 parent f5489b3 commit b5f45ca

2 files changed

Lines changed: 151 additions & 1 deletion

File tree

obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,13 @@ import com.openbankproject.commons.model.{AccountId, BankId, BankIdAccountId, Co
2828
import com.github.dwickern.macros.NameOf.nameOf
2929
import com.openbankproject.commons.ExecutionContext.Implicits.global
3030
import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus, ScannedApiVersion}
31+
import code.loginattempts.LoginAttempt
32+
import code.metrics.MappedMetric
33+
import code.users.UserAgreementProvider
3134
import net.liftweb.common.Full
3235
import net.liftweb.json.JsonAST.prettyRender
3336
import net.liftweb.json.{Extraction, Formats}
37+
import net.liftweb.mapper.{By, Descending, MaxRows, OrderBy}
3438
import org.http4s._
3539
import org.http4s.dsl.io._
3640

@@ -666,6 +670,68 @@ object Http4s700 {
666670
http4sPartialFunction = Some(getUsers)
667671
)
668672

673+
// Route: GET /obp/v7.0.0/users/user-id/USER_ID
674+
val getUserByUserId: HttpRoutes[IO] = HttpRoutes.of[IO] {
675+
case req @ GET -> `prefixPath` / "users" / "user-id" / userId =>
676+
EndpointHelpers.withUser(req) { (_, cc) =>
677+
for {
678+
user <- UserVend.users.vend.getUserByUserIdFuture(userId).map(
679+
x => unboxFullOrFail(x, cc.callContext, s"$UserNotFoundByUserId Current USER_ID($userId)", 404)
680+
)
681+
entitlements <- NewStyle.function.getEntitlementsByUserId(user.userId, cc.callContext)
682+
agreements <- Future {
683+
val acceptMarketingInfo = UserAgreementProvider.userAgreementProvider.vend.getLastUserAgreement(user.userId, "accept_marketing_info")
684+
val termsAndConditions = UserAgreementProvider.userAgreementProvider.vend.getLastUserAgreement(user.userId, "terms_and_conditions")
685+
val privacyConditions = UserAgreementProvider.userAgreementProvider.vend.getLastUserAgreement(user.userId, "privacy_conditions")
686+
val agreementList = acceptMarketingInfo.toList ::: termsAndConditions.toList ::: privacyConditions.toList
687+
if (agreementList.isEmpty) None else Some(agreementList)
688+
}
689+
isLocked = LoginAttempt.userIsLocked(user.provider, user.name)
690+
authUser = code.model.dataAccess.AuthUser.find(
691+
By(code.model.dataAccess.AuthUser.user, user.userPrimaryKey.value)
692+
)
693+
userMetrics <- Future {
694+
MappedMetric.findAll(
695+
By(MappedMetric.userId, userId),
696+
OrderBy(MappedMetric.date, Descending),
697+
MaxRows(5)
698+
)
699+
}
700+
lastActivityDate = userMetrics.headOption.map(_.getDate())
701+
recentOperationIds = userMetrics.map(_.getImplementedByPartialFunction()).distinct.take(5)
702+
} yield JSONFactory600.createUserInfoJsonV600(
703+
user,
704+
authUser.map(_.firstName.get).getOrElse(""),
705+
authUser.map(_.lastName.get).getOrElse(""),
706+
entitlements,
707+
agreements,
708+
isLocked,
709+
lastActivityDate,
710+
recentOperationIds
711+
)
712+
}
713+
}
714+
715+
resourceDocs += ResourceDoc(
716+
null,
717+
implementedInApiVersion,
718+
nameOf(getUserByUserId),
719+
"GET",
720+
"/users/user-id/USER_ID",
721+
"Get User by USER_ID",
722+
"""Get user by USER_ID.
723+
|
724+
|Authentication is required.
725+
|
726+
|CanGetAnyUser entitlement is required.""",
727+
EmptyBody,
728+
userInfoJsonV600,
729+
List($AuthenticatedUserIsRequired, UserHasMissingRoles, UserNotFoundByUserId, UnknownError),
730+
apiTagUser :: Nil,
731+
Some(List(canGetAnyUser)),
732+
http4sPartialFunction = Some(getUserByUserId)
733+
)
734+
669735
// Route: GET /obp/v7.0.0/banks/BANK_ID/customers
670736
val getCustomersAtOneBank: HttpRoutes[IO] = HttpRoutes.of[IO] {
671737
case req @ GET -> `prefixPath` / "banks" / bankIdStr / "customers" =>

obp-api/src/test/scala/code/api/v7_0_0/Http4s700RoutesTest.scala

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import code.api.Constant.SYSTEM_OWNER_VIEW_ID
55
import code.api.ResponseHeader
66
import code.api.util.APIUtil
77
import code.api.util.ApiRole.{canCreateEntitlementAtAnyBank, canDeleteEntitlementAtAnyBank, canGetAnyUser, canGetCardsForBank, canGetCustomersAtOneBank, canReadResourceDoc}
8-
import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, BankNotFound, UserHasMissingRoles}
8+
import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, BankNotFound, UserHasMissingRoles, UserNotFoundByUserId}
99
import code.customer.CustomerX
1010
import code.entitlement.Entitlement
1111
import code.metadata.counterparties.Counterparties
@@ -1419,6 +1419,90 @@ class Http4s700RoutesTest extends ServerSetupWithTestData {
14191419
}
14201420
}
14211421

1422+
// ─── getUserByUserId ──────────────────────────────────────────────────────────
1423+
1424+
feature("Http4s700 getUserByUserId endpoint") {
1425+
1426+
scenario("Reject unauthenticated access to /users/user-id/USER_ID", Http4s700RoutesTag) {
1427+
Given("GET /obp/v7.0.0/users/user-id/USER_ID with no auth headers")
1428+
val (statusCode, json, _) = makeHttpRequest(s"/obp/v7.0.0/users/user-id/${resourceUser1.userId}")
1429+
1430+
Then("Response is 401 with AuthenticatedUserIsRequired message")
1431+
statusCode shouldBe 401
1432+
json match {
1433+
case JObject(fields) =>
1434+
toFieldMap(fields).get("message") match {
1435+
case Some(JString(msg)) => msg should include(AuthenticatedUserIsRequired)
1436+
case _ => fail("Expected message field")
1437+
}
1438+
case _ => fail("Expected JSON object")
1439+
}
1440+
}
1441+
1442+
scenario("Return 403 when authenticated but missing canGetAnyUser role", Http4s700RoutesTag) {
1443+
Given("GET /obp/v7.0.0/users/user-id/USER_ID with DirectLogin header but no role")
1444+
val headers = Map("DirectLogin" -> s"token=${token1.value}")
1445+
val (statusCode, json, _) = makeHttpRequest(s"/obp/v7.0.0/users/user-id/${resourceUser1.userId}", headers)
1446+
1447+
Then("Response is 403 with UserHasMissingRoles")
1448+
statusCode shouldBe 403
1449+
json match {
1450+
case JObject(fields) =>
1451+
toFieldMap(fields).get("message") match {
1452+
case Some(JString(msg)) =>
1453+
msg should include(UserHasMissingRoles)
1454+
msg should include(canGetAnyUser.toString)
1455+
case _ => fail("Expected message field")
1456+
}
1457+
case _ => fail("Expected JSON object")
1458+
}
1459+
}
1460+
1461+
scenario("Return 200 with user fields when authenticated with canGetAnyUser role", Http4s700RoutesTag) {
1462+
Given("canGetAnyUser role granted to resourceUser1")
1463+
addEntitlement("", resourceUser1.userId, canGetAnyUser.toString)
1464+
1465+
When(s"GET /obp/v7.0.0/users/user-id/${resourceUser1.userId} with DirectLogin header")
1466+
val headers = Map("DirectLogin" -> s"token=${token1.value}")
1467+
val (statusCode, json, _) = makeHttpRequest(s"/obp/v7.0.0/users/user-id/${resourceUser1.userId}", headers)
1468+
1469+
Then("Response is 200 with user_id, username, email fields")
1470+
statusCode shouldBe 200
1471+
json match {
1472+
case JObject(fields) =>
1473+
val m = toFieldMap(fields)
1474+
m.get("user_id") match {
1475+
case Some(JString(id)) => id shouldBe resourceUser1.userId
1476+
case _ => fail("Expected user_id field")
1477+
}
1478+
m.keys should contain("username")
1479+
m.keys should contain("email")
1480+
m.keys should contain("entitlements")
1481+
case _ => fail("Expected JSON object for getUserByUserId")
1482+
}
1483+
}
1484+
1485+
scenario("Return 404 when USER_ID does not exist", Http4s700RoutesTag) {
1486+
Given("canGetAnyUser role granted to resourceUser1")
1487+
addEntitlement("", resourceUser1.userId, canGetAnyUser.toString)
1488+
1489+
When("GET /obp/v7.0.0/users/user-id/non-existing-user-id with DirectLogin header")
1490+
val headers = Map("DirectLogin" -> s"token=${token1.value}")
1491+
val (statusCode, json, _) = makeHttpRequest("/obp/v7.0.0/users/user-id/non-existing-user-id-xyz", headers)
1492+
1493+
Then("Response is 404 with UserNotFoundByUserId message")
1494+
statusCode shouldBe 404
1495+
json match {
1496+
case JObject(fields) =>
1497+
toFieldMap(fields).get("message") match {
1498+
case Some(JString(msg)) => msg should include(UserNotFoundByUserId)
1499+
case _ => fail("Expected message field")
1500+
}
1501+
case _ => fail("Expected JSON object")
1502+
}
1503+
}
1504+
}
1505+
14221506
// ─── getCustomersAtOneBank ────────────────────────────────────────────────────
14231507

14241508
feature("Http4s700 getCustomersAtOneBank endpoint") {

0 commit comments

Comments
 (0)