Skip to content

Commit

Permalink
PIN-4370 BKE - Tenant process Added route for addition certified attr…
Browse files Browse the repository at this point in the history
…ibute
  • Loading branch information
nttdata-rtorsoli committed Jan 15, 2024
1 parent c18763e commit 5a62d6b
Show file tree
Hide file tree
Showing 12 changed files with 284 additions and 1 deletion.
48 changes: 48 additions & 0 deletions src/main/resources/interface-specification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,46 @@ paths:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
/tenants/{tenantId}/attributes/certified:
parameters:
- $ref: '#/components/parameters/CorrelationIdHeader'
- name: tenantId
in: path
description: Tenant id
required: true
schema:
type: string
format: uuid
post:
tags:
- tenant
operationId: addCertifiedAttribute
description: Add a certified attribute to a Tenant by the requester Tenant
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CertifiedTenantAttributeSeed'
required: true
responses:
'200':
description: Updated Tenant
content:
application/json:
schema:
$ref: '#/components/schemas/Tenant'
'403':
description: Forbidden
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
'404':
description: Tenant Not Found
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
/tenants/attributes/declared:
parameters:
- $ref: '#/components/parameters/CorrelationIdHeader'
Expand Down Expand Up @@ -988,6 +1028,14 @@ components:
format: date-time
required:
- id
CertifiedTenantAttributeSeed:
type: object
properties:
id:
type: string
format: uuid
required:
- id
UpdateVerifiedTenantAttributeSeed:
type: object
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import it.pagopa.interop.tenantmanagement.client.model.MailKind.{DIGITAL_ADDRESS
import it.pagopa.interop.tenantmanagement.client.model.{
Certifier => DependencyCertifier,
DeclaredTenantAttribute => DependencyDeclaredTenantAttribute,
CertifiedTenantAttribute => DependencyCertifiedTenantAttribute,
ExternalId => DependencyExternalId,
MailKind => DependencyMailKind,
MailSeed => DependencyMailSeed,
Expand Down Expand Up @@ -71,12 +72,21 @@ object ApiAdapters {
)
}

implicit class TenantUnitTypeWrapper(private val u: TenantUnitType) extends AnyVal {
implicit class TenantUnitTypeWrapper(private val u: TenantUnitType) extends AnyVal {
def toDependency: DependencyTenantUnitType = u match {
case TenantUnitType.AOO => DependencyTenantUnitType.AOO
case TenantUnitType.UO => DependencyTenantUnitType.UO
}
}
implicit class CertifiedTenantAttributeSeedWrapper(private val seed: CertifiedTenantAttributeSeed) extends AnyVal {
def toCreateDependency(now: OffsetDateTime): DependencyTenantAttribute =
DependencyTenantAttribute(
declared = None,
verified = None,
certified =
DependencyCertifiedTenantAttribute(id = seed.id, assignmentTimestamp = now, revocationTimestamp = None).some
)
}

implicit class VerifiedTenantAttributeSeedWrapper(private val seed: VerifiedTenantAttributeSeed) extends AnyVal {
def toCreateDependency(now: OffsetDateTime, requesterId: UUID): DependencyTenantAttribute =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ object TenantApiMarshallerImpl extends TenantApiMarshaller with SprayJsonSupport

override implicit def toEntityMarshallerResourceId: ToEntityMarshaller[ResourceId] =
sprayJsonMarshaller[ResourceId]

override implicit def fromEntityUnmarshallerCertifiedTenantAttributeSeed
: FromEntityUnmarshaller[CertifiedTenantAttributeSeed] = sprayJsonUnmarshaller[CertifiedTenantAttributeSeed]
}
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,45 @@ final case class TenantApiServiceImpl(
}
}

def addCertifiedAttribute(tenantId: String, seed: CertifiedTenantAttributeSeed)(implicit
contexts: Seq[(String, String)],
toEntityMarshallerProblem: ToEntityMarshaller[Problem],
toEntityMarshallerTenant: ToEntityMarshaller[Tenant]
): Route = authorize(ADMIN_ROLE) {
val operationLabel = s"Add certified attribute ${seed.id} to tenant $tenantId"
logger.info(operationLabel)

val now: OffsetDateTime = dateTimeSupplier.get()

val result: Future[Tenant] = for {
requesterTenantUuid <- getOrganizationIdFutureUUID(contexts)
targetTenantUuid <- tenantId.toFutureUUID
requesterTenant <- tenantManagementService.getTenantById(requesterTenantUuid).map(_.toManagement)
_ <- requesterTenant.features
.collectFirstSome(_.certifier.map(_.certifierId))
.toFuture(TenantIsNotACertifier(requesterTenantUuid))
attribute <- attributeRegistryManagementService
.getAttributeById(seed.id)
_ <- attribute.kind match {
case Certified => Future.unit
case _ => Future.failed(RegistryAttributeIdNotFound(attribute.id))
}
targetTenant <- tenantManagementService.getTenantById(targetTenantUuid).map(_.toManagement)
attribute = targetTenant.attributes.flatMap(_.certified).find(_.id == seed.id)
updatedTenant <- attribute.fold(
tenantManagementService.addTenantAttribute(targetTenantUuid, seed.toCreateDependency(now))
)(_ => Future.failed(CertifiedAttributeAlreadyExists(targetTenantUuid, seed.id)))
_ <- agreementProcessService.computeAgreementsByAttribute(
seed.id,
CompactTenant(updatedTenant.id, updatedTenant.attributes.map(_.toAgreementApi))
)
} yield updatedTenant.toApi

onComplete(result) {
addCertifiedAttributeResponse[Tenant](operationLabel)(addCertifiedAttribute200)
}
}

override def verifyVerifiedAttribute(tenantId: String, seed: VerifiedTenantAttributeSeed)(implicit
contexts: Seq[(String, String)],
toEntityMarshallerProblem: ToEntityMarshaller[Problem],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ package object impl extends SprayJsonSupport with DefaultJsonProtocol {
implicit def m2mAttributeSeedFormat: RootJsonFormat[M2MAttributeSeed] = jsonFormat1(M2MAttributeSeed)
implicit def m2mTenantSeedFormat: RootJsonFormat[M2MTenantSeed] = jsonFormat3(M2MTenantSeed)
implicit def selfcareTenantSeedFormat: RootJsonFormat[SelfcareTenantSeed] = jsonFormat6(SelfcareTenantSeed)
implicit def certifiedTenantAttributeSeedFormat: RootJsonFormat[CertifiedTenantAttributeSeed] = jsonFormat1(
CertifiedTenantAttributeSeed
)

implicit def certifierFormat: RootJsonFormat[Certifier] = jsonFormat1(Certifier)
implicit def tenantFeatureFormat: RootJsonFormat[TenantFeature] = jsonFormat1(TenantFeature)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ object ResponseHandlers extends AkkaResponses {
case Failure(ex) => internalServerError(ex, logMessage)
}

def addCertifiedAttributeResponse[T](logMessage: String)(
success: T => Route
)(result: Try[T])(implicit contexts: Seq[(String, String)], logger: LoggerTakingImplicit[ContextFieldsToLog]): Route =
result match {
case Success(s) => success(s)
case Failure(ex: TenantIsNotACertifier) => forbidden(ex, logMessage)
case Failure(ex: TenantByIdNotFound) => notFound(ex, logMessage)
case Failure(ex: CertifiedAttributeAlreadyExists) => conflict(ex, logMessage)
case Failure(ex) => internalServerError(ex, logMessage)
}

def verifyVerifiedAttributeResponse[T](logMessage: String)(
success: T => Route
)(result: Try[T])(implicit contexts: Seq[(String, String)], logger: LoggerTakingImplicit[ContextFieldsToLog]): Route =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,6 @@ object TenantProcessErrors {
"0022",
s"Certified Attribute ($attributeOrigin, $attributeCode) already in tenant $tenantId"
)
final case class CertifiedAttributeAlreadyExists(tenantId: UUID, attributeId: UUID)
extends ComponentError("0023", s"Certified Attribute $attributeId already exists in tenant $tenantId")
}
7 changes: 7 additions & 0 deletions src/test/resources/authz.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@
"admin",
"api"
]
},
{
"route": "addCertifiedAttribute",
"verb": "POST",
"roles": [
"admin"
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,13 @@ class TenantApiServiceAuthzSpec extends ClusteredMUnitRouteTest with SpecData {
}
)
}

test("Tenant api should accept authorized roles for addCertifiedAttribute") {
validateAuthorization(
endpoints("addCertifiedAttribute"),
{ implicit c: Seq[(String, String)] =>
tenantService.addCertifiedAttribute(UUID.randomUUID().toString, fakeCertifiedTenantAttributeSeed)
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package it.pagopa.interop.tenantprocess.provider

import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.testkit.ScalatestRouteTest
import it.pagopa.interop.agreementprocess.client.model.CompactTenant
import it.pagopa.interop.tenantmanagement.client.{model => Dependency}
import it.pagopa.interop.tenantmanagement.model.tenant.{PersistentTenantFeature}
import it.pagopa.interop.attributeregistrymanagement.model.persistence.attribute.Declared
import it.pagopa.interop.tenantprocess.model.CertifiedTenantAttributeSeed
import it.pagopa.interop.tenantprocess.api.impl.TenantApiMarshallerImpl._
import it.pagopa.interop.tenantprocess.utils.SpecHelper
import org.scalatest.wordspec.AnyWordSpecLike

import java.util.UUID

class CertifiedAttributeSpec extends AnyWordSpecLike with SpecHelper with ScalatestRouteTest {

"Certified attribute addition" should {
"succeed" in {
implicit val context: Seq[(String, String)] = adminContext

val tenantUuid = UUID.randomUUID()
val attributeId = UUID.randomUUID()
val seed = CertifiedTenantAttributeSeed(attributeId)
val managementSeed = Dependency.TenantAttribute(
declared = None,
certified = Some(
Dependency.CertifiedTenantAttribute(seed.id, assignmentTimestamp = timestamp, revocationTimestamp = None)
),
verified = None
)

val requester = persistentTenant.copy(
id = organizationId,
features = List(PersistentTenantFeature.PersistentCertifier("certifier"))
)

val tenant = persistentTenant.copy(
id = tenantUuid,
attributes = List(persistentCertifiedAttribute, persistentDeclaredAttribute, persistentVerifiedAttribute)
)

mockDateTimeGet()
mockGetTenantById(organizationId, requester)
mockGetTenantById(tenantUuid, tenant)
mockGetAttributeById(seed.id, persistentAttribute.copy(id = seed.id))
mockAddTenantAttribute(tenantUuid, managementSeed)
mockComputeAgreementState(attributeId, CompactTenant(tenantUuid, Nil))

Post() ~> tenantService.addCertifiedAttribute(tenantUuid.toString, seed) ~> check {
assert(status == StatusCodes.OK)
}
}
}
"fail if requester is not a certifier" in {
implicit val context: Seq[(String, String)] = adminContext

val tenantUuid = UUID.randomUUID()
val attributeId = UUID.randomUUID()
val seed = CertifiedTenantAttributeSeed(attributeId)

val requester = persistentTenant.copy(id = organizationId)

mockDateTimeGet()
mockGetTenantById(organizationId, requester)

Post() ~> tenantService.addCertifiedAttribute(tenantUuid.toString, seed) ~> check {
assert(status == StatusCodes.Forbidden)
}
}

"fail if attribute does not exists" in {
implicit val context: Seq[(String, String)] = adminContext

val tenantUuid = UUID.randomUUID()
val attributeId = UUID.randomUUID()
val seed = CertifiedTenantAttributeSeed(attributeId)

val requester = persistentTenant.copy(
id = organizationId,
features = List(PersistentTenantFeature.PersistentCertifier("certifier"))
)

mockDateTimeGet()
mockGetTenantById(organizationId, requester)
mockGetAttributeByIdNotFound(seed.id)

Post() ~> tenantService.addCertifiedAttribute(tenantUuid.toString, seed) ~> check {
assert(status == StatusCodes.InternalServerError)
}
}

"fail if attribute exists but is not certified" in {
implicit val context: Seq[(String, String)] = adminContext

val tenantUuid = UUID.randomUUID()
val attributeId = UUID.randomUUID()
val seed = CertifiedTenantAttributeSeed(attributeId)

val requester = persistentTenant.copy(
id = organizationId,
features = List(PersistentTenantFeature.PersistentCertifier("certifier"))
)

mockDateTimeGet()
mockGetTenantById(organizationId, requester)
mockGetAttributeById(seed.id, persistentAttribute.copy(id = seed.id, kind = Declared))

Post() ~> tenantService.addCertifiedAttribute(tenantUuid.toString, seed) ~> check {
assert(status == StatusCodes.InternalServerError)
}
}

"fail if certified tenant attribute already exists" in {
implicit val context: Seq[(String, String)] = adminContext

val tenantUuid = UUID.randomUUID()
val attributeId = UUID.randomUUID()
val seed = CertifiedTenantAttributeSeed(attributeId)

val requester = persistentTenant.copy(
id = organizationId,
features = List(PersistentTenantFeature.PersistentCertifier("certifier"))
)

val tenant = persistentTenant.copy(
id = tenantUuid,
attributes =
List(persistentCertifiedAttribute.copy(id = seed.id), persistentDeclaredAttribute, persistentVerifiedAttribute)
)

mockDateTimeGet()
mockGetTenantById(organizationId, requester)
mockGetTenantById(tenantUuid, tenant)
mockGetAttributeById(seed.id, persistentAttribute.copy(id = seed.id))

Post() ~> tenantService.addCertifiedAttribute(tenantUuid.toString, seed) ~> check {
assert(status == StatusCodes.Conflict)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ trait SpecData {

val fakeMailSeed = MailSeed(MailKind.CONTACT_EMAIL, address = "fakeAddress", description = None)

val fakeCertifiedTenantAttributeSeed = CertifiedTenantAttributeSeed(UUID.randomUUID())

val persistentTenantVerifier: PersistentTenantVerifier = PersistentTenantVerifier(
id = UUID.randomUUID(),
verificationDate = timestamp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import it.pagopa.interop.tenantmanagement.model.tenant.{
import it.pagopa.interop.tenantprocess.error.TenantProcessErrors.{
RegistryAttributeNotFound,
TenantAttributeNotFound,
RegistryAttributeIdNotFound,
TenantNotFound
}
import it.pagopa.interop.tenantmanagement.client.model._
Expand Down Expand Up @@ -216,6 +217,13 @@ trait SpecHelper extends MockFactory with SpecData {
.once()
.returns(Future.successful(result.copy(id = id)))

def mockGetAttributeByIdNotFound(id: UUID) =
(mockAttributeRegistryManagement
.getAttributeById(_: UUID)(_: ExecutionContext, _: ReadModelService))
.expects(id, *, *)
.once()
.returns(Future.failed(RegistryAttributeIdNotFound(id)))

def mockComputeAgreementState(attributeId: UUID, consumer: CompactTenant)(implicit contexts: Seq[(String, String)]) =
(mockAgreementProcess
.computeAgreementsByAttribute(_: UUID, _: CompactTenant)(_: Seq[(String, String)]))
Expand Down

0 comments on commit 5a62d6b

Please sign in to comment.