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/02/03 02:51:06 UTC

[james-project] branch master updated (6b4fca0 -> e5cdea9)

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

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


    from 6b4fca0  JAMES-3444 Perform JMAP TransportChecks only when JMAP is enabled
     new 7fb867f  JAMES-3491 MemoryUploadContract => MemoryUploadTest
     new f47042a  JAMES-3491 JMAP WebSocket transport model
     new fe23fea  JAMES-3491 JMAP WebSocket transport JSON serialization
     new 57acf28  JAMES-3491 Implement WebSocket routes
     new 647e2e8  JAMES-3491 Advertise the JMAP websocket capability in the session
     new ff7f6d1  JAMES-3491 Custom extensions should be advertised in the JMAP session
     new 29d6ef0  JAMES-3491 JMAP WebSocket transport test contract skeleton
     new 34f1a68  JAMES-3491 WIP write tests for RFC-8887 JMAP over websocket support
     new 103be4e  JAMES-3491 Write tests for RFC-8887 JMAP over websocket support
     new 8401dd4  JAMES-3495 Better factorize error handling between HTTP and WebSocket transport
     new 72a1ae1  JAMES-3491 Cleanup WebSocket tests
     new e5cdea9  JAMES-3491 Experiment sttp for websocket client

The 12 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../org/apache/james/jmap/draft/JMAPModule.java    |   5 +
 .../james/jmap/rfc8621/RFC8621MethodsModule.java   |   9 +-
 ...loadTest.java => DistributedWebSocketTest.java} |   4 +-
 .../jmap-rfc-8621-integration-tests-common/pom.xml |   9 +
 .../rfc8621/contract/CustomMethodContract.scala    | 118 ++++++-
 .../rfc8621/contract/SessionRoutesContract.scala   |   9 +
 .../jmap/rfc8621/contract/WebSocketContract.scala  | 383 +++++++++++++++++++++
 ...ryUploadContract.java => MemoryUploadTest.java} |   2 +-
 ...ploadContract.java => MemoryWebSocketTest.java} |   4 +-
 .../org/apache/james/jmap/core/Capabilities.scala  |  25 +-
 .../org/apache/james/jmap/core/Capability.scala    |  11 +-
 .../james/jmap/core/JmapRfc8621Configuration.scala |   1 +
 .../apache/james/jmap/core/ProblemDetails.scala    |  25 +-
 ...sponseObject.scala => WebSocketTransport.scala} |  14 +-
 .../james/jmap/json/ResponseSerializer.scala       |  62 +++-
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   |  35 +-
 .../apache/james/jmap/routes/SessionSupplier.scala |  17 +-
 .../apache/james/jmap/routes/WebSocketRoutes.scala | 115 +++++++
 .../jmap/json/MailboxGetSerializationTest.scala    |   7 +-
 .../james/jmap/json/SessionSerializationTest.scala |   2 +-
 .../james/jmap/routes/JMAPApiRoutesTest.scala      |   4 +-
 .../james/jmap/routes/SessionRoutesTest.scala      |  13 +-
 .../james/jmap/routes/SessionSupplierTest.scala    |   8 +-
 .../main/java/org/apache/james/jmap/JMAPUrls.java  |   1 +
 24 files changed, 803 insertions(+), 80 deletions(-)
 copy server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/{DistributedUploadTest.java => DistributedWebSocketTest.java} (95%)
 create mode 100644 server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
 copy server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/{MemoryUploadContract.java => MemoryUploadTest.java} (96%)
 rename server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/{MemoryUploadContract.java => MemoryWebSocketTest.java} (93%)
 copy server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/{ResponseObject.scala => WebSocketTransport.scala} (71%)
 create mode 100644 server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala


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


[james-project] 09/12: JAMES-3491 Write tests for RFC-8887 JMAP over websocket support

Posted by bt...@apache.org.
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

commit 103be4ed1f83f2f59ea0e5acd544ee37e3e7c734
Author: Raphael Ouazana <ra...@linagora.com>
AuthorDate: Thu Jan 28 15:04:22 2021 +0100

    JAMES-3491 Write tests for RFC-8887 JMAP over websocket support
---
 .../jmap/rfc8621/contract/WebSocketContract.scala  | 183 ++++++++++++++++++++-
 1 file changed, 174 insertions(+), 9 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/WebSocketContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
index 73ea439..8b87d55 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
@@ -251,22 +251,187 @@ trait WebSocketContract {
 
   @Test
   def badTypeFieldShouldTriggerError(server: GuiceJamesServer): Unit = {
-    /*
-    * TODO send something with @type being a JsNumber and get an error level error
-    * */
+    println("started")
+    val port = server.getProbe(classOf[JmapGuiceProbe])
+      .getJmapPort
+      .getValue
+    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
+    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
+    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
+
+    client.connectBlocking()
+
+    Thread.sleep(500)
+
+    client.send("""{
+                  |  "@type": 42,
+                  |  "requestId": "req-36",
+                  |  "using": [ "urn:ietf:params:jmap:core"],
+                  |  "methodCalls": [
+                  |    [
+                  |      "Core/echo",
+                  |      {
+                  |        "arg1": "arg1data",
+                  |        "arg2": "arg2data"
+                  |      },
+                  |      "c1"
+                  |    ]
+                  |  ]
+                  |}""".stripMargin)
+
+    Thread.sleep(500)
+
+    assertThat(client.receivedResponses).hasSize(1)
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
+      """
+        |{
+        |  "status":400,
+        |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Invalid @type filed on a webSocket inbound message: expecting a JsString, got 42),ArraySeq()))))",
+        |  "type":"urn:ietf:params:jmap:error:notRequest",
+        |  "requestId":null,
+        |  "@type":"RequestError"
+        |}
+        |""".stripMargin
+    )
+
   }
 
   @Test
   def unknownTypeFieldShouldTriggerError(server: GuiceJamesServer): Unit = {
-    /*
-    * TODO send something with @type being a JsString("unknown") and get an error level error
-    * */
+    println("started")
+    val port = server.getProbe(classOf[JmapGuiceProbe])
+      .getJmapPort
+      .getValue
+    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
+    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
+    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
+
+    client.connectBlocking()
+
+    Thread.sleep(500)
+
+    client.send(
+      """{
+        |  "@type": "unknown",
+        |  "requestId": "req-36",
+        |  "using": [ "urn:ietf:params:jmap:core"],
+        |  "methodCalls": [
+        |    [
+        |      "Core/echo",
+        |      {
+        |        "arg1": "arg1data",
+        |        "arg2": "arg2data"
+        |      },
+        |      "c1"
+        |    ]
+        |  ]
+        |}""".stripMargin)
+
+    Thread.sleep(500)
+
+    assertThat(client.receivedResponses).hasSize(1)
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
+      """
+        |{
+        |  "status":400,
+        |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Unknown @type filed on a webSocket inbound message: unknown),ArraySeq()))))",
+        |  "type":"urn:ietf:params:jmap:error:notRequest",
+        |  "requestId":null,
+        |  "@type":"RequestError"
+        |}
+        |""".stripMargin
+    )
+  }
+
+
+  @Test
+  def clientSendingARespondTypeFieldShouldTriggerError(server: GuiceJamesServer): Unit = {
+    println("started")
+    val port = server.getProbe(classOf[JmapGuiceProbe])
+      .getJmapPort
+      .getValue
+    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
+    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
+    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
+
+    client.connectBlocking()
+
+    Thread.sleep(500)
+
+    client.send(
+      """{
+        |  "@type": "Response",
+        |  "requestId": "req-36",
+        |  "using": [ "urn:ietf:params:jmap:core"],
+        |  "methodCalls": [
+        |    [
+        |      "Core/echo",
+        |      {
+        |        "arg1": "arg1data",
+        |        "arg2": "arg2data"
+        |      },
+        |      "c1"
+        |    ]
+        |  ]
+        |}""".stripMargin)
+
+    Thread.sleep(500)
+
+    assertThat(client.receivedResponses).hasSize(1)
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
+      """
+        |{
+        |  "status":400,
+        |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Unknown @type filed on a webSocket inbound message: Response),ArraySeq()))))",
+        |  "type":"urn:ietf:params:jmap:error:notRequest",
+        |  "requestId":null,
+        |  "@type":"RequestError"
+        |}
+        |""".stripMargin
+    )
+
   }
 
   @Test
   def requestLevelErrorShouldReturnAPIError(server: GuiceJamesServer): Unit = {
-    /*
-    * TODO send a request triggering a method level error (eg Mailbox/get with an invalid JSON payload)
-    * */
+    println("started")
+    val port = server.getProbe(classOf[JmapGuiceProbe])
+      .getJmapPort
+      .getValue
+    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
+    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
+    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
+
+    client.connectBlocking()
+
+    Thread.sleep(500)
+
+    client.send(s"""{
+                   |  "@type": "Request",
+                   |  "using": [
+                   |    "urn:ietf:params:jmap:core",
+                   |    "urn:ietf:params:jmap:mail"],
+                   |  "methodCalls": [[
+                   |      "Mailbox/get",
+                   |      {
+                   |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+                   |        "properties": ["invalidProperty"]
+                   |      },
+                   |      "c1"]]
+                   |}""".stripMargin)
+
+    Thread.sleep(500)
+
+
+    assertThat(client.receivedResponses).hasSize(1)
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
+      """
+        |{
+        |  "@type": "Response",
+        |  "requestId": null,
+        |  "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+        |  "methodResponses": [["error",{"type":"invalidArguments","description":"The following properties [invalidProperty] do not exist."},"c1"]]
+        |}
+        |""".stripMargin)
   }
 }


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


[james-project] 02/12: JAMES-3491 JMAP WebSocket transport model

Posted by bt...@apache.org.
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

commit f47042a172387cc867c1112069aee1761a9029a2
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Jan 28 10:47:41 2021 +0700

    JAMES-3491 JMAP WebSocket transport model
---
 .../james/jmap/core/WebSocketTransport.scala       | 32 ++++++++++++++++++++++
 1 file changed, 32 insertions(+)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/WebSocketTransport.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/WebSocketTransport.scala
new file mode 100644
index 0000000..f7df48b
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/WebSocketTransport.scala
@@ -0,0 +1,32 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ * http://www.apache.org/licenses/LICENSE-2.0                   *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.core
+
+sealed trait WebSocketInboundMessage
+
+sealed trait WebSocketOutboundMessage
+
+case class RequestId(value: String) extends AnyVal
+
+case class WebSocketRequest(requestId: Option[RequestId], requestObject: RequestObject) extends WebSocketInboundMessage
+
+case class WebSocketResponse(requestId: Option[RequestId], responseObject: ResponseObject) extends WebSocketOutboundMessage
+
+case class WebSocketError(requestId: Option[RequestId], problemDetails: ProblemDetails) extends WebSocketOutboundMessage
\ No newline at end of file


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


[james-project] 08/12: JAMES-3491 WIP write tests for RFC-8887 JMAP over websocket support

Posted by bt...@apache.org.
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

commit 34f1a6888b26bd3d86edcbc46b06d5c8d28786e8
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Jan 28 17:25:10 2021 +0700

    JAMES-3491 WIP write tests for RFC-8887 JMAP over websocket support
    
    Exercise only the transport layer, no PUSH support yet.
---
 .../jmap-rfc-8621-integration-tests-common/pom.xml |   9 +
 .../jmap/rfc8621/contract/WebSocketContract.scala  | 225 +++++++++++++++++++--
 2 files changed, 215 insertions(+), 19 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
index 539a520..b84b5f6 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
@@ -40,6 +40,10 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-jmap-draft</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>testing-base</artifactId>
         </dependency>
         <dependency>
@@ -58,6 +62,11 @@
             <groupId>org.awaitility</groupId>
             <artifactId>awaitility</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.java-websocket</groupId>
+            <artifactId>Java-WebSocket</artifactId>
+            <version>1.5.1</version>
+        </dependency>
     </dependencies>
 
     <build>
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/WebSocketContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
index ae91a42..73ea439 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
@@ -18,10 +18,18 @@
  ****************************************************************/
 package org.apache.james.jmap.rfc8621.contract
 
+import java.net.URI
+import java.util
+
+import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.draft.JmapGuiceProbe
 import org.apache.james.jmap.rfc8621.contract.Fixture._
 import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
 import org.apache.james.utils.DataProbeImpl
+import org.assertj.core.api.Assertions.assertThat
+import org.java_websocket.client.WebSocketClient
+import org.java_websocket.handshake.ServerHandshake
 import org.junit.jupiter.api.{BeforeEach, Tag, Test}
 
 trait WebSocketContract {
@@ -33,51 +41,230 @@ trait WebSocketContract {
       .addUser(BOB.asString(), BOB_PASSWORD)
   }
 
+  class ExampleClient(uri: URI) extends WebSocketClient(uri) {
+    val receivedResponses: util.LinkedList[String] = new util.LinkedList[String]()
+    var closeCode: Option[Integer] = None
+    var closeString: Option[String] = None
+
+    override def onOpen(serverHandshake: ServerHandshake): Unit = {
+      println(s"handshake ${serverHandshake.getHttpStatus}")
+    }
+
+    override def onMessage(s: String): Unit = {
+      println(s"Received: $s")
+      receivedResponses.add(s)
+    }
+
+    override def onClose(i: Int, s: String, b: Boolean): Unit = {
+      closeCode = Some(i)
+      closeString = Some(s)
+      println(s"Closing connection $i $s $b")
+    }
+
+    override def onError(e: Exception): Unit = {
+      println("Error: " + e.getMessage)
+    }
+  }
+
   @Test
   @Tag(CategoryTags.BASIC_FEATURE)
-  def apiRequestsShouldBeProcessed(): Unit = {
-    /*
-    * TODO test an echo response - request (success)
-    * */
+  def apiRequestsShouldBeProcessed(server: GuiceJamesServer): Unit = {
+    println("started")
+    val port = server.getProbe(classOf[JmapGuiceProbe])
+      .getJmapPort
+      .getValue
+    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
+    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
+    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
+
+    client.connectBlocking()
+
+    Thread.sleep(500)
+
+    client.send("""{
+                  |  "@type": "Request",
+                  |  "requestId": "req-36",
+                  |  "using": [ "urn:ietf:params:jmap:core"],
+                  |  "methodCalls": [
+                  |    [
+                  |      "Core/echo",
+                  |      {
+                  |        "arg1": "arg1data",
+                  |        "arg2": "arg2data"
+                  |      },
+                  |      "c1"
+                  |    ]
+                  |  ]
+                  |}""".stripMargin)
+
+    Thread.sleep(500)
+
+
+    assertThat(client.receivedResponses).hasSize(1)
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
+      """
+        |{
+        |  "@type":"Response",
+        |  "requestId":"req-36",
+        |  "sessionState":"2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+        |  "methodResponses":[["Core/echo",{"arg1":"arg1data","arg2":"arg2data"},"c1"]]
+        |}
+        |""".stripMargin)
   }
 
   @Test
-  def nonJsonPayloadShouldTriggerError(): Unit = {
-    /*
-    * TODO send 'the quick brown fox' and get an error level error
-    * */
+  def apiRequestsShouldBeProcessedWhenNoRequestId(server: GuiceJamesServer): Unit = {
+    println("started")
+    val port = server.getProbe(classOf[JmapGuiceProbe])
+      .getJmapPort
+      .getValue
+    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
+    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
+    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
+
+    client.connectBlocking()
+
+    Thread.sleep(500)
+
+    client.send("""{
+                  |  "@type": "Request",
+                  |  "using": [ "urn:ietf:params:jmap:core"],
+                  |  "methodCalls": [
+                  |    [
+                  |      "Core/echo",
+                  |      {
+                  |        "arg1": "arg1data",
+                  |        "arg2": "arg2data"
+                  |      },
+                  |      "c1"
+                  |    ]
+                  |  ]
+                  |}""".stripMargin)
+
+    Thread.sleep(500)
+
+
+    assertThat(client.receivedResponses).hasSize(1)
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
+      """
+        |{
+        |  "@type":"Response",
+        |  "requestId":null,
+        |  "sessionState":"2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+        |  "methodResponses":[["Core/echo",{"arg1":"arg1data","arg2":"arg2data"},"c1"]]
+        |}
+        |""".stripMargin)
   }
 
   @Test
-  def handshakeShouldBeAuthenticated(): Unit = {
-    /*
-    * TODO set up no auth
-    * */
+  def nonJsonPayloadShouldTriggerError(server: GuiceJamesServer): Unit = {
+    println("started")
+    val port = server.getProbe(classOf[JmapGuiceProbe])
+      .getJmapPort
+      .getValue
+    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
+    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
+    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
+
+    client.connectBlocking()
+
+    Thread.sleep(500)
+
+    client.send("The quick brown fox".stripMargin)
+
+    Thread.sleep(500)
+
+    assertThat(client.receivedResponses).hasSize(1)
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
+      """
+        |{
+        |  "status":400,
+        |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Unrecognized token 'The': was expecting ('true', 'false' or 'null')\n at [Source: (String)\"The quick brown fox\"; line: 1, column: 4]),ArraySeq()))))",
+        |  "type":"urn:ietf:params:jmap:error:notRequest",
+        |  "requestId":null,
+        |  "@type":"RequestError"
+        |}
+        |""".stripMargin)
   }
 
   @Test
-  def noTypeFiledShouldTriggerError(): Unit = {
-    /*
-    * TODO send something without @type and get an error level error
-    * */
+  def handshakeShouldBeAuthenticated(server: GuiceJamesServer): Unit = {
+    val port = server.getProbe(classOf[JmapGuiceProbe])
+      .getJmapPort
+      .getValue
+    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
+    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
+
+    client.connectBlocking()
+
+    Thread.sleep(100)
+
+    assertThat(client.isClosed).isTrue
+    assertThat(client.closeString).isEqualTo(Some("Invalid status code received: 401 Status line: HTTP/1.1 401 Unauthorized"))
+  }
+
+  @Test
+  def noTypeFiledShouldTriggerError(server: GuiceJamesServer): Unit = {
+    println("started")
+    val port = server.getProbe(classOf[JmapGuiceProbe])
+      .getJmapPort
+      .getValue
+    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
+    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
+    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
+
+    client.connectBlocking()
+
+    Thread.sleep(500)
+
+    client.send("""{
+                  |  "requestId": "req-36",
+                  |  "using": [ "urn:ietf:params:jmap:core"],
+                  |  "methodCalls": [
+                  |    [
+                  |      "Core/echo",
+                  |      {
+                  |        "arg1": "arg1data",
+                  |        "arg2": "arg2data"
+                  |      },
+                  |      "c1"
+                  |    ]
+                  |  ]
+                  |}""".stripMargin)
+
+    Thread.sleep(500)
+
+
+    assertThat(client.receivedResponses).hasSize(1)
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
+      """
+        |{
+        |  "status":400,
+        |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Missing @type filed on a webSocket inbound message),ArraySeq()))))",
+        |  "type":"urn:ietf:params:jmap:error:notRequest",
+        |  "requestId":null,
+        |  "@type":"RequestError"
+        |}
+        |""".stripMargin
+    )
   }
 
   @Test
-  def badTypeFieldShouldTriggerError(): Unit = {
+  def badTypeFieldShouldTriggerError(server: GuiceJamesServer): Unit = {
     /*
     * TODO send something with @type being a JsNumber and get an error level error
     * */
   }
 
   @Test
-  def unknownTypeFieldShouldTriggerError(): Unit = {
+  def unknownTypeFieldShouldTriggerError(server: GuiceJamesServer): Unit = {
     /*
     * TODO send something with @type being a JsString("unknown") and get an error level error
     * */
   }
 
   @Test
-  def requestLevelErrorShouldReturnAPIError(): Unit = {
+  def requestLevelErrorShouldReturnAPIError(server: GuiceJamesServer): Unit = {
     /*
     * TODO send a request triggering a method level error (eg Mailbox/get with an invalid JSON payload)
     * */


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


[james-project] 04/12: JAMES-3491 Implement WebSocket routes

Posted by bt...@apache.org.
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

commit 57acf284e6264b0cf3078f8cb2370118602bb8bf
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Jan 28 11:41:35 2021 +0700

    JAMES-3491 Implement WebSocket routes
---
 .../james/jmap/rfc8621/RFC8621MethodsModule.java   |   9 +-
 .../james/jmap/json/ResponseSerializer.scala       |  11 +-
 .../apache/james/jmap/routes/WebSocketRoutes.scala | 145 +++++++++++++++++++++
 .../main/java/org/apache/james/jmap/JMAPUrls.java  |   1 +
 4 files changed, 158 insertions(+), 8 deletions(-)

diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
index 4de781d..530c7ec 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
+++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
@@ -54,6 +54,7 @@ import org.apache.james.jmap.routes.DownloadRoutes;
 import org.apache.james.jmap.routes.JMAPApiRoutes;
 import org.apache.james.jmap.routes.SessionRoutes;
 import org.apache.james.jmap.routes.UploadRoutes;
+import org.apache.james.jmap.routes.WebSocketRoutes;
 import org.apache.james.metrics.api.MetricFactory;
 import org.apache.james.utils.InitializationOperation;
 import org.apache.james.utils.InitilizationOperationBuilder;
@@ -97,8 +98,12 @@ public class RFC8621MethodsModule extends AbstractModule {
     }
 
     @ProvidesIntoSet
-    JMAPRoutesHandler routesHandler(SessionRoutes sessionRoutes, JMAPApiRoutes jmapApiRoutes, DownloadRoutes downloadRoutes, UploadRoutes uploadRoutes) {
-        return new JMAPRoutesHandler(Version.RFC8621, jmapApiRoutes, sessionRoutes, downloadRoutes, uploadRoutes);
+    JMAPRoutesHandler routesHandler(SessionRoutes sessionRoutes,
+                                    JMAPApiRoutes jmapApiRoutes,
+                                    DownloadRoutes downloadRoutes,
+                                    UploadRoutes uploadRoutes,
+                                    WebSocketRoutes webSocketRoutes) {
+        return new JMAPRoutesHandler(Version.RFC8621, jmapApiRoutes, sessionRoutes, downloadRoutes, uploadRoutes, webSocketRoutes);
     }
 
     @Provides
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
index edace41..02ca667 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
@@ -202,6 +202,10 @@ object ResponseSerializer {
       "requestId" -> error.requestId.map(_.value).map(JsString).getOrElse(JsNull))
       ++ errorJson.value)
   }
+  private implicit val webSocketOutboundWrites: Writes[WebSocketOutboundMessage] = {
+    case response: WebSocketResponse => webSocketResponseWrites.writes(response)
+    case error: WebSocketError => webSocketErrorWrites.writes(error)
+  }
 
   def serialize(session: Session): JsValue = Json.toJson(session)
 
@@ -213,12 +217,7 @@ object ResponseSerializer {
 
   def serialize(errors: JsError): JsValue = Json.toJson(errors)
 
-  def serialize(response: WebSocketOutboundMessage): JsValue = {
-    case response: WebSocketResponse => Json.toJson(response)
-    case error: WebSocketError => Json.toJson(error)
-  }
-
-  def serialize(errors: WebSocketError): JsValue = Json.toJson(errors)
+  def serialize(outboundMessage: WebSocketOutboundMessage): JsValue = Json.toJson(outboundMessage)
 
   def deserializeRequestObject(input: String): JsResult[RequestObject] = Json.parse(input).validate[RequestObject]
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala
new file mode 100644
index 0000000..c4b2900
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala
@@ -0,0 +1,145 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *  http://www.apache.org/licenses/LICENSE-2.0                  *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.routes
+
+import com.fasterxml.jackson.core.JsonParseException
+import io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE
+import io.netty.handler.codec.http.HttpResponseStatus.{BAD_REQUEST, INTERNAL_SERVER_ERROR, UNAUTHORIZED}
+import io.netty.handler.codec.http.websocketx.WebSocketFrame
+import io.netty.handler.codec.http.{HttpMethod, HttpResponseStatus}
+import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
+import org.apache.james.jmap.JMAPUrls.JMAP_WS
+import org.apache.james.jmap.core.ProblemDetails.{notJSONProblem, notRequestProblem, unknownCapabilityProblem}
+import org.apache.james.jmap.core.{ProblemDetails, RequestId, WebSocketError, WebSocketOutboundMessage, WebSocketRequest, WebSocketResponse}
+import org.apache.james.jmap.exceptions.UnauthorizedException
+import org.apache.james.jmap.http.rfc8621.InjectionKeys
+import org.apache.james.jmap.http.{Authenticator, UserProvisioning}
+import org.apache.james.jmap.json.ResponseSerializer
+import org.apache.james.jmap.routes.WebSocketRoutes.LOGGER
+import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
+import org.apache.james.mailbox.MailboxSession
+import org.slf4j.{Logger, LoggerFactory}
+import reactor.core.publisher.Mono
+import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.scheduler.Schedulers
+import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
+import reactor.netty.http.websocket.{WebsocketInbound, WebsocketOutbound}
+
+import java.nio.charset.StandardCharsets
+import java.util.stream
+import javax.inject.{Inject, Named}
+
+object WebSocketRoutes {
+  val LOGGER: Logger = LoggerFactory.getLogger(classOf[WebSocketRoutes])
+}
+
+class WebSocketRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticator: Authenticator,
+                                 userProvisioner: UserProvisioning,
+                                 jmapApi: JMAPApi) extends JMAPRoutes {
+
+  override def routes(): stream.Stream[JMAPRoute] = stream.Stream.of(
+    JMAPRoute.builder
+      .endpoint(new Endpoint(HttpMethod.GET, JMAP_WS))
+      .action(this.handleWebSockets)
+      .corsHeaders,
+    JMAPRoute.builder
+      .endpoint(new Endpoint(HttpMethod.OPTIONS, JMAP_WS))
+      .action(JMAPRoutes.CORS_CONTROL)
+      .corsHeaders())
+
+  private def handleWebSockets(httpServerRequest: HttpServerRequest, httpServerResponse: HttpServerResponse): Mono[Void] = {
+    SMono(authenticator.authenticate(httpServerRequest))
+      .flatMap((mailboxSession: MailboxSession) => userProvisioner.provisionUser(mailboxSession)
+        .`then`
+        .`then`(SMono(httpServerResponse.sendWebsocket((in, out) => handleWebSocketConnection(mailboxSession)(in, out)))))
+      .onErrorResume(throwable => handleHttpHandshakeError(throwable, httpServerResponse))
+      .subscribeOn(Schedulers.elastic)
+      .asJava()
+      .`then`()
+  }
+
+  private def handleWebSocketConnection(session: MailboxSession)(in: WebsocketInbound, out: WebsocketOutbound): Mono[Void] =
+    SFlux[WebSocketFrame](in.aggregateFrames()
+      .receiveFrames())
+      .map(frame => {
+        val bytes = new Array[Byte](frame.content().readableBytes)
+        frame.content().readBytes(bytes)
+        new String(bytes, StandardCharsets.UTF_8)
+      })
+      .flatMap(handleClientMessages(session))
+      .onErrorResume(e => SMono.just(asError(None)(e)))
+      .map(ResponseSerializer.serialize)
+      .map(_.toString)
+      .flatMap(response => out.sendString(SMono.just(response), StandardCharsets.UTF_8))
+      .onErrorResume(e => {
+        e.printStackTrace()
+        SMono.empty
+      })
+      .`then`()
+      .asJava()
+      .`then`()
+
+  private def handleClientMessages(session: MailboxSession)(message: String): SMono[WebSocketOutboundMessage] =
+    ResponseSerializer.deserializeWebSocketInboundMessage(message)
+      .fold(invalid => {
+        val error = asError(None)(new IllegalArgumentException(invalid.toString()))
+        SMono.just[WebSocketOutboundMessage](error)
+      }, {
+          case request: WebSocketRequest =>
+            jmapApi.process(request.requestObject, session)
+              .map[WebSocketOutboundMessage](WebSocketResponse(request.requestId, _))
+              .onErrorResume(e => SMono.just(asError(request.requestId)(e)))
+              .subscribeOn(Schedulers.elastic)
+        })
+
+  private def handleHttpHandshakeError(throwable: Throwable, response: HttpServerResponse): SMono[Void] = throwable match {
+    case e: UnauthorizedException =>
+      LOGGER.warn("Unauthorized", e)
+      respondDetails(response,
+        ProblemDetails(status = UNAUTHORIZED, detail = e.getMessage),
+        UNAUTHORIZED)
+    case e =>
+      LOGGER.error("Unexpected error upon WebSocket handshake request", e)
+      respondDetails(response,
+        ProblemDetails(status = INTERNAL_SERVER_ERROR, detail = e.getMessage),
+        INTERNAL_SERVER_ERROR)
+  }
+
+  private def asError(requestId: Option[RequestId])(throwable: Throwable): WebSocketError = throwable match {
+    case exception: IllegalArgumentException =>
+      WebSocketError(requestId, notRequestProblem(
+        s"The request was successfully parsed as JSON but did not match the type signature of the Request object: ${exception.getMessage}"))
+    case exception: JsonParseException =>
+      WebSocketError(requestId, notJSONProblem(
+        s"The content type of the request was not application/json or the request did not parse as I-JSON: ${exception.getMessage}"))
+    case exception: UnsupportedCapabilitiesException =>
+      WebSocketError(requestId, unknownCapabilityProblem(s"The request used unsupported capabilities: ${exception.capabilities}"))
+    case e =>
+      LOGGER.error("Unexpected error upon API request", e)
+      WebSocketError(requestId, ProblemDetails(status = INTERNAL_SERVER_ERROR, detail = e.getMessage))
+  }
+
+  private def respondDetails(httpServerResponse: HttpServerResponse, details: ProblemDetails, statusCode: HttpResponseStatus = BAD_REQUEST): SMono[Void] =
+    SMono.fromPublisher(httpServerResponse.status(statusCode)
+      .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
+      .sendString(SMono.fromCallable(() => ResponseSerializer.serialize(details).toString),
+        StandardCharsets.UTF_8)
+      .`then`)
+}
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPUrls.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPUrls.java
index f7256c7..6148383 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPUrls.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPUrls.java
@@ -21,6 +21,7 @@ package org.apache.james.jmap;
 
 public interface JMAPUrls {
     String JMAP = "/jmap";
+    String JMAP_WS = "/jmap/ws";
     String AUTHENTICATION = "/authentication";
     String DOWNLOAD = "/download";
     String UPLOAD = "/upload";


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


[james-project] 06/12: JAMES-3491 Custom extensions should be advertised in the JMAP session

Posted by bt...@apache.org.
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

commit ff7f6d1953cc7fe9a19b3bb6aa8f59a675e22e13
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Jan 28 12:51:29 2021 +0700

    JAMES-3491 Custom extensions should be advertised in the JMAP session
---
 .../org/apache/james/jmap/draft/JMAPModule.java    |   5 +
 .../rfc8621/contract/CustomMethodContract.scala    | 118 +++++++++++++++++++--
 .../org/apache/james/jmap/core/Capabilities.scala  |  10 +-
 .../james/jmap/json/ResponseSerializer.scala       |   5 +-
 .../apache/james/jmap/routes/SessionSupplier.scala |  17 ++-
 .../james/jmap/json/SessionSerializationTest.scala |   2 +-
 .../james/jmap/routes/SessionRoutesTest.scala      |   4 +-
 .../james/jmap/routes/SessionSupplierTest.scala    |   8 +-
 8 files changed, 144 insertions(+), 25 deletions(-)

diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java
index 7185537..f585569 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java
+++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java
@@ -162,6 +162,11 @@ public class JMAPModule extends AbstractModule {
         return DefaultCapabilities.coreCapability(configuration.maxUploadSize());
     }
 
+    @ProvidesIntoSet
+    Capability webSocketCapability(JmapRfc8621Configuration configuration) {
+        return DefaultCapabilities.webSocketCapability(configuration.webSocketUrl());
+    }
+
     @Provides
     @Singleton
     JMAPConfiguration provideConfiguration(PropertiesProvider propertiesProvider) throws ConfigurationException {
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/CustomMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala
index 87ca8d1..48b8b8b 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala
@@ -20,7 +20,7 @@
 package org.apache.james.jmap.rfc8621.contract
 
 import com.google.inject.AbstractModule
-import com.google.inject.multibindings.{Multibinder, ProvidesIntoSet}
+import com.google.inject.multibindings.Multibinder
 import eu.timepit.refined.auto._
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured._
@@ -44,20 +44,107 @@ import reactor.core.scala.publisher.SMono
 
 object CustomMethodContract {
   val CUSTOM: CapabilityIdentifier = "urn:apache:james:params:jmap:custom"
+
+  private val expected_session_object: String =
+    s"""{
+      |  "capabilities" : {
+      |    "urn:ietf:params:jmap:submission": {
+      |      "maxDelayedSend": 0,
+      |      "submissionExtensions": []
+      |    },
+      |    "urn:ietf:params:jmap:core" : {
+      |      "maxSizeUpload" : 20971520,
+      |      "maxConcurrentUpload" : 4,
+      |      "maxSizeRequest" : 10000000,
+      |      "maxConcurrentRequests" : 4,
+      |      "maxCallsInRequest" : 16,
+      |      "maxObjectsInGet" : 500,
+      |      "maxObjectsInSet" : 500,
+      |      "collationAlgorithms" : [ "i;unicode-casemap" ]
+      |    },
+      |    "urn:ietf:params:jmap:mail" : {
+      |      "maxMailboxesPerEmail" : 10000000,
+      |      "maxMailboxDepth" : null,
+      |      "maxSizeMailboxName" : 200,
+      |      "maxSizeAttachmentsPerEmail" : 20000000,
+      |      "emailQuerySortOptions" : ["receivedAt", "sentAt"],
+      |      "mayCreateTopLevelMailbox" : true
+      |    },
+      |    "urn:ietf:params:jmap:websocket": {
+      |      "supportsPush": false,
+      |      "url": "http://domain.com/jmap/ws"
+      |    },
+      |    "urn:apache:james:params:jmap:mail:quota": {},
+      |    "$CUSTOM": {},
+      |    "urn:apache:james:params:jmap:mail:shares": {},
+      |    "urn:ietf:params:jmap:vacationresponse":{}
+      |  },
+      |  "accounts" : {
+      |    "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6" : {
+      |      "name" : "bob@domain.tld",
+      |      "isPersonal" : true,
+      |      "isReadOnly" : false,
+      |      "accountCapabilities" : {
+      |        "urn:ietf:params:jmap:submission": {
+      |          "maxDelayedSend": 0,
+      |          "submissionExtensions": []
+      |        },
+      |        "urn:ietf:params:jmap:websocket": {
+      |            "supportsPush": false,
+      |            "url": "http://domain.com/jmap/ws"
+      |        },
+      |        "urn:ietf:params:jmap:core" : {
+      |          "maxSizeUpload" : 20971520,
+      |          "maxConcurrentUpload" : 4,
+      |          "maxSizeRequest" : 10000000,
+      |          "maxConcurrentRequests" : 4,
+      |          "maxCallsInRequest" : 16,
+      |          "maxObjectsInGet" : 500,
+      |          "maxObjectsInSet" : 500,
+      |          "collationAlgorithms" : [ "i;unicode-casemap" ]
+      |        },
+      |        "urn:ietf:params:jmap:mail" : {
+      |          "maxMailboxesPerEmail" : 10000000,
+      |          "maxMailboxDepth" : null,
+      |          "maxSizeMailboxName" : 200,
+      |          "maxSizeAttachmentsPerEmail" : 20000000,
+      |          "emailQuerySortOptions" : ["receivedAt", "sentAt"],
+      |          "mayCreateTopLevelMailbox" : true
+      |        },
+      |        "urn:apache:james:params:jmap:mail:quota": {},
+      |        "urn:apache:james:params:jmap:mail:shares": {},
+      |        "$CUSTOM": {},
+      |        "urn:ietf:params:jmap:vacationresponse":{}
+      |      }
+      |    }
+      |  },
+      |  "primaryAccounts" : {
+      |    "urn:ietf:params:jmap:submission": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+      |    "urn:ietf:params:jmap:websocket": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+      |    "urn:ietf:params:jmap:core" : "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+      |    "urn:ietf:params:jmap:mail" : "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+      |    "urn:apache:james:params:jmap:mail:quota": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+      |    "urn:apache:james:params:jmap:mail:shares": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+      |    "$CUSTOM": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+      |    "urn:ietf:params:jmap:vacationresponse": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6"
+      |  },
+      |  "username" : "bob@domain.tld",
+      |  "apiUrl" : "http://domain.com/jmap",
+      |  "downloadUrl" : "http://domain.com/download/{accountId}/{blobId}/?type={type}&name={name}",
+      |  "uploadUrl" : "http://domain.com/upload/{accountId}",
+      |  "eventSourceUrl" : "http://domain.com/eventSource",
+      |  "state" : "2c9f1b12-b35a-43e6-9af2-0106fb53a943"
+      |}""".stripMargin
 }
 
 case class CustomCapabilityProperties() extends CapabilityProperties
 
 case class CustomCapability(properties: CustomCapabilityProperties = CustomCapabilityProperties(), identifier: CapabilityIdentifier = CUSTOM) extends Capability
 
-class CustomCapabilitiesModule extends AbstractModule {
-  @ProvidesIntoSet
-  private def capability(): Capability = CustomCapability()
-}
-
 class CustomMethodModule extends AbstractModule {
   override protected def configure(): Unit = {
-    install(new CustomCapabilitiesModule)
+    val supportedCapabilities: Multibinder[Capability] = Multibinder.newSetBinder(binder, classOf[Capability])
+    supportedCapabilities.addBinding.toInstance(CustomCapability())
     Multibinder.newSetBinder(binder(), classOf[Method])
       .addBinding()
       .to(classOf[CustomMethod])
@@ -74,7 +161,6 @@ class CustomMethod extends Method {
 }
 
 trait CustomMethodContract {
-
   @BeforeEach
   def setUp(server: GuiceJamesServer): Unit = {
     server.getProbe(classOf[DataProbeImpl])
@@ -88,6 +174,22 @@ trait CustomMethodContract {
   }
 
   @Test
+  def getShouldReturnCorrectSession(): Unit = {
+    val sessionJson: String = `given`()
+      .when()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .get("/session")
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract()
+      .body()
+      .asString()
+
+    assertThatJson(sessionJson).isEqualTo(CustomMethodContract.expected_session_object)
+  }
+
+  @Test
   def customMethodShouldRespondOKWithRFC8621VersionAndSupportedMethod(): Unit = {
     val response = `given`()
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capabilities.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capabilities.scala
index 689c9ca..d3f7be4 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capabilities.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capabilities.scala
@@ -51,7 +51,7 @@ object DefaultCapabilities {
   val VACATION_RESPONSE_CAPABILITY = VacationResponseCapability()
   val SUBMISSION_CAPABILITY = SubmissionCapability()
 
-  def supported(configuration: JmapRfc8621Configuration): Capabilities = Capabilities(
+  def supported(configuration: JmapRfc8621Configuration): Capabilities = Capabilities.of(
     coreCapability(configuration.maxUploadSize),
     MAIL_CAPABILITY,
     QUOTA_CAPABILITY,
@@ -61,8 +61,10 @@ object DefaultCapabilities {
     webSocketCapability(configuration.webSocketUrl))
 }
 
-case class Capabilities(capabilities: Capability*) {
-  def toSet: Set[Capability] = capabilities.toSet
+object Capabilities {
+  def of(capabilities: Capability*): Capabilities = Capabilities(capabilities.toSet)
+}
 
-  def ids: Set[CapabilityIdentifier] = toSet.map(_.identifier())
+case class Capabilities(capabilities: Set[Capability]) {
+  def ids: Set[CapabilityIdentifier] = capabilities.map(_.identifier())
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
index f9fcee5..ebf7eb4 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
@@ -118,12 +118,13 @@ object ResponseSerializer {
             jsObject.+(capability.identifier.value, submissionPropertiesWrites.writes(capability.properties))
           case capability: WebSocketCapability =>
             jsObject.+(capability.identifier.value, webSocketPropertiesWrites.writes(capability.properties))
-          case _ => jsObject
+          case _ =>
+            jsObject.+(capability.identifier.value, JsObject(Map[String, JsValue]()))
         }
       })
     }
 
-  private implicit val capabilitiesWrites: Writes[Capabilities] = capabilities => setCapabilityWrites.writes(capabilities.toSet)
+  private implicit val capabilitiesWrites: Writes[Capabilities] = capabilities => setCapabilityWrites.writes(capabilities.capabilities)
 
   private implicit val identifierMapWrite: Writes[Map[CapabilityIdentifier, AccountId]] =
     mapWrites[CapabilityIdentifier, AccountId](_.value, accountIdWrites)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
index bb9270f..b9beac4 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
@@ -22,13 +22,20 @@ package org.apache.james.jmap.routes
 import javax.inject.Inject
 import org.apache.james.core.Username
 import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
-import org.apache.james.jmap.core.{Account, AccountId, DefaultCapabilities, IsPersonal, IsReadOnly, JmapRfc8621Configuration, Session}
+import org.apache.james.jmap.core.{Account, AccountId, Capabilities, Capability, IsPersonal, IsReadOnly, JmapRfc8621Configuration, Session}
+
+import scala.jdk.CollectionConverters._
+
+class SessionSupplier(val configuration: JmapRfc8621Configuration, defaultCapabilities: Set[Capability]) {
+  @Inject
+  def this(configuration: JmapRfc8621Configuration, defaultCapabilities: java.util.Set[Capability]) {
+    this(configuration, defaultCapabilities.asScala.toSet)
+  }
 
-class SessionSupplier @Inject() (val configuration: JmapRfc8621Configuration) {
   def generate(username: Username): Either[IllegalArgumentException, Session] =
     accounts(username)
       .map(account => Session(
-        DefaultCapabilities.supported(configuration),
+        Capabilities(defaultCapabilities),
         List(account),
         primaryAccounts(account.accountId),
         username,
@@ -38,10 +45,10 @@ class SessionSupplier @Inject() (val configuration: JmapRfc8621Configuration) {
         eventSourceUrl = configuration.eventSourceUrl))
 
   private def accounts(username: Username): Either[IllegalArgumentException, Account] =
-    Account.from(username, IsPersonal(true), IsReadOnly(false), DefaultCapabilities.supported(configuration).toSet)
+    Account.from(username, IsPersonal(true), IsReadOnly(false), defaultCapabilities)
 
   private def primaryAccounts(accountId: AccountId): Map[CapabilityIdentifier, AccountId] =
-    DefaultCapabilities.supported(configuration).toSet
+    defaultCapabilities
       .map(capability => (capability.identifier(), accountId))
       .toMap
 }
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/SessionSerializationTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/SessionSerializationTest.scala
index 861ce63..9a28d4e 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/SessionSerializationTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/SessionSerializationTest.scala
@@ -76,7 +76,7 @@ object SessionSerializationTest {
     emailQuerySortOptions = EMAIL_QUERY_SORT_OPTIONS,
     mayCreateTopLevelMailbox = MAY_CREATE_TOP_LEVEL_MAILBOX))
 
-  private val CAPABILITIES = Capabilities(CORE_CAPABILITY, MAIL_CAPABILITY, QuotaCapability(), SharesCapability(), VacationResponseCapability())
+  private val CAPABILITIES = Capabilities.of(CORE_CAPABILITY, MAIL_CAPABILITY, QuotaCapability(), SharesCapability(), VacationResponseCapability())
 
   private val IS_PERSONAL : IsPersonal = IsPersonal(true)
   private val IS_NOT_PERSONAL : IsPersonal = IsPersonal(false)
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionRoutesTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionRoutesTest.scala
index 9e903e2..e857584 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionRoutesTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionRoutesTest.scala
@@ -30,9 +30,9 @@ import io.restassured.http.ContentType
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import org.apache.http.HttpStatus
 import org.apache.james.core.Username
-import org.apache.james.jmap.core.JmapRfc8621Configuration
 import org.apache.james.jmap.core.JmapRfc8621Configuration.LOCALHOST_URL_PREFIX
 import org.apache.james.jmap.core.State.INSTANCE
+import org.apache.james.jmap.core.{DefaultCapabilities, JmapRfc8621Configuration}
 import org.apache.james.jmap.http.Authenticator
 import org.apache.james.jmap.routes.SessionRoutesTest.{BOB, TEST_CONFIGURATION}
 import org.apache.james.jmap.{JMAPConfiguration, JMAPRoutesHandler, JMAPServer, Version, VersionParser}
@@ -66,7 +66,7 @@ class SessionRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
       .thenReturn(Mono.just(mockedSession))
 
     val sessionRoutes = new SessionRoutes(
-      sessionSupplier = new SessionSupplier(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION),
+      sessionSupplier = new SessionSupplier(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION, DefaultCapabilities.supported(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION).capabilities),
       authenticator = mockedAuthFilter)
     jmapServer = new JMAPServer(
       TEST_CONFIGURATION,
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionSupplierTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionSupplierTest.scala
index 83b7790..0b0eed7 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionSupplierTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionSupplierTest.scala
@@ -20,7 +20,7 @@
 package org.apache.james.jmap.routes
 
 import org.apache.james.core.Username
-import org.apache.james.jmap.core.JmapRfc8621Configuration
+import org.apache.james.jmap.core.{DefaultCapabilities, JmapRfc8621Configuration}
 import org.apache.james.jmap.routes.SessionSupplierTest.USERNAME
 import org.scalatest.matchers.should.Matchers
 import org.scalatest.wordspec.AnyWordSpec
@@ -33,11 +33,13 @@ class SessionSupplierTest extends AnyWordSpec with Matchers {
 
   "generate" should {
     "return correct username" in {
-      new SessionSupplier(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION).generate(USERNAME).toOption.get.username should equal(USERNAME)
+      new SessionSupplier(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION, DefaultCapabilities.supported(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION).capabilities)
+        .generate(USERNAME).toOption.get.username should equal(USERNAME)
     }
 
     "return correct account" which {
-      val accounts = new SessionSupplier(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION).generate(USERNAME).toOption.get.accounts
+      val accounts = new SessionSupplier(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION, DefaultCapabilities.supported(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION).capabilities)
+        .generate(USERNAME).toOption.get.accounts
 
       "has size" in {
         accounts should have size 1


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


[james-project] 12/12: JAMES-3491 Experiment sttp for websocket client

Posted by bt...@apache.org.
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

commit e5cdea90d6f709f9cd01d653967944cec42f27fe
Author: Raphael Ouazana <ra...@linagora.com>
AuthorDate: Thu Jan 28 17:26:19 2021 +0100

    JAMES-3491 Experiment sttp for websocket client
---
 .../jmap-rfc-8621-integration-tests-common/pom.xml |  10 +-
 .../jmap/rfc8621/contract/WebSocketContract.scala  | 540 ++++++++++++---------
 .../james/jmap/json/ResponseSerializer.scala       |  11 +-
 3 files changed, 314 insertions(+), 247 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
index b84b5f6..0fabc14 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
@@ -47,6 +47,11 @@
             <artifactId>testing-base</artifactId>
         </dependency>
         <dependency>
+            <groupId>com.softwaremill.sttp.client3</groupId>
+            <artifactId>okhttp-backend_${scala.base}</artifactId>
+            <version>3.0.0</version>
+        </dependency>
+        <dependency>
             <groupId>com.typesafe.play</groupId>
             <artifactId>play-json_${scala.base}</artifactId>
         </dependency>
@@ -62,11 +67,6 @@
             <groupId>org.awaitility</groupId>
             <artifactId>awaitility</artifactId>
         </dependency>
-        <dependency>
-            <groupId>org.java-websocket</groupId>
-            <artifactId>Java-WebSocket</artifactId>
-            <version>1.5.1</version>
-        </dependency>
     </dependencies>
 
     <build>
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/WebSocketContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
index 7bf45a3..0f708c7d 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
@@ -18,33 +18,28 @@
  ****************************************************************/
 package org.apache.james.jmap.rfc8621.contract
 
-import java.net.URI
-import java.util
-import java.util.concurrent.TimeUnit
+import java.net.{ProtocolException, URI}
 
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import org.apache.james.GuiceJamesServer
 import org.apache.james.jmap.draft.JmapGuiceProbe
 import org.apache.james.jmap.rfc8621.contract.Fixture._
-import org.apache.james.jmap.rfc8621.contract.WebSocketContract.{LOGGER, await}
-import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
 import org.apache.james.utils.DataProbeImpl
-import org.assertj.core.api.Assertions.assertThat
-import org.awaitility.Awaitility
-import org.java_websocket.client.WebSocketClient
-import org.java_websocket.handshake.ServerHandshake
-import org.junit.jupiter.api.{BeforeEach, Tag, Test}
-import org.slf4j.{Logger, LoggerFactory}
-
-object WebSocketContract {
-  val LOGGER: Logger = LoggerFactory.getLogger(classOf[WebSocketContract])
-
-  val await = Awaitility.await
-    .atMost(1, TimeUnit.SECONDS)
-    .pollInterval(100, TimeUnit.MILLISECONDS)
-}
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.junit.jupiter.api.{BeforeEach, Test}
+import sttp.capabilities.WebSockets
+import sttp.client3.monad.IdMonad
+import sttp.client3.okhttp.OkHttpSyncBackend
+import sttp.client3.{Identity, RequestT, SttpBackend, asWebSocket, basicRequest}
+import sttp.model.Uri
+import sttp.monad.MonadError
+import sttp.monad.syntax.MonadErrorOps
+import sttp.ws.WebSocketFrame
+import sttp.ws.WebSocketFrame.Text
 
 trait WebSocketContract {
+  private lazy val backend: SttpBackend[Identity, WebSockets] = OkHttpSyncBackend()
+  private lazy implicit val monadError: MonadError[Identity] = IdMonad
 
   @BeforeEach
   def setUp(server: GuiceJamesServer): Unit = {
@@ -54,264 +49,335 @@ trait WebSocketContract {
       .addUser(BOB.asString(), BOB_PASSWORD)
   }
 
-  class TestClient(uri: URI) extends WebSocketClient(uri) {
-    val receivedResponses: util.LinkedList[String] = new util.LinkedList[String]()
-    var closeString: Option[String] = None
-
-    override def onOpen(serverHandshake: ServerHandshake): Unit = {}
-
-    override def onMessage(s: String): Unit = receivedResponses.add(s)
-
-    override def onClose(i: Int, s: String, b: Boolean): Unit = closeString = Some(s)
-
-    override def onError(e: Exception): Unit = LOGGER.error("WebSocket error", e)
-  }
-
   @Test
-  @Tag(CategoryTags.BASIC_FEATURE)
   def apiRequestsShouldBeProcessed(server: GuiceJamesServer): Unit = {
-    val client: TestClient = authenticatedWebSocketClient(server)
-    client.connectBlocking()
-    client.send("""{
-                  |  "@type": "Request",
-                  |  "requestId": "req-36",
-                  |  "using": [ "urn:ietf:params:jmap:core"],
-                  |  "methodCalls": [
-                  |    [
-                  |      "Core/echo",
-                  |      {
-                  |        "arg1": "arg1data",
-                  |        "arg2": "arg2data"
-                  |      },
-                  |      "c1"
-                  |    ]
-                  |  ]
-                  |}""".stripMargin)
-
-    await.until(() => client.receivedResponses.size() == 1)
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo("""{
-        |  "@type":"Response",
-        |  "requestId":"req-36",
-        |  "sessionState":"2c9f1b12-b35a-43e6-9af2-0106fb53a943",
-        |  "methodResponses":[["Core/echo",{"arg1":"arg1data","arg2":"arg2data"},"c1"]]
-        |}""".stripMargin)
+    val response: Either[String, String] =
+      authenticatedRequest(server)
+        .response(asWebSocket[Identity, String] {
+          ws =>
+            ws.send(WebSocketFrame.text(
+              """{
+                |  "@type": "Request",
+                |  "requestId": "req-36",
+                |  "using": [ "urn:ietf:params:jmap:core"],
+                |  "methodCalls": [
+                |    [
+                |      "Core/echo",
+                |      {
+                |        "arg1": "arg1data",
+                |        "arg2": "arg2data"
+                |      },
+                |      "c1"
+                |    ]
+                |  ]
+                |}""".stripMargin))
+
+            ws.receive()
+              .map { case t: Text => t.payload }
+        })
+        .send(backend)
+        .body
+
+    assertThatJson(response.toOption.get)
+      .isEqualTo("""{
+                   |  "@type":"Response",
+                   |  "requestId":"req-36",
+                   |  "sessionState":"2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+                   |  "methodResponses":[
+                   |    ["Core/echo",
+                   |      {
+                   |        "arg1":"arg1data",
+                   |        "arg2":"arg2data"
+                   |      },"c1"]
+                   |  ]
+                   |}
+                   |""".stripMargin)
   }
 
   @Test
   def apiRequestsShouldBeProcessedWhenNoRequestId(server: GuiceJamesServer): Unit = {
-    val client: TestClient = authenticatedWebSocketClient(server)
-    client.connectBlocking()
-    client.send("""{
-                  |  "@type": "Request",
-                  |  "using": [ "urn:ietf:params:jmap:core"],
-                  |  "methodCalls": [
-                  |    [
-                  |      "Core/echo",
-                  |      {
-                  |        "arg1": "arg1data",
-                  |        "arg2": "arg2data"
-                  |      },
-                  |      "c1"
-                  |    ]
-                  |  ]
-                  |}""".stripMargin)
-
-    await.untilAsserted(() => assertThat(client.receivedResponses).hasSize(1))
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo("""{
-        |  "@type":"Response",
-        |  "requestId":null,
-        |  "sessionState":"2c9f1b12-b35a-43e6-9af2-0106fb53a943",
-        |  "methodResponses":[["Core/echo",{"arg1":"arg1data","arg2":"arg2data"},"c1"]]
-        |}""".stripMargin)
+    val response: Either[String, String] =
+      authenticatedRequest(server)
+        .response(asWebSocket[Identity, String] {
+          ws =>
+            ws.send(WebSocketFrame.text(
+              """{
+                |  "@type": "Request",
+                |  "using": [ "urn:ietf:params:jmap:core"],
+                |  "methodCalls": [
+                |    [
+                |      "Core/echo",
+                |      {
+                |        "arg1": "arg1data",
+                |        "arg2": "arg2data"
+                |      },
+                |      "c1"
+                |    ]
+                |  ]
+                |}""".stripMargin))
+
+            ws.receive()
+              .map { case t: Text => t.payload }
+        })
+        .send(backend)
+        .body
+
+    assertThatJson(response.toOption.get)
+      .isEqualTo("""{
+                   |  "@type":"Response",
+                   |  "requestId":null,
+                   |  "sessionState":"2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+                   |  "methodResponses":[["Core/echo",{"arg1":"arg1data","arg2":"arg2data"},"c1"]]
+                   |}""".stripMargin)
   }
 
   @Test
   def nonJsonPayloadShouldTriggerError(server: GuiceJamesServer): Unit = {
-    val client: TestClient = authenticatedWebSocketClient(server)
-    client.connectBlocking()
-    client.send("The quick brown fox".stripMargin)
-
-    await.untilAsserted(() => assertThat(client.receivedResponses).hasSize(1))
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo("""{
-        |  "status":400,
-        |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Unrecognized token 'The': was expecting ('true', 'false' or 'null')\n at [Source: (String)\"The quick brown fox\"; line: 1, column: 4]),ArraySeq()))))",
-        |  "type":"urn:ietf:params:jmap:error:notRequest",
-        |  "requestId":null,
-        |  "@type":"RequestError"
-        |}""".stripMargin)
+    val response: Either[String, String] =
+      authenticatedRequest(server)
+        .response(asWebSocket[Identity, String] {
+          ws =>
+            ws.send(WebSocketFrame.text("The quick brown fox"))
+
+            ws.receive()
+              .map { case t: Text => t.payload }
+        })
+        .send(backend)
+        .body
+
+    assertThatJson(response.toOption.get)
+      .isEqualTo("""{
+                   |  "status":400,
+                   |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Unrecognized token 'The': was expecting ('true', 'false' or 'null')\n at [Source: (String)\"The quick brown fox\"; line: 1, column: 4]),ArraySeq()))))",
+                   |  "type":"urn:ietf:params:jmap:error:notRequest",
+                   |  "requestId":null,
+                   |  "@type":"RequestError"
+                   |}""".stripMargin)
   }
 
   @Test
   def handshakeShouldBeAuthenticated(server: GuiceJamesServer): Unit = {
-    val client: TestClient = unauthenticatedWebSocketClient(server)
-    client.connectBlocking()
-
-    assertThat(client.isClosed).isTrue
-    assertThat(client.closeString).isEqualTo(Some("Invalid status code received: 401 Status line: HTTP/1.1 401 Unauthorized"))
+    assertThatThrownBy(() =>
+      unauthenticatedRequest(server)
+        .response(asWebSocket[Identity, String] {
+          ws =>
+            ws.send(WebSocketFrame.text("The quick brown fox"))
+
+          ws.receive()
+            .map { case t: Text => t.toString }
+      })
+      .send(backend)
+      .body)
+      .hasRootCause(new ProtocolException("Expected HTTP 101 response but was '401 Unauthorized'"))
   }
 
   @Test
-  def noTypeFiledShouldTriggerError(server: GuiceJamesServer): Unit = {
-    val client: TestClient = authenticatedWebSocketClient(server)
-    client.connectBlocking()
-    client.send("""{
-                  |  "requestId": "req-36",
-                  |  "using": [ "urn:ietf:params:jmap:core"],
-                  |  "methodCalls": [
-                  |    [
-                  |      "Core/echo",
-                  |      {
-                  |        "arg1": "arg1data",
-                  |        "arg2": "arg2data"
-                  |      },
-                  |      "c1"
-                  |    ]
-                  |  ]
-                  |}""".stripMargin)
-
-    await.until(() => client.receivedResponses.size() == 1)
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo("""{
-        |  "status":400,
-        |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Missing @type filed on a webSocket inbound message),ArraySeq()))))",
-        |  "type":"urn:ietf:params:jmap:error:notRequest",
-        |  "requestId":null,
-        |  "@type":"RequestError"
-        |}""".stripMargin)
+  def noTypeFieldShouldTriggerError(server: GuiceJamesServer): Unit = {
+    val response: Either[String, String] =
+      authenticatedRequest(server)
+        .response(asWebSocket[Identity, String] {
+          ws =>
+            ws.send(WebSocketFrame.text(
+              """{
+                |  "requestId": "req-36",
+                |  "using": [ "urn:ietf:params:jmap:core"],
+                |  "methodCalls": [
+                |    [
+                |      "Core/echo",
+                |      {
+                |        "arg1": "arg1data",
+                |        "arg2": "arg2data"
+                |      },
+                |      "c1"
+                |    ]
+                |  ]
+                |}""".stripMargin))
+
+            ws.receive()
+              .map { case t: Text => t.payload }
+        })
+        .send(backend)
+        .body
+
+    assertThatJson(response.toOption.get)
+      .isEqualTo("""{
+                   |  "status":400,
+                   |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Missing @type field on a webSocket inbound message),ArraySeq()))))",
+                   |  "type":"urn:ietf:params:jmap:error:notRequest",
+                   |  "requestId":null,
+                   |  "@type":"RequestError"
+                   |}""".stripMargin)
   }
 
   @Test
   def badTypeFieldShouldTriggerError(server: GuiceJamesServer): Unit = {
-    val client: TestClient = authenticatedWebSocketClient(server)
-    client.connectBlocking()
-    client.send("""{
-                  |  "@type": 42,
-                  |  "requestId": "req-36",
-                  |  "using": [ "urn:ietf:params:jmap:core"],
-                  |  "methodCalls": [
-                  |    [
-                  |      "Core/echo",
-                  |      {
-                  |        "arg1": "arg1data",
-                  |        "arg2": "arg2data"
-                  |      },
-                  |      "c1"
-                  |    ]
-                  |  ]
-                  |}""".stripMargin)
-
-    await.untilAsserted(() => assertThat(client.receivedResponses).hasSize(1))
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
-      """{
-        |  "status":400,
-        |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Invalid @type filed on a webSocket inbound message: expecting a JsString, got 42),ArraySeq()))))",
-        |  "type":"urn:ietf:params:jmap:error:notRequest",
-        |  "requestId":null,
-        |  "@type":"RequestError"
-        |}""".stripMargin)
+    val response: Either[String, String] =
+      authenticatedRequest(server)
+        .response(asWebSocket[Identity, String] {
+          ws =>
+            ws.send(WebSocketFrame.text(
+              """{
+                |  "@type": 42,
+                |  "requestId": "req-36",
+                |  "using": [ "urn:ietf:params:jmap:core"],
+                |  "methodCalls": [
+                |    [
+                |      "Core/echo",
+                |      {
+                |        "arg1": "arg1data",
+                |        "arg2": "arg2data"
+                |      },
+                |      "c1"
+                |    ]
+                |  ]
+                |}""".stripMargin))
+
+            ws.receive()
+              .map { case t: Text => t.payload }
+        })
+        .send(backend)
+        .body
+
+    assertThatJson(response.toOption.get)
+      .isEqualTo("""{
+                   |  "status":400,
+                   |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Invalid @type field on a webSocket inbound message: expecting a JsString, got 42),ArraySeq()))))",
+                   |  "type":"urn:ietf:params:jmap:error:notRequest",
+                   |  "requestId":null,
+                   |  "@type":"RequestError"
+                   |}""".stripMargin)
   }
 
   @Test
   def unknownTypeFieldShouldTriggerError(server: GuiceJamesServer): Unit = {
-    val client: TestClient = authenticatedWebSocketClient(server)
-    client.connectBlocking()
-    client.send(
-      """{
-        |  "@type": "unknown",
-        |  "requestId": "req-36",
-        |  "using": [ "urn:ietf:params:jmap:core"],
-        |  "methodCalls": [
-        |    [
-        |      "Core/echo",
-        |      {
-        |        "arg1": "arg1data",
-        |        "arg2": "arg2data"
-        |      },
-        |      "c1"
-        |    ]
-        |  ]
-        |}""".stripMargin)
-
-    await.untilAsserted(() => assertThat(client.receivedResponses).hasSize(1))
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo("""{
-        |  "status":400,
-        |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Unknown @type filed on a webSocket inbound message: unknown),ArraySeq()))))",
-        |  "type":"urn:ietf:params:jmap:error:notRequest",
-        |  "requestId":null,
-        |  "@type":"RequestError"
-        |}""".stripMargin)
+    val response: Either[String, String] =
+      authenticatedRequest(server)
+        .response(asWebSocket[Identity, String] {
+          ws =>
+            ws.send(WebSocketFrame.text(
+              """{
+                |  "@type": "unknown",
+                |  "requestId": "req-36",
+                |  "using": [ "urn:ietf:params:jmap:core"],
+                |  "methodCalls": [
+                |    [
+                |      "Core/echo",
+                |      {
+                |        "arg1": "arg1data",
+                |        "arg2": "arg2data"
+                |      },
+                |      "c1"
+                |    ]
+                |  ]
+                |}""".stripMargin))
+
+            ws.receive()
+              .map { case t: Text => t.payload }
+        })
+        .send(backend)
+        .body
+
+    assertThatJson(response.toOption.get)
+      .isEqualTo("""{
+                   |  "status":400,
+                   |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Unknown @type field on a webSocket inbound message: unknown),ArraySeq()))))",
+                   |  "type":"urn:ietf:params:jmap:error:notRequest",
+                   |  "requestId":null,
+                   |  "@type":"RequestError"
+                   |}""".stripMargin)
   }
 
-
   @Test
   def clientSendingARespondTypeFieldShouldTriggerError(server: GuiceJamesServer): Unit = {
-    val client: TestClient = authenticatedWebSocketClient(server)
-    client.connectBlocking()
-    client.send(
-      """{
-        |  "@type": "Response",
-        |  "requestId": "req-36",
-        |  "using": [ "urn:ietf:params:jmap:core"],
-        |  "methodCalls": [
-        |    [
-        |      "Core/echo",
-        |      {
-        |        "arg1": "arg1data",
-        |        "arg2": "arg2data"
-        |      },
-        |      "c1"
-        |    ]
-        |  ]
-        |}""".stripMargin)
-
-    await.untilAsserted(() => assertThat(client.receivedResponses).hasSize(1))
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo("""{
-        |  "status":400,
-        |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Unknown @type filed on a webSocket inbound message: Response),ArraySeq()))))",
-        |  "type":"urn:ietf:params:jmap:error:notRequest",
-        |  "requestId":null,
-        |  "@type":"RequestError"
-        |}""".stripMargin)
+    val response: Either[String, String] =
+      authenticatedRequest(server)
+        .response(asWebSocket[Identity, String] {
+          ws =>
+            ws.send(WebSocketFrame.text(
+              """{
+                |  "@type": "Response",
+                |  "requestId": "req-36",
+                |  "using": [ "urn:ietf:params:jmap:core"],
+                |  "methodCalls": [
+                |    [
+                |      "Core/echo",
+                |      {
+                |        "arg1": "arg1data",
+                |        "arg2": "arg2data"
+                |      },
+                |      "c1"
+                |    ]
+                |  ]
+                |}""".stripMargin))
+
+            ws.receive()
+              .map { case t: Text => t.payload }
+        })
+        .send(backend)
+        .body
+
+    assertThatJson(response.toOption.get)
+      .isEqualTo("""{
+                   |  "status":400,
+                   |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Unknown @type field on a webSocket inbound message: Response),ArraySeq()))))",
+                   |  "type":"urn:ietf:params:jmap:error:notRequest",
+                   |  "requestId":null,
+                   |  "@type":"RequestError"
+                   |}""".stripMargin)
   }
 
   @Test
   def requestLevelErrorShouldReturnAPIError(server: GuiceJamesServer): Unit = {
-    val client: TestClient = authenticatedWebSocketClient(server)
-    client.connectBlocking()
-    client.send(s"""{
-                   |  "@type": "Request",
-                   |  "using": [
-                   |    "urn:ietf:params:jmap:core",
-                   |    "urn:ietf:params:jmap:mail"],
-                   |  "methodCalls": [[
-                   |      "Mailbox/get",
-                   |      {
-                   |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-                   |        "properties": ["invalidProperty"]
-                   |      },
-                   |      "c1"]]
+    val response: Either[String, String] =
+      authenticatedRequest(server)
+        .response(asWebSocket[Identity, String] {
+          ws =>
+            ws.send(WebSocketFrame.text(
+              """{
+                |  "@type": "Request",
+                |  "using": [
+                |    "urn:ietf:params:jmap:core",
+                |    "urn:ietf:params:jmap:mail"],
+                |  "methodCalls": [[
+                |      "Mailbox/get",
+                |      {
+                |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+                |        "properties": ["invalidProperty"]
+                |      },
+                |      "c1"]]
+                |}""".stripMargin))
+
+            ws.receive()
+              .map { case t: Text => t.payload }
+        })
+        .send(backend)
+        .body
+
+    assertThatJson(response.toOption.get)
+      .isEqualTo("""{
+                   |  "@type": "Response",
+                   |  "requestId": null,
+                   |  "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+                   |  "methodResponses": [["error",{"type":"invalidArguments","description":"The following properties [invalidProperty] do not exist."},"c1"]]
                    |}""".stripMargin)
-
-    await.untilAsserted(() => assertThat(client.receivedResponses).hasSize(1))
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo("""{
-        |  "@type": "Response",
-        |  "requestId": null,
-        |  "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
-        |  "methodResponses": [["error",{"type":"invalidArguments","description":"The following properties [invalidProperty] do not exist."},"c1"]]
-        |}""".stripMargin)
   }
 
-  private def unauthenticatedWebSocketClient(server: GuiceJamesServer): TestClient = {
+  private def authenticatedRequest(server: GuiceJamesServer): RequestT[Identity, Either[String, String], Any] = {
     val port = server.getProbe(classOf[JmapGuiceProbe])
       .getJmapPort
       .getValue
-    val client = new TestClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
-    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
-    client
+
+    basicRequest.get(Uri.apply(new URI(s"ws://127.0.0.1:$port/jmap/ws")))
+      .header("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
+      .header("Accept", ACCEPT_RFC8621_VERSION_HEADER)
   }
 
-  private def authenticatedWebSocketClient(server: GuiceJamesServer): TestClient = {
-    val client = unauthenticatedWebSocketClient(server)
-    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
-    client
+  private def unauthenticatedRequest(server: GuiceJamesServer): RequestT[Identity, Either[String, String], Any] = {
+    val port = server.getProbe(classOf[JmapGuiceProbe])
+      .getJmapPort
+      .getValue
+
+    basicRequest.get(Uri.apply(new URI(s"ws://127.0.0.1:$port/jmap/ws")))
+      .header("Accept", ACCEPT_RFC8621_VERSION_HEADER)
   }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
index ebf7eb4..71e04f7 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
@@ -19,6 +19,9 @@
 
 package org.apache.james.jmap.json
 
+import java.io.InputStream
+import java.net.URL
+
 import eu.timepit.refined.refineV
 import io.netty.handler.codec.http.HttpResponseStatus
 import org.apache.james.core.Username
@@ -31,8 +34,6 @@ import org.apache.james.jmap.core.{Account, Invocation, Session, _}
 import play.api.libs.functional.syntax._
 import play.api.libs.json._
 
-import java.io.InputStream
-import java.net.URL
 import scala.collection.{Seq => LegacySeq}
 import scala.language.implicitConversions
 import scala.util.Try
@@ -188,9 +189,9 @@ object ResponseSerializer {
     case json: JsObject =>
       json.value.get("@type") match {
         case Some(JsString("Request")) => webSocketRequestReads.reads(json)
-        case Some(JsString(unknownType)) => JsError(s"Unknown @type filed on a webSocket inbound message: $unknownType")
-        case Some(invalidType) => JsError(s"Invalid @type filed on a webSocket inbound message: expecting a JsString, got $invalidType")
-        case None => JsError(s"Missing @type filed on a webSocket inbound message")
+        case Some(JsString(unknownType)) => JsError(s"Unknown @type field on a webSocket inbound message: $unknownType")
+        case Some(invalidType) => JsError(s"Invalid @type field on a webSocket inbound message: expecting a JsString, got $invalidType")
+        case None => JsError(s"Missing @type field on a webSocket inbound message")
       }
     case _ => JsError("Expecting a JsObject to represent a webSocket inbound message")
   }


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


[james-project] 03/12: JAMES-3491 JMAP WebSocket transport JSON serialization

Posted by bt...@apache.org.
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

commit fe23fea43f3dd556b186e320f041f9e32aa06a2d
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Jan 28 10:47:54 2021 +0700

    JAMES-3491 JMAP WebSocket transport JSON serialization
---
 .../james/jmap/json/ResponseSerializer.scala       | 58 ++++++++++++++++++++--
 1 file changed, 53 insertions(+), 5 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
index 8bdbe4a..edace41 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
@@ -19,9 +19,6 @@
 
 package org.apache.james.jmap.json
 
-import java.io.InputStream
-import java.net.URL
-
 import eu.timepit.refined.refineV
 import io.netty.handler.codec.http.HttpResponseStatus
 import org.apache.james.core.Username
@@ -34,8 +31,11 @@ import org.apache.james.jmap.core.{Account, Invocation, Session, _}
 import play.api.libs.functional.syntax._
 import play.api.libs.json._
 
+import java.io.InputStream
+import java.net.URL
 import scala.collection.{Seq => LegacySeq}
 import scala.language.implicitConversions
+import scala.util.Try
 
 object ResponseSerializer {
   // CreateIds
@@ -68,7 +68,7 @@ object ResponseSerializer {
 
   private implicit val stateWrites: Writes[State] = Json.valueWrites[State]
   // ResponseObject
-  private implicit val responseObjectFormat: Format[ResponseObject] = Json.format[ResponseObject]
+  private implicit val responseObjectFormat: OFormat[ResponseObject] = Json.format[ResponseObject]
 
   private implicit val maxSizeUploadWrites: Writes[MaxSizeUpload] = Json.valueWrites[MaxSizeUpload]
   private implicit val maxConcurrentUploadWrites: Writes[MaxConcurrentUpload] = Json.valueWrites[MaxConcurrentUpload]
@@ -163,7 +163,45 @@ object ResponseSerializer {
 
   private implicit val jsErrorWrites: Writes[JsError] = Json.writes[JsError]
 
-  private implicit val problemDetailsWrites: Writes[ProblemDetails] = Json.writes[ProblemDetails]
+  private implicit val problemDetailsWrites: OWrites[ProblemDetails] = Json.writes[ProblemDetails]
+
+  private implicit val requestIdFormat: Format[RequestId] = Json.valueFormat[RequestId]
+  private implicit val webSocketRequestReads: Reads[WebSocketRequest] = {
+    case jsObject: JsObject =>
+      for {
+        requestId <- jsObject.value.get("requestId")
+          .map(requestIdJson => requestIdFormat.reads(requestIdJson).map(Some(_)))
+          .getOrElse(JsSuccess(None))
+        request <- requestObjectRead.reads(jsObject)
+      } yield {
+        WebSocketRequest(requestId, request)
+      }
+    case _ => JsError("Expecting a JsObject to represent a webSocket inbound request")
+  }
+  private implicit val webSocketInboundReads: Reads[WebSocketInboundMessage] = {
+    case json: JsObject =>
+      json.value.get("@type") match {
+        case Some(JsString("Request")) => webSocketRequestReads.reads(json)
+        case Some(JsString(unknownType)) => JsError(s"Unknown @type filed on a webSocket inbound message: $unknownType")
+        case Some(invalidType) => JsError(s"Invalid @type filed on a webSocket inbound message: expecting a JsString, got $invalidType")
+        case None => JsError(s"Missing @type filed on a webSocket inbound message")
+      }
+    case _ => JsError("Expecting a JsObject to represent a webSocket inbound message")
+  }
+  private implicit val webSocketResponseWrites: Writes[WebSocketResponse] = response => {
+    val apiResponseJson: JsObject = responseObjectFormat.writes(response.responseObject)
+    JsObject(Map(
+      "@type" -> JsString("Response"),
+      "requestId" -> response.requestId.map(_.value).map(JsString).getOrElse(JsNull))
+      ++ apiResponseJson.value)
+  }
+  private implicit val webSocketErrorWrites: Writes[WebSocketError] = error => {
+    val errorJson: JsObject = problemDetailsWrites.writes(error.problemDetails)
+    JsObject(Map(
+      "@type" -> JsString("RequestError"),
+      "requestId" -> error.requestId.map(_.value).map(JsString).getOrElse(JsNull))
+      ++ errorJson.value)
+  }
 
   def serialize(session: Session): JsValue = Json.toJson(session)
 
@@ -175,8 +213,18 @@ object ResponseSerializer {
 
   def serialize(errors: JsError): JsValue = Json.toJson(errors)
 
+  def serialize(response: WebSocketOutboundMessage): JsValue = {
+    case response: WebSocketResponse => Json.toJson(response)
+    case error: WebSocketError => Json.toJson(error)
+  }
+
+  def serialize(errors: WebSocketError): JsValue = Json.toJson(errors)
+
   def deserializeRequestObject(input: String): JsResult[RequestObject] = Json.parse(input).validate[RequestObject]
 
+  def deserializeWebSocketInboundMessage(input: String): JsResult[WebSocketInboundMessage] = Try(Json.parse(input).validate[WebSocketInboundMessage])
+    .fold(e => JsError(e.getMessage), result => result)
+
   def deserializeRequestObject(input: InputStream): JsResult[RequestObject] = Json.parse(input).validate[RequestObject]
 
   def deserializeResponseObject(input: String): JsResult[ResponseObject] = Json.parse(input).validate[ResponseObject]


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


[james-project] 10/12: JAMES-3495 Better factorize error handling between HTTP and WebSocket transport

Posted by bt...@apache.org.
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

commit 8401dd43c84823cfc79c61ac07b7f05d93221e5d
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Jan 29 10:33:56 2021 +0700

    JAMES-3495 Better factorize error handling between HTTP and WebSocket transport
---
 .../apache/james/jmap/core/ProblemDetails.scala    | 25 ++++++++++-
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   | 35 +++-------------
 .../apache/james/jmap/routes/WebSocketRoutes.scala | 48 +++++-----------------
 3 files changed, 41 insertions(+), 67 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/ProblemDetails.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/ProblemDetails.scala
index 9547f00..85c223d 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/ProblemDetails.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/ProblemDetails.scala
@@ -18,9 +18,13 @@
  ****************************************************************/
 package org.apache.james.jmap.core
 
+import com.fasterxml.jackson.core.JsonParseException
 import io.netty.handler.codec.http.HttpResponseStatus
-import io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST
+import io.netty.handler.codec.http.HttpResponseStatus.{BAD_REQUEST, INTERNAL_SERVER_ERROR, UNAUTHORIZED}
 import org.apache.james.jmap.core.RequestLevelErrorType.{DEFAULT_ERROR_TYPE, ErrorTypeIdentifier}
+import org.apache.james.jmap.exceptions.UnauthorizedException
+import org.apache.james.jmap.routes.UnsupportedCapabilitiesException
+import org.slf4j.{Logger, LoggerFactory}
 
 /**
  * Problem Details for HTTP APIs within the JMAP context
@@ -33,6 +37,25 @@ case class ProblemDetails(`type`: ErrorTypeIdentifier = DEFAULT_ERROR_TYPE,
                           detail: String)
 
 object ProblemDetails {
+  val LOGGER: Logger = LoggerFactory.getLogger(classOf[ProblemDetails])
+
+  def forThrowable(throwable: Throwable): ProblemDetails = throwable match {
+    case exception: IllegalArgumentException =>
+      notRequestProblem(
+        s"The request was successfully parsed as JSON but did not match the type signature of the Request object: ${exception.getMessage}")
+    case e: UnauthorizedException =>
+      LOGGER.warn("Unauthorized", e)
+      ProblemDetails(status = UNAUTHORIZED, detail = e.getMessage)
+    case exception: JsonParseException =>
+      notJSONProblem(
+        s"The content type of the request was not application/json or the request did not parse as I-JSON: ${exception.getMessage}")
+    case exception: UnsupportedCapabilitiesException =>
+      unknownCapabilityProblem(s"The request used unsupported capabilities: ${exception.capabilities}")
+    case e =>
+      LOGGER.error("Unexpected error upon API request", e)
+      ProblemDetails(status = INTERNAL_SERVER_ERROR, detail = e.getMessage)
+  }
+
   def notRequestProblem(message: String): ProblemDetails = ProblemDetails(RequestLevelErrorType.NOT_REQUEST, BAD_REQUEST, None, message)
   def notJSONProblem(message: String): ProblemDetails = ProblemDetails(RequestLevelErrorType.NOT_JSON, BAD_REQUEST, None, message)
   def unknownCapabilityProblem(message: String): ProblemDetails = ProblemDetails(RequestLevelErrorType.UNKNOWN_CAPABILITY, BAD_REQUEST, None, message)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
index 2bc48bb..a09b4cc 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
@@ -23,21 +23,17 @@ import java.nio.charset.StandardCharsets
 import java.util.stream
 import java.util.stream.Stream
 
-import com.fasterxml.jackson.core.JsonParseException
 import io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE
-import io.netty.handler.codec.http.HttpResponseStatus.{BAD_REQUEST, INTERNAL_SERVER_ERROR, OK, UNAUTHORIZED}
-import io.netty.handler.codec.http.{HttpMethod, HttpResponseStatus}
+import io.netty.handler.codec.http.HttpMethod
+import io.netty.handler.codec.http.HttpResponseStatus.OK
 import javax.inject.{Inject, Named}
 import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
 import org.apache.james.jmap.JMAPUrls.JMAP
 import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
-import org.apache.james.jmap.core.ProblemDetails.{notJSONProblem, notRequestProblem, unknownCapabilityProblem}
 import org.apache.james.jmap.core.{ProblemDetails, RequestObject}
-import org.apache.james.jmap.exceptions.UnauthorizedException
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.http.{Authenticator, UserProvisioning}
 import org.apache.james.jmap.json.ResponseSerializer
-import org.apache.james.jmap.routes.DownloadRoutes.LOGGER
 import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
 import org.apache.james.mailbox.MailboxSession
 import org.slf4j.{Logger, LoggerFactory}
@@ -104,30 +100,11 @@ class JMAPApiRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticator:
           StandardCharsets.UTF_8)
         .`then`()))
 
-  private def handleError(throwable: Throwable, response: HttpServerResponse): SMono[Void] = throwable match {
-    case exception: IllegalArgumentException => respondDetails(response,
-      notRequestProblem(
-        s"The request was successfully parsed as JSON but did not match the type signature of the Request object: ${exception.getMessage}"))
+  private def handleError(throwable: Throwable, response: HttpServerResponse): SMono[Void] =
+    respondDetails(response, ProblemDetails.forThrowable(throwable))
 
-    case e: UnauthorizedException =>
-      LOGGER.warn("Unauthorized", e)
-      respondDetails(response,
-        ProblemDetails(status = UNAUTHORIZED, detail = e.getMessage),
-        UNAUTHORIZED)
-    case exception: JsonParseException => respondDetails(response,
-      notJSONProblem(
-        s"The content type of the request was not application/json or the request did not parse as I-JSON: ${exception.getMessage}"))
-    case exception: UnsupportedCapabilitiesException => respondDetails(response,
-      unknownCapabilityProblem(s"The request used unsupported capabilities: ${exception.capabilities}"))
-    case e =>
-      LOGGER.error("Unexpected error upon API request", e)
-      respondDetails(response,
-        ProblemDetails(status = INTERNAL_SERVER_ERROR, detail = e.getMessage),
-        INTERNAL_SERVER_ERROR)
-  }
-
-  private def respondDetails(httpServerResponse: HttpServerResponse, details: ProblemDetails, statusCode: HttpResponseStatus = BAD_REQUEST): SMono[Void] =
-    SMono.fromPublisher(httpServerResponse.status(statusCode)
+  private def respondDetails(httpServerResponse: HttpServerResponse, details: ProblemDetails): SMono[Void] =
+    SMono.fromPublisher(httpServerResponse.status(details.status)
       .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
       .sendString(SMono.fromCallable(() => ResponseSerializer.serialize(details).toString),
         StandardCharsets.UTF_8)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala
index c4b2900..3965ac7 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala
@@ -19,20 +19,19 @@
 
 package org.apache.james.jmap.routes
 
-import com.fasterxml.jackson.core.JsonParseException
+import java.nio.charset.StandardCharsets
+import java.util.stream
+
 import io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE
-import io.netty.handler.codec.http.HttpResponseStatus.{BAD_REQUEST, INTERNAL_SERVER_ERROR, UNAUTHORIZED}
+import io.netty.handler.codec.http.HttpMethod
 import io.netty.handler.codec.http.websocketx.WebSocketFrame
-import io.netty.handler.codec.http.{HttpMethod, HttpResponseStatus}
+import javax.inject.{Inject, Named}
 import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
 import org.apache.james.jmap.JMAPUrls.JMAP_WS
-import org.apache.james.jmap.core.ProblemDetails.{notJSONProblem, notRequestProblem, unknownCapabilityProblem}
 import org.apache.james.jmap.core.{ProblemDetails, RequestId, WebSocketError, WebSocketOutboundMessage, WebSocketRequest, WebSocketResponse}
-import org.apache.james.jmap.exceptions.UnauthorizedException
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.http.{Authenticator, UserProvisioning}
 import org.apache.james.jmap.json.ResponseSerializer
-import org.apache.james.jmap.routes.WebSocketRoutes.LOGGER
 import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
 import org.apache.james.mailbox.MailboxSession
 import org.slf4j.{Logger, LoggerFactory}
@@ -42,10 +41,6 @@ import reactor.core.scheduler.Schedulers
 import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
 import reactor.netty.http.websocket.{WebsocketInbound, WebsocketOutbound}
 
-import java.nio.charset.StandardCharsets
-import java.util.stream
-import javax.inject.{Inject, Named}
-
 object WebSocketRoutes {
   val LOGGER: Logger = LoggerFactory.getLogger(classOf[WebSocketRoutes])
 }
@@ -109,35 +104,14 @@ class WebSocketRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticato
               .subscribeOn(Schedulers.elastic)
         })
 
-  private def handleHttpHandshakeError(throwable: Throwable, response: HttpServerResponse): SMono[Void] = throwable match {
-    case e: UnauthorizedException =>
-      LOGGER.warn("Unauthorized", e)
-      respondDetails(response,
-        ProblemDetails(status = UNAUTHORIZED, detail = e.getMessage),
-        UNAUTHORIZED)
-    case e =>
-      LOGGER.error("Unexpected error upon WebSocket handshake request", e)
-      respondDetails(response,
-        ProblemDetails(status = INTERNAL_SERVER_ERROR, detail = e.getMessage),
-        INTERNAL_SERVER_ERROR)
-  }
+  private def handleHttpHandshakeError(throwable: Throwable, response: HttpServerResponse): SMono[Void] =
+    respondDetails(response, ProblemDetails.forThrowable(throwable))
 
-  private def asError(requestId: Option[RequestId])(throwable: Throwable): WebSocketError = throwable match {
-    case exception: IllegalArgumentException =>
-      WebSocketError(requestId, notRequestProblem(
-        s"The request was successfully parsed as JSON but did not match the type signature of the Request object: ${exception.getMessage}"))
-    case exception: JsonParseException =>
-      WebSocketError(requestId, notJSONProblem(
-        s"The content type of the request was not application/json or the request did not parse as I-JSON: ${exception.getMessage}"))
-    case exception: UnsupportedCapabilitiesException =>
-      WebSocketError(requestId, unknownCapabilityProblem(s"The request used unsupported capabilities: ${exception.capabilities}"))
-    case e =>
-      LOGGER.error("Unexpected error upon API request", e)
-      WebSocketError(requestId, ProblemDetails(status = INTERNAL_SERVER_ERROR, detail = e.getMessage))
-  }
+  private def asError(requestId: Option[RequestId])(throwable: Throwable): WebSocketError =
+    WebSocketError(requestId, ProblemDetails.forThrowable(throwable))
 
-  private def respondDetails(httpServerResponse: HttpServerResponse, details: ProblemDetails, statusCode: HttpResponseStatus = BAD_REQUEST): SMono[Void] =
-    SMono.fromPublisher(httpServerResponse.status(statusCode)
+  private def respondDetails(httpServerResponse: HttpServerResponse, details: ProblemDetails): SMono[Void] =
+    SMono.fromPublisher(httpServerResponse.status(details.status)
       .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
       .sendString(SMono.fromCallable(() => ResponseSerializer.serialize(details).toString),
         StandardCharsets.UTF_8)


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


[james-project] 01/12: JAMES-3491 MemoryUploadContract => MemoryUploadTest

Posted by bt...@apache.org.
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

commit 7fb867f9d8785393dca7a1c7a2f4ef120345c011
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Jan 28 13:28:00 2021 +0700

    JAMES-3491 MemoryUploadContract => MemoryUploadTest
---
 .../rfc8621/memory/{MemoryUploadContract.java => MemoryUploadTest.java} | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryUploadContract.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryUploadTest.java
similarity index 96%
rename from server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryUploadContract.java
rename to server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryUploadTest.java
index 9f55acd..fe745f5 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryUploadContract.java
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryUploadTest.java
@@ -28,7 +28,7 @@ import org.apache.james.jmap.rfc8621.contract.UploadContract;
 import org.apache.james.modules.TestJMAPServerModule;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
-public class MemoryUploadContract implements UploadContract {
+public class MemoryUploadTest implements UploadContract {
     @RegisterExtension
     static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
         .server(configuration -> GuiceJamesServer.forConfiguration(configuration)


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


[james-project] 07/12: JAMES-3491 JMAP WebSocket transport test contract skeleton

Posted by bt...@apache.org.
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

commit 29d6ef00a17a2c34d2d931de861d2b66ebad4768
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Jan 28 13:38:56 2021 +0700

    JAMES-3491 JMAP WebSocket transport test contract skeleton
---
 .../distributed/DistributedWebSocketTest.java      | 53 ++++++++++++++
 .../jmap/rfc8621/contract/WebSocketContract.scala  | 85 ++++++++++++++++++++++
 .../rfc8621/memory/MemoryWebSocketContract.java    | 38 ++++++++++
 3 files changed, 176 insertions(+)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedWebSocketTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedWebSocketTest.java
new file mode 100644
index 0000000..6445ae5
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedWebSocketTest.java
@@ -0,0 +1,53 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.distributed;
+
+import org.apache.james.CassandraExtension;
+import org.apache.james.CassandraRabbitMQJamesConfiguration;
+import org.apache.james.CassandraRabbitMQJamesServerMain;
+import org.apache.james.DockerElasticSearchExtension;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.WebSocketContract;
+import org.apache.james.modules.AwsS3BlobStoreExtension;
+import org.apache.james.modules.RabbitMQExtension;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.apache.james.modules.blobstore.BlobStoreConfiguration;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class DistributedWebSocketTest implements WebSocketContract {
+    @RegisterExtension
+    static JamesServerExtension testExtension = new JamesServerBuilder<CassandraRabbitMQJamesConfiguration>(tmpDir ->
+        CassandraRabbitMQJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .configurationFromClasspath()
+            .blobStore(BlobStoreConfiguration.builder()
+                .s3()
+                .disableCache()
+                .deduplication())
+            .build())
+        .extension(new DockerElasticSearchExtension())
+        .extension(new CassandraExtension())
+        .extension(new RabbitMQExtension())
+        .extension(new AwsS3BlobStoreExtension())
+        .server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration)
+            .overrideWith(new TestJMAPServerModule()))
+        .build();
+}
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/WebSocketContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
new file mode 100644
index 0000000..ae91a42
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
@@ -0,0 +1,85 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+package org.apache.james.jmap.rfc8621.contract
+
+import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.rfc8621.contract.Fixture._
+import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
+import org.apache.james.utils.DataProbeImpl
+import org.junit.jupiter.api.{BeforeEach, Tag, Test}
+
+trait WebSocketContract {
+  @BeforeEach
+  def setUp(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl])
+      .fluent()
+      .addDomain(DOMAIN.asString())
+      .addUser(BOB.asString(), BOB_PASSWORD)
+  }
+
+  @Test
+  @Tag(CategoryTags.BASIC_FEATURE)
+  def apiRequestsShouldBeProcessed(): Unit = {
+    /*
+    * TODO test an echo response - request (success)
+    * */
+  }
+
+  @Test
+  def nonJsonPayloadShouldTriggerError(): Unit = {
+    /*
+    * TODO send 'the quick brown fox' and get an error level error
+    * */
+  }
+
+  @Test
+  def handshakeShouldBeAuthenticated(): Unit = {
+    /*
+    * TODO set up no auth
+    * */
+  }
+
+  @Test
+  def noTypeFiledShouldTriggerError(): Unit = {
+    /*
+    * TODO send something without @type and get an error level error
+    * */
+  }
+
+  @Test
+  def badTypeFieldShouldTriggerError(): Unit = {
+    /*
+    * TODO send something with @type being a JsNumber and get an error level error
+    * */
+  }
+
+  @Test
+  def unknownTypeFieldShouldTriggerError(): Unit = {
+    /*
+    * TODO send something with @type being a JsString("unknown") and get an error level error
+    * */
+  }
+
+  @Test
+  def requestLevelErrorShouldReturnAPIError(): Unit = {
+    /*
+    * TODO send a request triggering a method level error (eg Mailbox/get with an invalid JSON payload)
+    * */
+  }
+}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryWebSocketContract.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryWebSocketContract.java
new file mode 100644
index 0000000..b128f72
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryWebSocketContract.java
@@ -0,0 +1,38 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.memory;
+
+import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE;
+
+import org.apache.james.GuiceJamesServer;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.WebSocketContract;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class MemoryWebSocketContract implements WebSocketContract {
+    @RegisterExtension
+    static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+        .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
+            .combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE)
+            .overrideWith(new TestJMAPServerModule()))
+        .build();
+}


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


[james-project] 05/12: JAMES-3491 Advertise the JMAP websocket capability in the session

Posted by bt...@apache.org.
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

commit 647e2e8843b83dc21ec1908a599ee71f45458e84
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Jan 28 12:29:50 2021 +0700

    JAMES-3491 Advertise the JMAP websocket capability in the session
---
 .../jmap/rfc8621/contract/SessionRoutesContract.scala   |  9 +++++++++
 .../scala/org/apache/james/jmap/core/Capabilities.scala | 17 +++++++++++------
 .../scala/org/apache/james/jmap/core/Capability.scala   | 11 ++++++++++-
 .../james/jmap/core/JmapRfc8621Configuration.scala      |  1 +
 .../org/apache/james/jmap/json/ResponseSerializer.scala |  5 +++++
 .../org/apache/james/jmap/routes/SessionSupplier.scala  |  8 +++-----
 .../james/jmap/json/MailboxGetSerializationTest.scala   |  7 +++++--
 .../apache/james/jmap/routes/JMAPApiRoutesTest.scala    |  4 ++--
 .../apache/james/jmap/routes/SessionRoutesTest.scala    |  9 +++++++++
 9 files changed, 55 insertions(+), 16 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/SessionRoutesContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/SessionRoutesContract.scala
index 9ba8df0..7e1abc8 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/SessionRoutesContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/SessionRoutesContract.scala
@@ -63,6 +63,10 @@ object SessionRoutesContract {
                          |      "emailQuerySortOptions" : ["receivedAt", "sentAt"],
                          |      "mayCreateTopLevelMailbox" : true
                          |    },
+                         |    "urn:ietf:params:jmap:websocket": {
+                         |      "supportsPush": false,
+                         |      "url": "http://domain.com/jmap/ws"
+                         |    },
                          |    "urn:apache:james:params:jmap:mail:quota": {},
                          |    "urn:apache:james:params:jmap:mail:shares": {},
                          |    "urn:ietf:params:jmap:vacationresponse":{}
@@ -77,6 +81,10 @@ object SessionRoutesContract {
                          |          "maxDelayedSend": 0,
                          |          "submissionExtensions": []
                          |        },
+                         |        "urn:ietf:params:jmap:websocket": {
+                         |            "supportsPush": false,
+                         |            "url": "http://domain.com/jmap/ws"
+                         |        },
                          |        "urn:ietf:params:jmap:core" : {
                          |          "maxSizeUpload" : 20971520,
                          |          "maxConcurrentUpload" : 4,
@@ -103,6 +111,7 @@ object SessionRoutesContract {
                          |  },
                          |  "primaryAccounts" : {
                          |    "urn:ietf:params:jmap:submission": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+                         |    "urn:ietf:params:jmap:websocket": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
                          |    "urn:ietf:params:jmap:core" : "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
                          |    "urn:ietf:params:jmap:mail" : "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
                          |    "urn:apache:james:params:jmap:mail:quota": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capabilities.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capabilities.scala
index af4ad66..689c9ca 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capabilities.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capabilities.scala
@@ -18,8 +18,10 @@
  ****************************************************************/
 package org.apache.james.jmap.core
 
+import java.net.URL
+
 import eu.timepit.refined.auto._
-import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, EMAIL_SUBMISSION, JAMES_QUOTA, JAMES_SHARES, JMAP_CORE, JMAP_MAIL, JMAP_VACATION_RESPONSE}
+import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
 
 object DefaultCapabilities {
   def coreCapability(maxUploadSize: MaxSizeUpload) = CoreCapability(
@@ -32,6 +34,10 @@ object DefaultCapabilities {
       MaxObjectsInGet(500L),
       MaxObjectsInSet(500L),
       collationAlgorithms = List("i;unicode-casemap")))
+
+  def webSocketCapability(url: URL) = WebSocketCapability(
+    properties = WebSocketCapabilityProperties(SupportsPush(false), url))
+
   val MAIL_CAPABILITY = MailCapability(
     properties = MailCapabilityProperties(
       MaxMailboxesPerEmail(Some(10_000_000L)),
@@ -45,15 +51,14 @@ object DefaultCapabilities {
   val VACATION_RESPONSE_CAPABILITY = VacationResponseCapability()
   val SUBMISSION_CAPABILITY = SubmissionCapability()
 
-  val SUPPORTED_CAPABILITY_IDENTIFIERS: Set[CapabilityIdentifier] =
-    Set(JMAP_CORE, JMAP_MAIL, JMAP_VACATION_RESPONSE, JAMES_SHARES, JAMES_QUOTA, EMAIL_SUBMISSION)
-
-  def supported(maxUploadSize: MaxSizeUpload): Capabilities = Capabilities(coreCapability(maxUploadSize),
+  def supported(configuration: JmapRfc8621Configuration): Capabilities = Capabilities(
+    coreCapability(configuration.maxUploadSize),
     MAIL_CAPABILITY,
     QUOTA_CAPABILITY,
     SHARES_CAPABILITY,
     VACATION_RESPONSE_CAPABILITY,
-    SUBMISSION_CAPABILITY)
+    SUBMISSION_CAPABILITY,
+    webSocketCapability(configuration.webSocketUrl))
 }
 
 case class Capabilities(capabilities: Capability*) {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala
index 94b0c50..d3582bd 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala
@@ -19,12 +19,14 @@
 
 package org.apache.james.jmap.core
 
+import java.net.URL
+
 import eu.timepit.refined
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.auto._
 import eu.timepit.refined.collection.NonEmpty
 import eu.timepit.refined.string.Uri
-import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, EMAIL_SUBMISSION, JAMES_QUOTA, JAMES_SHARES, JMAP_CORE, JMAP_MAIL, JMAP_VACATION_RESPONSE}
+import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, EMAIL_SUBMISSION, JAMES_QUOTA, JAMES_SHARES, JMAP_CORE, JMAP_MAIL, JMAP_VACATION_RESPONSE, JMAP_WEBSOCKET}
 import org.apache.james.jmap.core.CoreCapabilityProperties.CollationAlgorithm
 import org.apache.james.jmap.core.MailCapability.EmailQuerySortOption
 import org.apache.james.jmap.core.UnsignedInt.{UnsignedInt, UnsignedIntConstraint}
@@ -41,6 +43,7 @@ object CapabilityIdentifier {
   val JMAP_MAIL: CapabilityIdentifier = "urn:ietf:params:jmap:mail"
   val JMAP_VACATION_RESPONSE: CapabilityIdentifier = "urn:ietf:params:jmap:vacationresponse"
   val EMAIL_SUBMISSION: CapabilityIdentifier = "urn:ietf:params:jmap:submission"
+  val JMAP_WEBSOCKET: CapabilityIdentifier = "urn:ietf:params:jmap:websocket"
   val JAMES_QUOTA: CapabilityIdentifier = "urn:apache:james:params:jmap:mail:quota"
   val JAMES_SHARES: CapabilityIdentifier = "urn:apache:james:params:jmap:mail:shares"
 }
@@ -55,6 +58,8 @@ trait Capability {
 final case class CoreCapability(properties: CoreCapabilityProperties,
                                 identifier: CapabilityIdentifier = JMAP_CORE) extends Capability
 
+case class WebSocketCapability(properties: WebSocketCapabilityProperties, identifier: CapabilityIdentifier = JMAP_WEBSOCKET) extends Capability
+
 object MaxSizeUpload {
   def of(size: Size): Try[MaxSizeUpload] = refined.refineV[UnsignedIntConstraint](size.asBytes()) match {
     case Right(value) => Success(MaxSizeUpload(value))
@@ -83,6 +88,10 @@ final case class CoreCapabilityProperties(maxSizeUpload: MaxSizeUpload,
                                           maxObjectsInSet: MaxObjectsInSet,
                                           collationAlgorithms: List[CollationAlgorithm]) extends CapabilityProperties
 
+final case class WebSocketCapabilityProperties(supportsPush: SupportsPush,
+                                               url: URL) extends CapabilityProperties
+
+final case class SupportsPush(value: Boolean) extends AnyVal
 final case class MaxDelayedSend(value: Int) extends AnyVal
 final case class EhloName(value: String) extends AnyVal
 final case class EhloArgs(value: String) extends AnyVal
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/JmapRfc8621Configuration.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/JmapRfc8621Configuration.scala
index 9ce796a..43f8420 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/JmapRfc8621Configuration.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/JmapRfc8621Configuration.scala
@@ -48,4 +48,5 @@ case class JmapRfc8621Configuration(urlPrefixString: String, maxUploadSize: MaxS
   val downloadUrl: URL = new URL(urlPrefixString + "/download/{accountId}/{blobId}/?type={type}&name={name}")
   val uploadUrl: URL = new URL(s"$urlPrefixString/upload/{accountId}")
   val eventSourceUrl: URL = new URL(s"$urlPrefixString/eventSource")
+  val webSocketUrl: URL = new URL(s"$urlPrefixString/jmap/ws")
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
index 02ca667..f9fcee5 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ResponseSerializer.scala
@@ -91,11 +91,14 @@ object ResponseSerializer {
   private implicit val maxDelayedSendWrites: Writes[MaxDelayedSend] = Json.valueWrites[MaxDelayedSend]
   private implicit val ehloNameWrites: Writes[EhloName] = Json.valueWrites[EhloName]
   private implicit val ehloArgsWrites: Writes[EhloArgs] = Json.valueWrites[EhloArgs]
+  private implicit val supportsPushWrites: Writes[SupportsPush] = Json.valueWrites[SupportsPush]
   private implicit val submissionPropertiesWrites: Writes[SubmissionProperties] = Json.writes[SubmissionProperties]
+  private implicit val webSocketPropertiesWrites: Writes[WebSocketCapabilityProperties] = Json.writes[WebSocketCapabilityProperties]
   private implicit val quotaCapabilityWrites: Writes[QuotaCapabilityProperties] = OWrites[QuotaCapabilityProperties](_ => Json.obj())
   private implicit val sharesCapabilityWrites: Writes[SharesCapabilityProperties] = OWrites[SharesCapabilityProperties](_ => Json.obj())
   private implicit val vacationResponseCapabilityWrites: Writes[VacationResponseCapabilityProperties] = OWrites[VacationResponseCapabilityProperties](_ => Json.obj())
   private implicit val submissionCapabilityWrites: Writes[SubmissionCapability] = OWrites[SubmissionCapability](_ => Json.obj())
+  private implicit val webSocketCapabilityWrites: Writes[WebSocketCapability] = OWrites[WebSocketCapability](_ => Json.obj())
 
   private implicit val setCapabilityWrites: Writes[Set[_ <: Capability]] =
     (set: Set[_ <: Capability]) => {
@@ -113,6 +116,8 @@ object ResponseSerializer {
             jsObject.+(capability.identifier.value, vacationResponseCapabilityWrites.writes(capability.properties))
           case capability: SubmissionCapability =>
             jsObject.+(capability.identifier.value, submissionPropertiesWrites.writes(capability.properties))
+          case capability: WebSocketCapability =>
+            jsObject.+(capability.identifier.value, webSocketPropertiesWrites.writes(capability.properties))
           case _ => jsObject
         }
       })
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
index c9d631c..bb9270f 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
@@ -25,12 +25,10 @@ import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.core.{Account, AccountId, DefaultCapabilities, IsPersonal, IsReadOnly, JmapRfc8621Configuration, Session}
 
 class SessionSupplier @Inject() (val configuration: JmapRfc8621Configuration) {
-  private val maxSizeUpload = configuration.maxUploadSize
-
   def generate(username: Username): Either[IllegalArgumentException, Session] =
     accounts(username)
       .map(account => Session(
-        DefaultCapabilities.supported(maxSizeUpload),
+        DefaultCapabilities.supported(configuration),
         List(account),
         primaryAccounts(account.accountId),
         username,
@@ -40,10 +38,10 @@ class SessionSupplier @Inject() (val configuration: JmapRfc8621Configuration) {
         eventSourceUrl = configuration.eventSourceUrl))
 
   private def accounts(username: Username): Either[IllegalArgumentException, Account] =
-    Account.from(username, IsPersonal(true), IsReadOnly(false), DefaultCapabilities.supported(maxSizeUpload).toSet)
+    Account.from(username, IsPersonal(true), IsReadOnly(false), DefaultCapabilities.supported(configuration).toSet)
 
   private def primaryAccounts(accountId: AccountId): Map[CapabilityIdentifier, AccountId] =
-    DefaultCapabilities.supported(maxSizeUpload).toSet
+    DefaultCapabilities.supported(configuration).toSet
       .map(capability => (capability.identifier(), accountId))
       .toMap
 }
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala
index 5eb339a..62c4e92 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala
@@ -21,8 +21,9 @@ package org.apache.james.jmap.json
 
 import eu.timepit.refined.auto._
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
+import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, EMAIL_SUBMISSION, JAMES_QUOTA, JAMES_SHARES, JMAP_CORE, JMAP_MAIL, JMAP_VACATION_RESPONSE}
 import org.apache.james.jmap.core.State.INSTANCE
-import org.apache.james.jmap.core.{AccountId, DefaultCapabilities, Properties}
+import org.apache.james.jmap.core.{AccountId, Properties}
 import org.apache.james.jmap.json.Fixture._
 import org.apache.james.jmap.json.MailboxGetSerializationTest._
 import org.apache.james.jmap.json.MailboxSerializationTest.MAILBOX
@@ -144,6 +145,8 @@ class MailboxGetSerializationTest extends AnyWordSpec with Matchers {
 
   "Serialize MailboxGetResponse" should {
     "succeed" in {
+      val supportedCapabilityIdentifiers: Set[CapabilityIdentifier] =
+        Set(JMAP_CORE, JMAP_MAIL, JMAP_VACATION_RESPONSE, JAMES_SHARES, JAMES_QUOTA, EMAIL_SUBMISSION)
       val actualValue: MailboxGetResponse = MailboxGetResponse(
         accountId = ACCOUNT_ID,
         state = INSTANCE,
@@ -197,7 +200,7 @@ class MailboxGetSerializationTest extends AnyWordSpec with Matchers {
           |}
           |""".stripMargin
 
-      assertThatJson(Json.stringify(SERIALIZER.serialize(actualValue, Mailbox.allProperties, DefaultCapabilities.SUPPORTED_CAPABILITY_IDENTIFIERS))).isEqualTo(expectedJson)
+      assertThatJson(Json.stringify(SERIALIZER.serialize(actualValue, Mailbox.allProperties, supportedCapabilityIdentifiers))).isEqualTo(expectedJson)
     }
   }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
index 61faf1b..ab43abe 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
@@ -78,7 +78,7 @@ object JMAPApiRoutesTest {
   private val userProvisionner: UserProvisioning = new UserProvisioning(usersRepository, new RecordingMetricFactory)
   private val JMAP_METHODS: Set[Method] = Set(new CoreEchoMethod)
 
-  private val JMAP_API_ROUTE: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, new JMAPApi(JMAP_METHODS, DefaultCapabilities.supported(JmapRfc8621Configuration.UPLOAD_LIMIT_30_MB).capabilities.toSet))
+  private val JMAP_API_ROUTE: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, new JMAPApi(JMAP_METHODS, DefaultCapabilities.supported(JmapRfc8621Configuration("http://127.0.0.1")).capabilities.toSet))
   private val ROUTES_HANDLER: ImmutableSet[JMAPRoutesHandler] = ImmutableSet.of(new JMAPRoutesHandler(Version.RFC8621, JMAP_API_ROUTE))
 
   private val userBase64String: String = Base64.getEncoder.encodeToString("user1:password".getBytes(StandardCharsets.UTF_8))
@@ -442,7 +442,7 @@ class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
     when(mockCoreEchoMethod.requiredCapabilities).thenReturn(Set(JMAP_CORE))
 
     val methods: Set[Method] = Set(mockCoreEchoMethod)
-    val apiRoute: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, new JMAPApi(methods, DefaultCapabilities.supported(JmapRfc8621Configuration.UPLOAD_LIMIT_30_MB).capabilities.toSet))
+    val apiRoute: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, new JMAPApi(methods, DefaultCapabilities.supported(JmapRfc8621Configuration("http://127.0.0.1")).capabilities.toSet))
     val routesHandler: ImmutableSet[JMAPRoutesHandler] = ImmutableSet.of(new JMAPRoutesHandler(Version.RFC8621, apiRoute))
 
     val versionParser: VersionParser = new VersionParser(SUPPORTED_VERSIONS, JMAPConfiguration.DEFAULT)
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionRoutesTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionRoutesTest.scala
index 2f21d81..9e903e2 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionRoutesTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionRoutesTest.scala
@@ -142,6 +142,10 @@ class SessionRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
                          |      "emailQuerySortOptions" : ["receivedAt", "sentAt"],
                          |      "mayCreateTopLevelMailbox" : true
                          |    },
+                         |    "urn:ietf:params:jmap:websocket": {
+                         |      "supportsPush": false,
+                         |      "url": "http://localhost/jmap/ws"
+                         |    },
                          |    "urn:apache:james:params:jmap:mail:quota": {},
                          |    "urn:apache:james:params:jmap:mail:shares": {},
                          |    "urn:ietf:params:jmap:vacationresponse":{}
@@ -156,6 +160,10 @@ class SessionRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
                          |          "maxDelayedSend": 0,
                          |          "submissionExtensions": []
                          |        },
+                         |        "urn:ietf:params:jmap:websocket": {
+                         |            "supportsPush": false,
+                         |            "url": "http://localhost/jmap/ws"
+                         |        },
                          |        "urn:ietf:params:jmap:core" : {
                          |          "maxSizeUpload" : 31457280,
                          |          "maxConcurrentUpload" : 4,
@@ -182,6 +190,7 @@ class SessionRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
                          |  },
                          |  "primaryAccounts" : {
                          |    "urn:ietf:params:jmap:submission": "0fe275bf13ff761407c17f64b1dfae2f4b3186feea223d7267b79f873a105401",
+                         |    "urn:ietf:params:jmap:websocket": "0fe275bf13ff761407c17f64b1dfae2f4b3186feea223d7267b79f873a105401",
                          |    "urn:ietf:params:jmap:core" : "0fe275bf13ff761407c17f64b1dfae2f4b3186feea223d7267b79f873a105401",
                          |    "urn:ietf:params:jmap:mail" : "0fe275bf13ff761407c17f64b1dfae2f4b3186feea223d7267b79f873a105401",
                          |    "urn:apache:james:params:jmap:mail:quota": "0fe275bf13ff761407c17f64b1dfae2f4b3186feea223d7267b79f873a105401",


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


[james-project] 11/12: JAMES-3491 Cleanup WebSocket tests

Posted by bt...@apache.org.
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

commit 72a1ae1da39fcd4125ea406ab977c58f7871f836
Author: Raphael Ouazana <ra...@linagora.com>
AuthorDate: Thu Jan 28 15:53:32 2021 +0100

    JAMES-3491 Cleanup WebSocket tests
---
 .../jmap/rfc8621/contract/WebSocketContract.scala  | 252 ++++++---------------
 ...ocketContract.java => MemoryWebSocketTest.java} |   2 +-
 .../apache/james/jmap/routes/WebSocketRoutes.scala |   6 +-
 3 files changed, 68 insertions(+), 192 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/WebSocketContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
index 8b87d55..7bf45a3 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
@@ -20,19 +20,32 @@ package org.apache.james.jmap.rfc8621.contract
 
 import java.net.URI
 import java.util
+import java.util.concurrent.TimeUnit
 
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import org.apache.james.GuiceJamesServer
 import org.apache.james.jmap.draft.JmapGuiceProbe
 import org.apache.james.jmap.rfc8621.contract.Fixture._
+import org.apache.james.jmap.rfc8621.contract.WebSocketContract.{LOGGER, await}
 import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
 import org.apache.james.utils.DataProbeImpl
 import org.assertj.core.api.Assertions.assertThat
+import org.awaitility.Awaitility
 import org.java_websocket.client.WebSocketClient
 import org.java_websocket.handshake.ServerHandshake
 import org.junit.jupiter.api.{BeforeEach, Tag, Test}
+import org.slf4j.{Logger, LoggerFactory}
+
+object WebSocketContract {
+  val LOGGER: Logger = LoggerFactory.getLogger(classOf[WebSocketContract])
+
+  val await = Awaitility.await
+    .atMost(1, TimeUnit.SECONDS)
+    .pollInterval(100, TimeUnit.MILLISECONDS)
+}
 
 trait WebSocketContract {
+
   @BeforeEach
   def setUp(server: GuiceJamesServer): Unit = {
     server.getProbe(classOf[DataProbeImpl])
@@ -41,46 +54,24 @@ trait WebSocketContract {
       .addUser(BOB.asString(), BOB_PASSWORD)
   }
 
-  class ExampleClient(uri: URI) extends WebSocketClient(uri) {
+  class TestClient(uri: URI) extends WebSocketClient(uri) {
     val receivedResponses: util.LinkedList[String] = new util.LinkedList[String]()
-    var closeCode: Option[Integer] = None
     var closeString: Option[String] = None
 
-    override def onOpen(serverHandshake: ServerHandshake): Unit = {
-      println(s"handshake ${serverHandshake.getHttpStatus}")
-    }
+    override def onOpen(serverHandshake: ServerHandshake): Unit = {}
 
-    override def onMessage(s: String): Unit = {
-      println(s"Received: $s")
-      receivedResponses.add(s)
-    }
+    override def onMessage(s: String): Unit = receivedResponses.add(s)
 
-    override def onClose(i: Int, s: String, b: Boolean): Unit = {
-      closeCode = Some(i)
-      closeString = Some(s)
-      println(s"Closing connection $i $s $b")
-    }
+    override def onClose(i: Int, s: String, b: Boolean): Unit = closeString = Some(s)
 
-    override def onError(e: Exception): Unit = {
-      println("Error: " + e.getMessage)
-    }
+    override def onError(e: Exception): Unit = LOGGER.error("WebSocket error", e)
   }
 
   @Test
   @Tag(CategoryTags.BASIC_FEATURE)
   def apiRequestsShouldBeProcessed(server: GuiceJamesServer): Unit = {
-    println("started")
-    val port = server.getProbe(classOf[JmapGuiceProbe])
-      .getJmapPort
-      .getValue
-    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
-    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
-    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
-
+    val client: TestClient = authenticatedWebSocketClient(server)
     client.connectBlocking()
-
-    Thread.sleep(500)
-
     client.send("""{
                   |  "@type": "Request",
                   |  "requestId": "req-36",
@@ -97,35 +88,19 @@ trait WebSocketContract {
                   |  ]
                   |}""".stripMargin)
 
-    Thread.sleep(500)
-
-
-    assertThat(client.receivedResponses).hasSize(1)
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
-      """
-        |{
+    await.until(() => client.receivedResponses.size() == 1)
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo("""{
         |  "@type":"Response",
         |  "requestId":"req-36",
         |  "sessionState":"2c9f1b12-b35a-43e6-9af2-0106fb53a943",
         |  "methodResponses":[["Core/echo",{"arg1":"arg1data","arg2":"arg2data"},"c1"]]
-        |}
-        |""".stripMargin)
+        |}""".stripMargin)
   }
 
   @Test
   def apiRequestsShouldBeProcessedWhenNoRequestId(server: GuiceJamesServer): Unit = {
-    println("started")
-    val port = server.getProbe(classOf[JmapGuiceProbe])
-      .getJmapPort
-      .getValue
-    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
-    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
-    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
-
+    val client: TestClient = authenticatedWebSocketClient(server)
     client.connectBlocking()
-
-    Thread.sleep(500)
-
     client.send("""{
                   |  "@type": "Request",
                   |  "using": [ "urn:ietf:params:jmap:core"],
@@ -141,82 +116,44 @@ trait WebSocketContract {
                   |  ]
                   |}""".stripMargin)
 
-    Thread.sleep(500)
-
-
-    assertThat(client.receivedResponses).hasSize(1)
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
-      """
-        |{
+    await.untilAsserted(() => assertThat(client.receivedResponses).hasSize(1))
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo("""{
         |  "@type":"Response",
         |  "requestId":null,
         |  "sessionState":"2c9f1b12-b35a-43e6-9af2-0106fb53a943",
         |  "methodResponses":[["Core/echo",{"arg1":"arg1data","arg2":"arg2data"},"c1"]]
-        |}
-        |""".stripMargin)
+        |}""".stripMargin)
   }
 
   @Test
   def nonJsonPayloadShouldTriggerError(server: GuiceJamesServer): Unit = {
-    println("started")
-    val port = server.getProbe(classOf[JmapGuiceProbe])
-      .getJmapPort
-      .getValue
-    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
-    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
-    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
-
+    val client: TestClient = authenticatedWebSocketClient(server)
     client.connectBlocking()
-
-    Thread.sleep(500)
-
     client.send("The quick brown fox".stripMargin)
 
-    Thread.sleep(500)
-
-    assertThat(client.receivedResponses).hasSize(1)
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
-      """
-        |{
+    await.untilAsserted(() => assertThat(client.receivedResponses).hasSize(1))
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo("""{
         |  "status":400,
         |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Unrecognized token 'The': was expecting ('true', 'false' or 'null')\n at [Source: (String)\"The quick brown fox\"; line: 1, column: 4]),ArraySeq()))))",
         |  "type":"urn:ietf:params:jmap:error:notRequest",
         |  "requestId":null,
         |  "@type":"RequestError"
-        |}
-        |""".stripMargin)
+        |}""".stripMargin)
   }
 
   @Test
   def handshakeShouldBeAuthenticated(server: GuiceJamesServer): Unit = {
-    val port = server.getProbe(classOf[JmapGuiceProbe])
-      .getJmapPort
-      .getValue
-    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
-    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
-
+    val client: TestClient = unauthenticatedWebSocketClient(server)
     client.connectBlocking()
 
-    Thread.sleep(100)
-
     assertThat(client.isClosed).isTrue
     assertThat(client.closeString).isEqualTo(Some("Invalid status code received: 401 Status line: HTTP/1.1 401 Unauthorized"))
   }
 
   @Test
   def noTypeFiledShouldTriggerError(server: GuiceJamesServer): Unit = {
-    println("started")
-    val port = server.getProbe(classOf[JmapGuiceProbe])
-      .getJmapPort
-      .getValue
-    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
-    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
-    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
-
+    val client: TestClient = authenticatedWebSocketClient(server)
     client.connectBlocking()
-
-    Thread.sleep(500)
-
     client.send("""{
                   |  "requestId": "req-36",
                   |  "using": [ "urn:ietf:params:jmap:core"],
@@ -232,37 +169,20 @@ trait WebSocketContract {
                   |  ]
                   |}""".stripMargin)
 
-    Thread.sleep(500)
-
-
-    assertThat(client.receivedResponses).hasSize(1)
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
-      """
-        |{
+    await.until(() => client.receivedResponses.size() == 1)
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo("""{
         |  "status":400,
         |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Missing @type filed on a webSocket inbound message),ArraySeq()))))",
         |  "type":"urn:ietf:params:jmap:error:notRequest",
         |  "requestId":null,
         |  "@type":"RequestError"
-        |}
-        |""".stripMargin
-    )
+        |}""".stripMargin)
   }
 
   @Test
   def badTypeFieldShouldTriggerError(server: GuiceJamesServer): Unit = {
-    println("started")
-    val port = server.getProbe(classOf[JmapGuiceProbe])
-      .getJmapPort
-      .getValue
-    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
-    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
-    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
-
+    val client: TestClient = authenticatedWebSocketClient(server)
     client.connectBlocking()
-
-    Thread.sleep(500)
-
     client.send("""{
                   |  "@type": 42,
                   |  "requestId": "req-36",
@@ -279,37 +199,21 @@ trait WebSocketContract {
                   |  ]
                   |}""".stripMargin)
 
-    Thread.sleep(500)
-
-    assertThat(client.receivedResponses).hasSize(1)
+    await.untilAsserted(() => assertThat(client.receivedResponses).hasSize(1))
     assertThatJson(client.receivedResponses.get(0)).isEqualTo(
-      """
-        |{
+      """{
         |  "status":400,
         |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Invalid @type filed on a webSocket inbound message: expecting a JsString, got 42),ArraySeq()))))",
         |  "type":"urn:ietf:params:jmap:error:notRequest",
         |  "requestId":null,
         |  "@type":"RequestError"
-        |}
-        |""".stripMargin
-    )
-
+        |}""".stripMargin)
   }
 
   @Test
   def unknownTypeFieldShouldTriggerError(server: GuiceJamesServer): Unit = {
-    println("started")
-    val port = server.getProbe(classOf[JmapGuiceProbe])
-      .getJmapPort
-      .getValue
-    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
-    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
-    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
-
+    val client: TestClient = authenticatedWebSocketClient(server)
     client.connectBlocking()
-
-    Thread.sleep(500)
-
     client.send(
       """{
         |  "@type": "unknown",
@@ -327,37 +231,21 @@ trait WebSocketContract {
         |  ]
         |}""".stripMargin)
 
-    Thread.sleep(500)
-
-    assertThat(client.receivedResponses).hasSize(1)
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
-      """
-        |{
+    await.untilAsserted(() => assertThat(client.receivedResponses).hasSize(1))
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo("""{
         |  "status":400,
         |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Unknown @type filed on a webSocket inbound message: unknown),ArraySeq()))))",
         |  "type":"urn:ietf:params:jmap:error:notRequest",
         |  "requestId":null,
         |  "@type":"RequestError"
-        |}
-        |""".stripMargin
-    )
+        |}""".stripMargin)
   }
 
 
   @Test
   def clientSendingARespondTypeFieldShouldTriggerError(server: GuiceJamesServer): Unit = {
-    println("started")
-    val port = server.getProbe(classOf[JmapGuiceProbe])
-      .getJmapPort
-      .getValue
-    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
-    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
-    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
-
+    val client: TestClient = authenticatedWebSocketClient(server)
     client.connectBlocking()
-
-    Thread.sleep(500)
-
     client.send(
       """{
         |  "@type": "Response",
@@ -375,37 +263,20 @@ trait WebSocketContract {
         |  ]
         |}""".stripMargin)
 
-    Thread.sleep(500)
-
-    assertThat(client.receivedResponses).hasSize(1)
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
-      """
-        |{
+    await.untilAsserted(() => assertThat(client.receivedResponses).hasSize(1))
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo("""{
         |  "status":400,
         |  "detail":"The request was successfully parsed as JSON but did not match the type signature of the Request object: List((,List(JsonValidationError(List(Unknown @type filed on a webSocket inbound message: Response),ArraySeq()))))",
         |  "type":"urn:ietf:params:jmap:error:notRequest",
         |  "requestId":null,
         |  "@type":"RequestError"
-        |}
-        |""".stripMargin
-    )
-
+        |}""".stripMargin)
   }
 
   @Test
   def requestLevelErrorShouldReturnAPIError(server: GuiceJamesServer): Unit = {
-    println("started")
-    val port = server.getProbe(classOf[JmapGuiceProbe])
-      .getJmapPort
-      .getValue
-    val client = new ExampleClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
-    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
-    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
-
+    val client: TestClient = authenticatedWebSocketClient(server)
     client.connectBlocking()
-
-    Thread.sleep(500)
-
     client.send(s"""{
                    |  "@type": "Request",
                    |  "using": [
@@ -420,18 +291,27 @@ trait WebSocketContract {
                    |      "c1"]]
                    |}""".stripMargin)
 
-    Thread.sleep(500)
-
-
-    assertThat(client.receivedResponses).hasSize(1)
-    assertThatJson(client.receivedResponses.get(0)).isEqualTo(
-      """
-        |{
+    await.untilAsserted(() => assertThat(client.receivedResponses).hasSize(1))
+    assertThatJson(client.receivedResponses.get(0)).isEqualTo("""{
         |  "@type": "Response",
         |  "requestId": null,
         |  "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
         |  "methodResponses": [["error",{"type":"invalidArguments","description":"The following properties [invalidProperty] do not exist."},"c1"]]
-        |}
-        |""".stripMargin)
+        |}""".stripMargin)
+  }
+
+  private def unauthenticatedWebSocketClient(server: GuiceJamesServer): TestClient = {
+    val port = server.getProbe(classOf[JmapGuiceProbe])
+      .getJmapPort
+      .getValue
+    val client = new TestClient(new URI(s"ws://127.0.0.1:$port/jmap/ws"))
+    client.addHeader("Accept", ACCEPT_RFC8621_VERSION_HEADER)
+    client
+  }
+
+  private def authenticatedWebSocketClient(server: GuiceJamesServer): TestClient = {
+    val client = unauthenticatedWebSocketClient(server)
+    client.addHeader("Authorization", "Basic Ym9iQGRvbWFpbi50bGQ6Ym9icGFzc3dvcmQ=")
+    client
   }
 }
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryWebSocketContract.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryWebSocketTest.java
similarity index 96%
rename from server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryWebSocketContract.java
rename to server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryWebSocketTest.java
index b128f72..ce526e6 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryWebSocketContract.java
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryWebSocketTest.java
@@ -28,7 +28,7 @@ import org.apache.james.jmap.rfc8621.contract.WebSocketContract;
 import org.apache.james.modules.TestJMAPServerModule;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
-public class MemoryWebSocketContract implements WebSocketContract {
+public class MemoryWebSocketTest implements WebSocketContract {
     @RegisterExtension
     static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
         .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala
index 3965ac7..4664eba 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/WebSocketRoutes.scala
@@ -79,14 +79,10 @@ class WebSocketRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticato
         new String(bytes, StandardCharsets.UTF_8)
       })
       .flatMap(handleClientMessages(session))
-      .onErrorResume(e => SMono.just(asError(None)(e)))
+      .onErrorResume(e => SMono.just[WebSocketOutboundMessage](asError(None)(e)))
       .map(ResponseSerializer.serialize)
       .map(_.toString)
       .flatMap(response => out.sendString(SMono.just(response), StandardCharsets.UTF_8))
-      .onErrorResume(e => {
-        e.printStackTrace()
-        SMono.empty
-      })
       .`then`()
       .asJava()
       .`then`()


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