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