You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2021/11/05 09:40:07 UTC

[james-project] branch master updated: JAMES-3539 PushSubscription/set update types (#725)

This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git


The following commit(s) were added to refs/heads/master by this push:
     new a1e9530  JAMES-3539 PushSubscription/set update types (#725)
a1e9530 is described below

commit a1e9530fb5df0a1026db029d2c8706f1e7249deb
Author: Benoit TELLIER <bt...@linagora.com>
AuthorDate: Fri Nov 5 16:40:02 2021 +0700

    JAMES-3539 PushSubscription/set update types (#725)
---
 .../PushSubscriptionSetMethodContract.scala        | 241 +++++++++++++++++++++
 .../james/jmap/core/PushSubscriptionSet.scala      |  46 +++-
 .../method/PushSubscriptionUpdatePerformer.scala   |  25 ++-
 3 files changed, 298 insertions(+), 14 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala
index e8b345c..08c3531 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala
@@ -62,6 +62,8 @@ import java.time.format.DateTimeFormatter
 import java.util.Base64
 import java.util.UUID
 
+import scala.jdk.CollectionConverters._
+
 object PushSubscriptionSetMethodContract {
   val TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX")
 }
@@ -191,6 +193,245 @@ trait PushSubscriptionSetMethodContract {
   }
 
   @Test
+  def updateShouldModifyTypes(server: GuiceJamesServer): Unit = {
+    val probe = server.getProbe(classOf[PushSubscriptionProbe])
+    val pushSubscription = probe
+      .createPushSubscription(username = BOB,
+        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        deviceId = DeviceClientId("12c6d086"),
+        types = Seq(MailboxTypeName))
+
+    val request: String =
+      s"""{
+         |    "using": ["urn:ietf:params:jmap:core"],
+         |    "methodCalls": [
+         |      [
+         |        "PushSubscription/set",
+         |        {
+         |            "update": {
+         |                "${pushSubscription.id.serialise}": {
+         |                  "types": ["Mailbox", "Email"]
+         |                }
+         |              }
+         |        },
+         |        "c1"
+         |      ]
+         |    ]
+         |  }""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |        [
+           |            "PushSubscription/set",
+           |            {
+           |                "updated": {
+           |                    "${pushSubscription.id.serialise}": {}
+           |                }
+           |            },
+           |            "c1"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+
+    assertThat(probe.retrievePushSubscription(BOB, pushSubscription.id).types.asJava)
+      .containsExactlyInAnyOrder(MailboxTypeName, EmailTypeName)
+  }
+
+  @Test
+  def updateShouldRejectUnknownTypes(server: GuiceJamesServer): Unit = {
+    val probe = server.getProbe(classOf[PushSubscriptionProbe])
+    val pushSubscription = probe
+      .createPushSubscription(username = BOB,
+        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        deviceId = DeviceClientId("12c6d086"),
+        types = Seq(MailboxTypeName))
+
+    val request: String =
+      s"""{
+         |    "using": ["urn:ietf:params:jmap:core"],
+         |    "methodCalls": [
+         |      [
+         |        "PushSubscription/set",
+         |        {
+         |            "update": {
+         |                "${pushSubscription.id.serialise}": {
+         |                  "types": ["Mailbox", "Unknown"]
+         |                }
+         |              }
+         |        },
+         |        "c1"
+         |      ]
+         |    ]
+         |  }""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |        [
+           |            "PushSubscription/set",
+           |            {
+           |                "notUpdated":{
+           |                    "${pushSubscription.id.serialise}":{
+           |                        "type":"invalidArguments",
+           |                        "description":"Unknown typeName Unknown",
+           |                        "properties":["types"]
+           |                    }
+           |                }
+           |            },
+           |            "c1"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def updateShouldRejectBadTypes(server: GuiceJamesServer): Unit = {
+    val probe = server.getProbe(classOf[PushSubscriptionProbe])
+    val pushSubscription = probe
+      .createPushSubscription(username = BOB,
+        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        deviceId = DeviceClientId("12c6d086"),
+        types = Seq(MailboxTypeName))
+
+    val request: String =
+      s"""{
+         |    "using": ["urn:ietf:params:jmap:core"],
+         |    "methodCalls": [
+         |      [
+         |        "PushSubscription/set",
+         |        {
+         |            "update": {
+         |                "${pushSubscription.id.serialise}": {
+         |                  "types": 36
+         |                }
+         |              }
+         |        },
+         |        "c1"
+         |      ]
+         |    ]
+         |  }""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |        [
+           |            "PushSubscription/set",
+           |            {
+           |                "notUpdated":{
+           |                    "${pushSubscription.id.serialise}":{
+           |                        "type":"invalidArguments",
+           |                        "description":"Expecting an array of JSON strings as an argument",
+           |                        "properties":["types"]
+           |                    }
+           |                }
+           |            },
+           |            "c1"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def updateShouldRejectBadType(server: GuiceJamesServer): Unit = {
+    val probe = server.getProbe(classOf[PushSubscriptionProbe])
+    val pushSubscription = probe
+      .createPushSubscription(username = BOB,
+        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        deviceId = DeviceClientId("12c6d086"),
+        types = Seq(MailboxTypeName))
+
+    val request: String =
+      s"""{
+         |    "using": ["urn:ietf:params:jmap:core"],
+         |    "methodCalls": [
+         |      [
+         |        "PushSubscription/set",
+         |        {
+         |            "update": {
+         |                "${pushSubscription.id.serialise}": {
+         |                  "types": ["Email", 36]
+         |                }
+         |              }
+         |        },
+         |        "c1"
+         |      ]
+         |    ]
+         |  }""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |        [
+           |            "PushSubscription/set",
+           |            {
+           |                "notUpdated":{
+           |                    "${pushSubscription.id.serialise}":{
+           |                        "type":"invalidArguments",
+           |                        "description":"Expecting an array of JSON strings as an argument",
+           |                        "properties":["types"]
+           |                    }
+           |                }
+           |            },
+           |            "c1"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+  }
+
+  @Test
   def setMethodShouldNotCreatedWhenMissingTypesPropertyInCreationRequest(): Unit = {
     val request: String =
       """{
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala
index be4014a..c6145cd 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala
@@ -21,17 +21,19 @@ package org.apache.james.jmap.core
 
 import java.util.UUID
 
+import cats.implicits._
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.auto._
 import eu.timepit.refined.collection.NonEmpty
 import eu.timepit.refined.refineV
 import eu.timepit.refined.types.string.NonEmptyString
-import org.apache.james.jmap.api.model.{PushSubscriptionExpiredTime, PushSubscriptionId, VerificationCode}
+import org.apache.james.jmap.api.change.TypeStateFactory
+import org.apache.james.jmap.api.model.{PushSubscriptionExpiredTime, PushSubscriptionId, TypeName, VerificationCode}
 import org.apache.james.jmap.core.Id.Id
 import org.apache.james.jmap.core.SetError.SetErrorDescription
 import org.apache.james.jmap.mail.{InvalidPropertyException, InvalidUpdateException, PatchUpdateValidationException, UnsupportedPropertyUpdatedException}
 import org.apache.james.jmap.method.WithoutAccountId
-import play.api.libs.json.{JsObject, JsString, JsValue}
+import play.api.libs.json.{JsArray, JsObject, JsString, JsValue}
 
 import scala.util.Try
 
@@ -71,14 +73,16 @@ object PushSubscriptionPatchObject {
 }
 
 case class PushSubscriptionPatchObject(value: Map[String, JsValue]) {
-  val updates: Iterable[Either[PatchUpdateValidationException, Update]] = value.map({
+  def computeUpdates(typeStateFactory: TypeStateFactory): Iterable[Either[PatchUpdateValidationException, Update]] = value.map({
     case (property, newValue) => property match {
       case "verificationCode" => VerificationCodeUpdate.parse(newValue)
+      case "types" => TypesUpdate.parse(newValue, typeStateFactory)
       case property => PushSubscriptionPatchObject.notFound(property)
     }
   })
 
-  def validate(): Either[PatchUpdateValidationException, ValidatedPushSubscriptionPatchObject] = {
+  def validate(typeStateFactory: TypeStateFactory): Either[PatchUpdateValidationException, ValidatedPushSubscriptionPatchObject] = {
+    val updates = computeUpdates(typeStateFactory)
     val maybeParseException: Option[PatchUpdateValidationException] = updates
       .flatMap(x => x match {
         case Left(e) => Some(e)
@@ -91,10 +95,17 @@ case class PushSubscriptionPatchObject(value: Map[String, JsValue]) {
         case _ => None
       }).headOption
 
+    val typesUpdate: Option[TypesUpdate] = updates
+      .flatMap(x => x match {
+        case Right(TypesUpdate(newTypes)) => Some(TypesUpdate(newTypes))
+        case _ => None
+      }).headOption
+
     maybeParseException
       .map(e => Left(e))
       .getOrElse(scala.Right(ValidatedPushSubscriptionPatchObject(
-        verificationCodeUpdate = verificationCodeUpdate.map(_.newVerificationCode))))
+        verificationCodeUpdate = verificationCodeUpdate.map(_.newVerificationCode),
+        typesUpdate = typesUpdate.map(_.types))))
   }
 }
 
@@ -105,18 +116,37 @@ object VerificationCodeUpdate {
   }
 }
 
+object TypesUpdate {
+  def parse(jsValue: JsValue, typeStateFactory: TypeStateFactory): Either[PatchUpdateValidationException, Update] = jsValue match {
+    case JsArray(aArray) => aArray.toList
+      .map(js => parseType(js, typeStateFactory))
+      .sequence
+      .map(_.toSet)
+      .map(TypesUpdate(_))
+    case _ => Left(InvalidUpdateException("types", "Expecting an array of JSON strings as an argument"))
+  }
+  def parseType(jsValue: JsValue, typeStateFactory: TypeStateFactory): Either[PatchUpdateValidationException, TypeName] = jsValue match {
+    case JsString(aString) => typeStateFactory.parse(aString).left.map(e => InvalidUpdateException("types", e.getMessage))
+    case _ => Left(InvalidUpdateException("types", "Expecting an array of JSON strings as an argument"))
+  }
+}
+
 sealed trait Update
 case class VerificationCodeUpdate(newVerificationCode: VerificationCode) extends Update
+case class TypesUpdate(types: Set[TypeName]) extends Update
 
 object ValidatedPushSubscriptionPatchObject {
   val verificationCodeProperty: NonEmptyString = "verificationCode"
+  val typesProperty: NonEmptyString = "types"
 }
 
-case class ValidatedPushSubscriptionPatchObject(verificationCodeUpdate: Option[VerificationCode]) {
-  val shouldUpdate: Boolean = verificationCodeUpdate.isDefined
+case class ValidatedPushSubscriptionPatchObject(verificationCodeUpdate: Option[VerificationCode],
+                                                typesUpdate: Option[Set[TypeName]]) {
+  val shouldUpdate: Boolean = verificationCodeUpdate.isDefined || typesUpdate.isDefined
 
   val updatedProperties: Properties = Properties(Set(
-    verificationCodeUpdate.map(_ => ValidatedPushSubscriptionPatchObject.verificationCodeProperty))
+    verificationCodeUpdate.map(_ => ValidatedPushSubscriptionPatchObject.verificationCodeProperty),
+    typesUpdate.map(_ => ValidatedPushSubscriptionPatchObject.typesProperty))
     .flatMap(_.toList))
 }
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionUpdatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionUpdatePerformer.scala
index 7fade4e..f3a42e5 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionUpdatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionUpdatePerformer.scala
@@ -22,15 +22,18 @@ package org.apache.james.jmap.method
 import com.google.common.collect.ImmutableSet
 import eu.timepit.refined.auto._
 import javax.inject.Inject
-import org.apache.james.jmap.api.model.{PushSubscription, PushSubscriptionId, PushSubscriptionNotFoundException, VerificationCode}
+import org.apache.james.jmap.api.change.TypeStateFactory
+import org.apache.james.jmap.api.model.{PushSubscription, PushSubscriptionId, PushSubscriptionNotFoundException, TypeName, VerificationCode}
 import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository
 import org.apache.james.jmap.core.SetError.SetErrorDescription
 import org.apache.james.jmap.core.{Properties, PushSubscriptionPatchObject, PushSubscriptionSetRequest, PushSubscriptionUpdateResponse, SetError, UnparsedPushSubscriptionId, ValidatedPushSubscriptionPatchObject}
-import org.apache.james.jmap.mail.{InvalidPropertyException, UnsupportedPropertyUpdatedException}
+import org.apache.james.jmap.mail.{InvalidPropertyException, InvalidUpdateException, UnsupportedPropertyUpdatedException}
 import org.apache.james.jmap.method.PushSubscriptionSetUpdatePerformer.{PushSubscriptionUpdateFailure, PushSubscriptionUpdateResult, PushSubscriptionUpdateResults, PushSubscriptionUpdateSuccess, WrongVerificationCodeException}
 import org.apache.james.mailbox.MailboxSession
 import reactor.core.scala.publisher.{SFlux, SMono}
 
+import scala.jdk.CollectionConverters._
+
 object PushSubscriptionSetUpdatePerformer {
   case class WrongVerificationCodeException() extends RuntimeException()
   sealed trait PushSubscriptionUpdateResult
@@ -40,6 +43,7 @@ object PushSubscriptionSetUpdatePerformer {
       case _: WrongVerificationCodeException => SetError.invalidProperties(SetErrorDescription("Wrong verification code"), Some(Properties("verificationCode")))
       case e: UnsupportedPropertyUpdatedException => SetError.invalidArguments(SetErrorDescription(s"${e.property} property do not exist thus cannot be updated"), Some(Properties(e.property)))
       case e: InvalidPropertyException => SetError.invalidPatch(SetErrorDescription(s"${e.cause}"))
+      case e: InvalidUpdateException => SetError.invalidArguments(SetErrorDescription(s"${e.cause}"), Some(Properties(e.property)))
       case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(e.getMessage), None)
       case e: PushSubscriptionNotFoundException => SetError.notFound(SetErrorDescription(e.getMessage))
       case _ => SetError.serverFail(SetErrorDescription(exception.getMessage))
@@ -58,7 +62,8 @@ object PushSubscriptionSetUpdatePerformer {
   }
 }
 
-class PushSubscriptionUpdatePerformer @Inject()(pushSubscriptionRepository: PushSubscriptionRepository) {
+class PushSubscriptionUpdatePerformer @Inject()(pushSubscriptionRepository: PushSubscriptionRepository,
+                                               typeStateFactory: TypeStateFactory) {
   def update(pushSubscriptionSetRequest: PushSubscriptionSetRequest,
              mailboxSession: MailboxSession): SMono[PushSubscriptionUpdateResults] =
     SFlux.fromIterable(pushSubscriptionSetRequest.update.getOrElse(Map()))
@@ -66,7 +71,7 @@ class PushSubscriptionUpdatePerformer @Inject()(pushSubscriptionRepository: Push
         case (unparsedId: UnparsedPushSubscriptionId, patch: PushSubscriptionPatchObject) =>
           val either = for {
             id <- unparsedId.parse
-            validatedPatch <- patch.validate()
+            validatedPatch <- patch.validate(typeStateFactory)
           } yield {
             updatePushSubscription(id, validatedPatch, mailboxSession)
           }
@@ -83,10 +88,14 @@ class PushSubscriptionUpdatePerformer @Inject()(pushSubscriptionRepository: Push
     if (validatedPatch.shouldUpdate) {
       SMono(pushSubscriptionRepository.get(mailboxSession.getUser, ImmutableSet.of(id)))
         .switchIfEmpty(SMono.error(PushSubscriptionNotFoundException(id)))
-        .flatMap(pushSubscription => SFlux(
+        .flatMap(pushSubscription => SFlux.concat(
           validatedPatch.verificationCodeUpdate
             .map(verificationCode => updateVerificationCode(pushSubscription, verificationCode, mailboxSession))
-            .getOrElse(SMono.empty)).last())
+            .getOrElse(SMono.empty),
+          validatedPatch.typesUpdate
+            .map(types => updateTypes(pushSubscription, types, mailboxSession))
+            .getOrElse(SMono.empty))
+          .last())
     } else {
       SMono.empty
     }
@@ -98,4 +107,8 @@ class PushSubscriptionUpdatePerformer @Inject()(pushSubscriptionRepository: Push
     } else {
       SMono.error[PushSubscriptionUpdateResult](WrongVerificationCodeException())
     }
+
+  private def updateTypes(pushSubscription: PushSubscription, types: Set[TypeName], mailboxSession: MailboxSession): SMono[PushSubscriptionUpdateResult] =
+    SMono(pushSubscriptionRepository.updateTypes(mailboxSession.getUser, pushSubscription.id, types.asJava))
+      .`then`(SMono.just(PushSubscriptionUpdateSuccess(pushSubscription.id)))
 }

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org