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