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

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

commit 22769e0aa47ad24a140b8b5b0c818fbb05dd4f2d
Author: Benoit Tellier <btell...@linagora.com>
AuthorDate: Mon Aug 17 15:49:30 2020 +0700

    JAMES-3359 Mailbox/set update patch validation
---
 .../contract/MailboxSetMethodContract.scala        | 214 ++++++++++++++++++++-
 .../org/apache/james/jmap/mail/MailboxSet.scala    |  25 ++-
 .../james/jmap/method/MailboxSetMethod.scala       |   5 +-
 3 files changed, 237 insertions(+), 7 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/MailboxSetMethodContract.scala
 
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
index 8d682ca..25b3859 100644
--- 
a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
+++ 
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
@@ -2262,6 +2262,218 @@ trait MailboxSetMethodContract {
   }
 
   @Test
+  def updateShouldFailWhenWrongJsonObject(server: GuiceJamesServer): Unit = {
+    val mailboxId1: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB,
 "previousName"))
+    val request =
+      s"""
+        |{
+        |   "using": [ "urn:ietf:params:jmap:core", 
"urn:ietf:params:jmap:mail" ],
+        |   "methodCalls": [
+        |       ["Mailbox/set",
+        |           {
+        |                "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |                "update": {
+        |                    "${mailboxId1.serialize()}": {
+        |                      "/name": ["newName"]
+        |                    }
+        |                }
+        |           },
+        |    "c1"]]
+        |}
+        |""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |     "sessionState": "75128aab4b1b",
+         |     "methodResponses": [
+         |             ["Mailbox/set", {
+         |                     "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                     "newState": "000001",
+         |                     "notUpdated": {
+         |                             "${mailboxId1.serialize()}": {
+         |                                     "type": "invalidArguments",
+         |                                     "description": "Expecting a 
JSON string as an argument",
+         |                                     "properties": ["/name"]
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def updateShouldFailWhenUnknownProperty(server: GuiceJamesServer): Unit = {
+    val mailboxId1: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB,
 "previousName"))
+    val request =
+      s"""
+        |{
+        |   "using": [ "urn:ietf:params:jmap:core", 
"urn:ietf:params:jmap:mail" ],
+        |   "methodCalls": [
+        |       ["Mailbox/set",
+        |           {
+        |                "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |                "update": {
+        |                    "${mailboxId1.serialize()}": {
+        |                      "/unknown": "newValue"
+        |                    }
+        |                }
+        |           },
+        |    "c1"]]
+        |}
+        |""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |     "sessionState": "75128aab4b1b",
+         |     "methodResponses": [
+         |             ["Mailbox/set", {
+         |                     "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                     "newState": "000001",
+         |                     "notUpdated": {
+         |                             "${mailboxId1.serialize()}": {
+         |                                     "type": "invalidArguments",
+         |                                     "description": "/unknown 
property do not exist thus cannot be updated",
+         |                                     "properties": ["/unknown"]
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def updateShouldFailWhenEmptyProperty(server: GuiceJamesServer): Unit = {
+    val mailboxId1: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB,
 "previousName"))
+    val request =
+      s"""
+        |{
+        |   "using": [ "urn:ietf:params:jmap:core", 
"urn:ietf:params:jmap:mail" ],
+        |   "methodCalls": [
+        |       ["Mailbox/set",
+        |           {
+        |                "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |                "update": {
+        |                    "${mailboxId1.serialize()}": {
+        |                      "": "newValue"
+        |                    }
+        |                }
+        |           },
+        |    "c1"]]
+        |}
+        |""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    val message = "Invalid property specified in a patch object: Both 
predicates of (!isEmpty() && \\\"\\\".startsWith(\\\"/\\\")) failed. Left: 
Predicate isEmpty() did not fail. Right: Predicate failed: 
\\\"\\\".startsWith(\\\"/\\\")."
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |     "sessionState": "75128aab4b1b",
+         |     "methodResponses": [
+         |             ["Mailbox/set", {
+         |                     "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                     "newState": "000001",
+         |                     "notUpdated": {
+         |                             "${mailboxId1.serialize()}": {
+         |                                     "type": "invalidPatch",
+         |                                     "description": "$message"
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def updateShouldFailWhenInvalidProperty(server: GuiceJamesServer): Unit = {
+    val mailboxId1: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB,
 "previousName"))
+    val request =
+      s"""
+        |{
+        |   "using": [ "urn:ietf:params:jmap:core", 
"urn:ietf:params:jmap:mail" ],
+        |   "methodCalls": [
+        |       ["Mailbox/set",
+        |           {
+        |                "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |                "update": {
+        |                    "${mailboxId1.serialize()}": {
+        |                      "name": "newValue"
+        |                    }
+        |                }
+        |           },
+        |    "c1"]]
+        |}
+        |""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    val message = "Invalid property specified in a patch object: Right 
predicate of (!isEmpty(name) && \\\"name\\\".startsWith(\\\"/\\\")) failed: 
Predicate failed: \\\"name\\\".startsWith(\\\"/\\\")."
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |     "sessionState": "75128aab4b1b",
+         |     "methodResponses": [
+         |             ["Mailbox/set", {
+         |                     "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                     "newState": "000001",
+         |                     "notUpdated": {
+         |                             "${mailboxId1.serialize()}": {
+         |                                     "type": "invalidPatch",
+         |                                     "description": "$message"
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin)
+  }
+
+  @Test
   def updateShouldFailWhenMailboxNameIsTooLong(server: GuiceJamesServer): Unit 
= {
     val mailboxId1: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB,
 "previousName"))
     val request =
@@ -2523,6 +2735,4 @@ trait MailboxSetMethodContract {
          |     ]
          |}""".stripMargin)
   }
-
-  // TODO invalid path handling (unknown property, invalid name)
 }
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala
index 3452209..e0964d6 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala
@@ -22,8 +22,12 @@ package org.apache.james.jmap.mail
 import eu.timepit.refined
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.auto._
+import eu.timepit.refined.boolean.And
 import eu.timepit.refined.collection.NonEmpty
+import eu.timepit.refined.refineV
+import eu.timepit.refined.string.StartsWith
 import org.apache.james.jmap.mail.MailboxName.MailboxName
+import org.apache.james.jmap.mail.MailboxPatchObject.MailboxPatchObjectKey
 import org.apache.james.jmap.mail.MailboxSetRequest.{MailboxCreationId, 
UnparsedMailboxId}
 import org.apache.james.jmap.model.AccountId
 import org.apache.james.jmap.model.State.State
@@ -55,11 +59,20 @@ case class MailboxCreationRequest(name: MailboxName,
                                   isSubscribed: Option[IsSubscribed],
                                   rights: Option[Rights])
 
+object MailboxPatchObject {
+  type KeyConstraint = NonEmpty And StartsWith["/"]
+  type MailboxPatchObjectKey = String Refined KeyConstraint
+}
+
 case class MailboxPatchObject(value: Map[String, JsValue]) {
   def updates: Iterable[Either[PatchUpdateValidationException, Update]] = 
value.map({
     case (property, newValue) => property match {
       case "/name" => NameUpdate.parse(newValue)
-      case property => Left(UnsupportedPropertyUpdated(property))
+      case property =>
+        val refinedKey: Either[String, MailboxPatchObjectKey] = 
refineV(property)
+        refinedKey.fold[Either[PatchUpdateValidationException, Update]](
+          cause => Left(InvalidPropertyException(property = property, cause = 
s"Invalid property specified in a patch object: $cause")),
+          value => Left(UnsupportedPropertyUpdatedException(value)))
     }
   })
 }
@@ -77,6 +90,7 @@ case class MailboxSetResponse(accountId: AccountId,
 object MailboxSetError {
   val invalidArgumentValue: SetErrorType = "invalidArguments"
   val serverFailValue: SetErrorType = "serverFail"
+  val invalidPatchValue: SetErrorType = "invalidPatch"
   val notFoundValue: SetErrorType = "notFound"
   val mailboxHasEmailValue: SetErrorType = "mailboxHasEmail"
   val mailboxHasChildValue: SetErrorType = "mailboxHasChild"
@@ -87,6 +101,7 @@ object MailboxSetError {
   def notFound(description: Option[SetErrorDescription]) = 
MailboxSetError(notFoundValue, description, None)
   def mailboxHasEmail(description: Option[SetErrorDescription]) = 
MailboxSetError(mailboxHasEmailValue, description, None)
   def mailboxHasChild(description: Option[SetErrorDescription]) = 
MailboxSetError(mailboxHasChildValue, description, None)
+  def invalidPatch(description: Option[SetErrorDescription]) = 
MailboxSetError(invalidPatchValue, description, None)
   def forbidden(description: Option[SetErrorDescription], properties: 
Option[Properties]) = MailboxSetError(forbiddenValue, description, properties)
 }
 
@@ -114,7 +129,7 @@ case class MailboxUpdateResponse(value: JsObject)
 object NameUpdate {
   def parse(newValue: JsValue): Either[PatchUpdateValidationException, Update] 
= newValue match {
     case JsString(newName) => scala.Right(NameUpdate(newName))
-    case _ => Left(InvalidUpdate("name", "Expectint a JSON string as an 
argument"))
+    case _ => Left(InvalidUpdateException("/name", "Expecting a JSON string as 
an argument"))
   }
 }
 
@@ -122,5 +137,7 @@ sealed trait Update
 case class NameUpdate(newName: String) extends Update
 
 class PatchUpdateValidationException() extends IllegalArgumentException
-case class UnsupportedPropertyUpdated(property: String) extends 
PatchUpdateValidationException
-case class InvalidUpdate(property: String, cause: String) extends 
PatchUpdateValidationException
\ No newline at end of file
+case class UnsupportedPropertyUpdatedException(property: 
MailboxPatchObjectKey) extends PatchUpdateValidationException
+case class InvalidPropertyUpdatedException(property: MailboxPatchObjectKey) 
extends PatchUpdateValidationException
+case class InvalidPropertyException(property: String, cause: String) extends 
PatchUpdateValidationException
+case class InvalidUpdateException(property: MailboxPatchObjectKey, cause: 
String) extends PatchUpdateValidationException
\ No newline at end of file
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
index 6018c8f..2d10905 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
@@ -23,7 +23,7 @@ import eu.timepit.refined.auto._
 import javax.inject.Inject
 import org.apache.james.jmap.json.Serializer
 import org.apache.james.jmap.mail.MailboxSetRequest.{MailboxCreationId, 
UnparsedMailboxId}
-import org.apache.james.jmap.mail.{IsSubscribed, MailboxCreationRequest, 
MailboxCreationResponse, MailboxPatchObject, MailboxRights, MailboxSetError, 
MailboxSetRequest, MailboxSetResponse, MailboxUpdateResponse, NameUpdate, 
PatchUpdateValidationException, Properties, SetErrorDescription, TotalEmails, 
TotalThreads, UnreadEmails, UnreadThreads}
+import org.apache.james.jmap.mail.{InvalidPropertyException, 
InvalidUpdateException, IsSubscribed, MailboxCreationRequest, 
MailboxCreationResponse, MailboxPatchObject, MailboxRights, MailboxSetError, 
MailboxSetRequest, MailboxSetResponse, MailboxUpdateResponse, NameUpdate, 
PatchUpdateValidationException, Properties, SetErrorDescription, TotalEmails, 
TotalThreads, UnreadEmails, UnreadThreads, UnsupportedPropertyUpdatedException}
 import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
 import org.apache.james.jmap.model.{ClientId, Id, Invocation, ServerId, State}
@@ -123,6 +123,9 @@ case class UpdateFailure(mailboxId: UnparsedMailboxId, 
exception: Throwable) ext
     case e: MailboxNotFoundException => 
MailboxSetError.notFound(Some(SetErrorDescription(e.getMessage)))
     case e: MailboxNameException => 
MailboxSetError.invalidArgument(Some(SetErrorDescription(e.getMessage)), 
Some(Properties(List("/name"))))
     case e: MailboxExistsException => 
MailboxSetError.invalidArgument(Some(SetErrorDescription(e.getMessage)), 
Some(Properties(List("/name"))))
+    case e: UnsupportedPropertyUpdatedException => 
MailboxSetError.invalidArgument(Some(SetErrorDescription(s"${e.property} 
property do not exist thus cannot be updated")), 
Some(Properties(List(e.property))))
+    case e: InvalidUpdateException => 
MailboxSetError.invalidArgument(Some(SetErrorDescription(s"${e.cause}")), 
Some(Properties(List(e.property))))
+    case e: InvalidPropertyException => 
MailboxSetError.invalidPatch(Some(SetErrorDescription(s"${e.cause}")))
     case e: SystemMailboxChangeException => 
MailboxSetError.invalidArgument(Some(SetErrorDescription("Invalid change to a 
system mailbox")), Some(Properties(List("/name"))))
     case _ => 
MailboxSetError.serverFail(Some(SetErrorDescription(exception.getMessage)), 
None)
   }


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org
For additional commands, e-mail: server-dev-h...@james.apache.org

Reply via email to