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 <[email protected]> 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: [email protected] For additional commands, e-mail: [email protected]
