You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2021/11/08 01:54:07 UTC
[james-project] branch master updated: JAMES-3539 JMAP webpush
integration test (#736)
This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push:
new 7ef52c9 JAMES-3539 JMAP webpush integration test (#736)
7ef52c9 is described below
commit 7ef52c9fbb5dbe69b1d0d24d6d92c4c71e2c748a
Author: vttran <vt...@linagora.com>
AuthorDate: Mon Nov 8 08:53:59 2021 +0700
JAMES-3539 JMAP webpush integration test (#736)
---
.../java/org/apache/james/ClockExtension.java} | 37 +-
.../james/modules/event/JMAPEventBusModule.java | 8 +-
.../modules/protocols/JmapEventBusModule.java | 4 +
.../distributed/DistributedWebPushTest.java | 63 +++
.../src/test/resources/smtpserver.xml | 23 +
.../jmap-rfc-8621-integration-tests-common/pom.xml | 8 +
.../jmap/rfc8621/contract/WebPushContract.scala | 537 +++++++++++++++++++++
.../jmap/rfc8621/memory/MemoryWebPushTest.java} | 30 +-
8 files changed, 690 insertions(+), 20 deletions(-)
diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JmapEventBusModule.java b/server/container/guice/common/src/test/java/org/apache/james/ClockExtension.java
similarity index 51%
copy from server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JmapEventBusModule.java
copy to server/container/guice/common/src/test/java/org/apache/james/ClockExtension.java
index 0fafb26..0d09cf4 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JmapEventBusModule.java
+++ b/server/container/guice/common/src/test/java/org/apache/james/ClockExtension.java
@@ -17,17 +17,38 @@
* under the License. *
****************************************************************/
-package org.apache.james.modules.protocols;
+package org.apache.james;
-import org.apache.james.events.EventBus;
-import org.apache.james.jmap.InjectionKeys;
+import java.time.Clock;
+import java.time.Instant;
-import com.google.inject.AbstractModule;
-import com.google.inject.name.Names;
+import org.apache.james.utils.UpdatableTickingClock;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolutionException;
+
+import com.google.inject.Module;
+
+public class ClockExtension implements GuiceModuleTestExtension {
+ private UpdatableTickingClock clock;
+
+ @Override
+ public void beforeEach(ExtensionContext extensionContext) {
+ clock = new UpdatableTickingClock(Instant.now());
+ }
+
+ @Override
+ public Module getModule() {
+ return binder -> binder.bind(Clock.class).toInstance(clock);
+ }
+
+ @Override
+ public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
+ return parameterContext.getParameter().getType() == UpdatableTickingClock.class;
+ }
-public class JmapEventBusModule extends AbstractModule {
@Override
- protected void configure() {
- bind(EventBus.class).annotatedWith(Names.named(InjectionKeys.JMAP)).to(EventBus.class);
+ public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
+ return clock;
}
}
diff --git a/server/container/guice/distributed/src/main/java/org/apache/james/modules/event/JMAPEventBusModule.java b/server/container/guice/distributed/src/main/java/org/apache/james/modules/event/JMAPEventBusModule.java
index 62ca4f9..62a34bf 100644
--- a/server/container/guice/distributed/src/main/java/org/apache/james/modules/event/JMAPEventBusModule.java
+++ b/server/container/guice/distributed/src/main/java/org/apache/james/modules/event/JMAPEventBusModule.java
@@ -37,6 +37,7 @@ import org.apache.james.events.RoutingKeyConverter;
import org.apache.james.jmap.InjectionKeys;
import org.apache.james.jmap.change.Factory;
import org.apache.james.jmap.change.JmapEventSerializer;
+import org.apache.james.jmap.pushsubscription.PushListener;
import org.apache.james.metrics.api.MetricFactory;
import org.apache.james.utils.InitializationOperation;
import org.apache.james.utils.InitilizationOperationBuilder;
@@ -59,10 +60,13 @@ public class JMAPEventBusModule extends AbstractModule {
}
@ProvidesIntoSet
- InitializationOperation workQueue(@Named(InjectionKeys.JMAP) RabbitMQEventBus instance) {
+ InitializationOperation workQueue(@Named(InjectionKeys.JMAP) RabbitMQEventBus instance, PushListener pushListener) {
return InitilizationOperationBuilder
.forClass(RabbitMQEventBus.class)
- .init(instance::start);
+ .init(() -> {
+ instance.start();
+ instance.register(pushListener);
+ });
}
@ProvidesIntoSet
diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JmapEventBusModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JmapEventBusModule.java
index 0fafb26..71bdb0d 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JmapEventBusModule.java
+++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JmapEventBusModule.java
@@ -20,14 +20,18 @@
package org.apache.james.modules.protocols;
import org.apache.james.events.EventBus;
+import org.apache.james.events.EventListener;
import org.apache.james.jmap.InjectionKeys;
+import org.apache.james.jmap.pushsubscription.PushListener;
import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.Multibinder;
import com.google.inject.name.Names;
public class JmapEventBusModule extends AbstractModule {
@Override
protected void configure() {
bind(EventBus.class).annotatedWith(Names.named(InjectionKeys.JMAP)).to(EventBus.class);
+ Multibinder.newSetBinder(binder(), EventListener.ReactiveGroupEventListener.class).addBinding().to(PushListener.class);
}
}
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/DistributedWebPushTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedWebPushTest.java
new file mode 100644
index 0000000..8a9a2ae
--- /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/DistributedWebPushTest.java
@@ -0,0 +1,63 @@
+/****************************************************************
+ * 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.ClockExtension;
+import org.apache.james.DockerElasticSearchExtension;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.PushServerExtension;
+import org.apache.james.jmap.rfc8621.contract.PushSubscriptionProbeModule;
+import org.apache.james.jmap.rfc8621.contract.WebPushContract;
+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;
+
+class DistributedWebPushTest implements WebPushContract {
+ public static final DockerElasticSearchExtension ELASTIC_SEARCH_EXTENSION = new DockerElasticSearchExtension();
+
+ @RegisterExtension
+ static JamesServerExtension testExtension = new JamesServerBuilder<CassandraRabbitMQJamesConfiguration>(tmpDir ->
+ CassandraRabbitMQJamesConfiguration.builder()
+ .workingDirectory(tmpDir)
+ .configurationFromClasspath()
+ .blobStore(BlobStoreConfiguration.builder()
+ .s3()
+ .disableCache()
+ .deduplication()
+ .noCryptoConfig())
+ .build())
+ .extension(ELASTIC_SEARCH_EXTENSION)
+ .extension(new CassandraExtension())
+ .extension(new RabbitMQExtension())
+ .extension(new AwsS3BlobStoreExtension())
+ .extension(new ClockExtension())
+ .server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration)
+ .overrideWith(new TestJMAPServerModule(), new PushSubscriptionProbeModule()))
+ .build();
+
+ @RegisterExtension
+ static PushServerExtension pushServerExtension = new PushServerExtension();
+}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/resources/smtpserver.xml b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/resources/smtpserver.xml
index 3c9ee85..d62267a 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/resources/smtpserver.xml
+++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/resources/smtpserver.xml
@@ -20,6 +20,29 @@
-->
<smtpservers>
+ <smtpserver enabled="true">
+ <jmxName>smtpserver-global</jmxName>
+ <bind>0.0.0.0:0</bind>
+ <connectionBacklog>200</connectionBacklog>
+ <tls socketTLS="false" startTLS="false">
+ <keystore>file://conf/keystore</keystore>
+ <secret>james72laBalle</secret>
+ <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
+ <algorithm>SunX509</algorithm>
+ </tls>
+ <connectiontimeout>360</connectiontimeout>
+ <connectionLimit>0</connectionLimit>
+ <connectionLimitPerIP>0</connectionLimitPerIP>
+ <authRequired>false</authRequired>
+ <verifyIdentity>false</verifyIdentity>
+ <maxmessagesize>0</maxmessagesize>
+ <addressBracketsEnforcement>true</addressBracketsEnforcement>
+ <smtpGreeting>Apache JAMES awesome SMTP Server</smtpGreeting>
+ <handlerchain>
+ <handler class="org.apache.james.smtpserver.fastfail.ValidRcptHandler"/>
+ <handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/>
+ </handlerchain>
+ </smtpserver>
</smtpservers>
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 c3eba74..16e7079 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
@@ -48,10 +48,18 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-guice-smtp</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>james-server-jmap-draft</artifactId>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-testing</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>testing-base</artifactId>
</dependency>
<dependency>
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/WebPushContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebPushContract.scala
new file mode 100644
index 0000000..33b9ef4
--- /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/WebPushContract.scala
@@ -0,0 +1,537 @@
+/****************************************************************
+ * 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 com.google.crypto.tink.apps.webpush.WebPushHybridDecrypt
+import com.google.crypto.tink.subtle.EllipticCurves
+import com.google.crypto.tink.subtle.EllipticCurves.CurveType
+import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured.{`given`, requestSpecification}
+import io.restassured.http.ContentType.JSON
+import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
+import org.apache.http.HttpStatus
+import org.apache.http.HttpStatus.SC_OK
+import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.core.ResponseObject.SESSION_STATE
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
+import org.apache.james.mailbox.model.MailboxPath
+import org.apache.james.modules.MailboxProbeImpl
+import org.apache.james.modules.protocols.SmtpGuiceProbe
+import org.apache.james.utils.{DataProbeImpl, SMTPMessageSender, SpoolerProbe, UpdatableTickingClock}
+import org.awaitility.Awaitility
+import org.awaitility.Durations.ONE_HUNDRED_MILLISECONDS
+import org.awaitility.core.ConditionFactory
+import org.junit.jupiter.api.{BeforeEach, Tag, Test}
+import org.mockserver.integration.ClientAndServer
+import org.mockserver.mock.action.ExpectationResponseCallback
+import org.mockserver.model.HttpRequest.request
+import org.mockserver.model.HttpResponse.response
+import org.mockserver.model.JsonBody.json
+import org.mockserver.model.Not.not
+import org.mockserver.model.{HttpRequest, HttpResponse}
+import org.mockserver.verify.VerificationTimes
+import play.api.libs.json.{JsObject, JsString, Json}
+
+import java.nio.charset.StandardCharsets
+import java.security.KeyPair
+import java.security.interfaces.{ECPrivateKey, ECPublicKey}
+import java.time.temporal.ChronoUnit
+import java.util.Base64
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+
+trait WebPushContract {
+ private lazy val awaitAtMostTenSeconds: ConditionFactory = Awaitility.`with`
+ .pollInterval(ONE_HUNDRED_MILLISECONDS)
+ .and.`with`.pollDelay(ONE_HUNDRED_MILLISECONDS)
+ .await
+ .atMost(10, TimeUnit.SECONDS)
+ private lazy val PUSH_URL_PATH: String = "/push2"
+
+ @BeforeEach
+ def setUp(server: GuiceJamesServer): Unit = {
+ server.getProbe(classOf[DataProbeImpl])
+ .fluent()
+ .addDomain(DOMAIN.asString())
+ .addUser(BOB.asString(), BOB_PASSWORD)
+ .addUser(ANDRE.asString(), ANDRE_PASSWORD)
+
+ server.getProbe(classOf[MailboxProbeImpl])
+ .createMailbox(MailboxPath.inbox(BOB))
+
+ requestSpecification = baseRequestSpecBuilder(server)
+ .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+ .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .build()
+ }
+
+ private def getPushServerUrl(pushServer: ClientAndServer): String =
+ s"http://127.0.0.1:${pushServer.getLocalPort}$PUSH_URL_PATH"
+
+ // return pushSubscriptionId
+ private def createPushSubscription(pushServer: ClientAndServer): String =
+ `given`
+ .body(
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core"],
+ | "methodCalls": [
+ | [
+ | "PushSubscription/set",
+ | {
+ | "create": {
+ | "4f29": {
+ | "deviceClientId": "a889-ffea-910",
+ | "url": "${getPushServerUrl(pushServer)}",
+ | "types": ["Mailbox"]
+ | }
+ | }
+ | },
+ | "c1"
+ | ]
+ | ]
+ | }""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .extract()
+ .jsonPath()
+ .get("methodResponses[0][1].created.4f29.id")
+
+ private def updateValidateVerificationCode(pushSubscriptionId: String, verificationCode: String): String =
+ `given`()
+ .body(
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core"],
+ | "methodCalls": [
+ | [
+ | "PushSubscription/set",
+ | {
+ | "update": {
+ | "$pushSubscriptionId": {
+ | "verificationCode": "$verificationCode"
+ | }
+ | }
+ | },
+ | "c1"
+ | ]
+ | ]
+ | }""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract()
+ .body()
+ .asString()
+
+ private def sendEmailToBob(server: GuiceJamesServer): Unit = {
+ val smtpMessageSender: SMTPMessageSender = new SMTPMessageSender(DOMAIN.asString())
+ smtpMessageSender.connect("127.0.0.1", server.getProbe(classOf[SmtpGuiceProbe]).getSmtpPort)
+ .authenticate(ANDRE.asString, ANDRE_PASSWORD)
+ .sendMessage(ANDRE.asString, BOB.asString())
+ smtpMessageSender.close()
+
+ awaitAtMostTenSeconds.until(() => server.getProbe(classOf[SpoolerProbe]).processingFinished())
+ }
+
+ private def setupPushServerCallback(pushServer: ClientAndServer): AtomicReference[String] = {
+ val bodyRequestOnPushServer: AtomicReference[String] = new AtomicReference("")
+ pushServer
+ .when(request
+ .withPath(PUSH_URL_PATH)
+ .withMethod("POST"))
+ .respond(new ExpectationResponseCallback() {
+ override def handle(httpRequest: HttpRequest): HttpResponse = {
+ bodyRequestOnPushServer.set(httpRequest.getBodyAsString)
+ response()
+ .withStatusCode(HttpStatus.SC_CREATED)
+ }
+ })
+ bodyRequestOnPushServer
+ }
+
+ @Test
+ @Tag(CategoryTags.BASIC_FEATURE)
+ def correctBehaviourShouldSuccess(server: GuiceJamesServer, pushServer: ClientAndServer): Unit = {
+ // Setup mock-server for callback
+ val bodyRequestOnPushServer: AtomicReference[String] = setupPushServerCallback(pushServer)
+
+ // WHEN bob creates a push subscription
+ val pushSubscriptionId: String = createPushSubscription(pushServer)
+ // THEN a validation code is sent
+ awaitAtMostTenSeconds.untilAsserted { () =>
+ pushServer.verify(HttpRequest.request()
+ .withPath(PUSH_URL_PATH)
+ .withBody(json(
+ s"""{
+ | "@type": "PushVerification",
+ | "pushSubscriptionId": "$pushSubscriptionId",
+ | "verificationCode": "$${json-unit.any-string}"
+ |}""".stripMargin)),
+ VerificationTimes.atLeast(1))
+ }
+
+ // GIVEN bob retrieves the validation code from the mock server
+ val verificationCode: String = Json.parse(bodyRequestOnPushServer.get()).asInstanceOf[JsObject]
+ .value("verificationCode")
+ .asInstanceOf[JsString]
+ .value
+
+ // WHEN bob updates the validation code via JMAP
+ val updateVerificationCodeResponse: String = updateValidateVerificationCode(pushSubscriptionId, verificationCode)
+
+ // THEN it succeed
+ assertThatJson(updateVerificationCodeResponse)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [
+ | "PushSubscription/set",
+ | {
+ | "updated": {
+ | "$pushSubscriptionId": {}
+ | }
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+
+ // WHEN bob receives a mail
+ sendEmailToBob(server)
+
+ // THEN bob has a stateChange on the push gateway
+ awaitAtMostTenSeconds.untilAsserted { () =>
+ pushServer.verify(HttpRequest.request()
+ .withPath(PUSH_URL_PATH)
+ .withBody(json(
+ s"""{
+ | "@type": "StateChange",
+ | "changed": {
+ | "$ACCOUNT_ID": {
+ | "Mailbox": "$${json-unit.any-string}"
+ | }
+ | }
+ |}""".stripMargin)),
+ VerificationTimes.atLeast(1))
+ }
+ }
+
+ @Test
+ def webPushShouldNotPushToPushServerWhenExpiredSubscription(server: GuiceJamesServer, pushServer: ClientAndServer, clock: UpdatableTickingClock): Unit = {
+ // Setup mock-server for callback
+ val bodyRequestOnPushServer: AtomicReference[String] = setupPushServerCallback(pushServer)
+
+ // WHEN bob creates a push subscription
+ val pushSubscriptionId: String = createPushSubscription(pushServer)
+ // THEN a validation code is sent
+ awaitAtMostTenSeconds.untilAsserted { () =>
+ pushServer.verify(HttpRequest.request()
+ .withPath(PUSH_URL_PATH)
+ .withBody(json(
+ s"""{
+ | "@type": "PushVerification",
+ | "pushSubscriptionId": "$pushSubscriptionId",
+ | "verificationCode": "$${json-unit.any-string}"
+ |}""".stripMargin)),
+ VerificationTimes.atLeast(1))
+ }
+
+ // GIVEN bob retrieves the validation code from the mock server
+ val verificationCode: String = Json.parse(bodyRequestOnPushServer.get()).asInstanceOf[JsObject]
+ .value("verificationCode")
+ .asInstanceOf[JsString]
+ .value
+
+ // WHEN bob updates the validation code via JMAP
+ val updateVerificationCodeResponse: String = updateValidateVerificationCode(pushSubscriptionId, verificationCode)
+
+ // THEN it succeed
+ assertThatJson(updateVerificationCodeResponse)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [
+ | "PushSubscription/set",
+ | {
+ | "updated": {
+ | "$pushSubscriptionId": {}
+ | }
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+
+ // GIVEN 8 days passes
+ clock.setInstant(clock.instant().plus(8, ChronoUnit.DAYS))
+
+ // WHEN bob receives a mail
+ sendEmailToBob(server)
+
+ // THEN bob has a stateChange on the push gateway
+ TimeUnit.MILLISECONDS.sleep(200)
+
+ pushServer.verify(HttpRequest.request()
+ .withPath(PUSH_URL_PATH)
+ .withBody(json(
+ s"""{
+ | "@type": "StateChange",
+ | "changed": {
+ | "$ACCOUNT_ID": {
+ | "Mailbox": "$${json-unit.any-string}"
+ | }
+ | }
+ |}""".stripMargin)),
+ VerificationTimes.exactly(0))
+ }
+
+ @Test
+ def webPushShouldNotPushToPushServerWhenDeletedSubscription(server: GuiceJamesServer, pushServer: ClientAndServer): Unit = {
+ // Setup mock-server for callback
+ val bodyRequestOnPushServer: AtomicReference[String] = setupPushServerCallback(pushServer)
+
+ // WHEN bob creates a push subscription
+ val pushSubscriptionId: String = createPushSubscription(pushServer)
+ // THEN a validation code is sent
+ awaitAtMostTenSeconds.untilAsserted { () =>
+ pushServer.verify(HttpRequest.request()
+ .withPath(PUSH_URL_PATH)
+ .withBody(json(
+ s"""{
+ | "@type": "PushVerification",
+ | "pushSubscriptionId": "$pushSubscriptionId",
+ | "verificationCode": "$${json-unit.any-string}"
+ |}""".stripMargin)),
+ VerificationTimes.atLeast(1))
+ }
+
+ // GIVEN bob retrieves the validation code from the mock server
+ val verificationCode: String = Json.parse(bodyRequestOnPushServer.get()).asInstanceOf[JsObject]
+ .value("verificationCode")
+ .asInstanceOf[JsString]
+ .value
+
+ // WHEN bob updates the validation code via JMAP
+ val updateVerificationCodeResponse: String = updateValidateVerificationCode(pushSubscriptionId, verificationCode)
+
+ // THEN it succeed
+ assertThatJson(updateVerificationCodeResponse)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [
+ | "PushSubscription/set",
+ | {
+ | "updated": {
+ | "$pushSubscriptionId": {}
+ | }
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+
+ // GIVEN bob deletes the push subscription
+ val pushSubscriptionProbe: PushSubscriptionProbe = server.getProbe(classOf[PushSubscriptionProbe])
+
+ `given`
+ .body(
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core"],
+ | "methodCalls": [
+ | [
+ | "PushSubscription/set",
+ | {
+ | "destroy": ["$pushSubscriptionId"]
+ | },
+ | "c1"
+ | ]
+ | ]
+ | }""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+
+ // WHEN bob receives a mail
+ sendEmailToBob(server)
+
+ // THEN bob has no stateChange on the push gateway
+ TimeUnit.MILLISECONDS.sleep(200)
+
+ pushServer.verify(HttpRequest.request()
+ .withPath(PUSH_URL_PATH)
+ .withBody(json(
+ s"""{
+ | "@type": "StateChange",
+ | "changed": {
+ | "$ACCOUNT_ID": {
+ | "Mailbox": "$${json-unit.any-string}"
+ | }
+ | }
+ |}""".stripMargin)),
+ VerificationTimes.exactly(0))
+ }
+
+ @Test
+ def webPushShouldNotPushToPushServerWhenNotValidatedCode(server: GuiceJamesServer, pushServer: ClientAndServer): Unit = {
+ // Setup mock-server for callback
+ setupPushServerCallback(pushServer)
+
+ // WHEN bob creates a push subscription [no code validation]
+ createPushSubscription(pushServer)
+
+ // GIVEN bob receives a mail
+ sendEmailToBob(server)
+
+ // THEN bob has no stateChange on the push gateway
+ TimeUnit.MILLISECONDS.sleep(200)
+
+ pushServer.verify(HttpRequest.request()
+ .withPath(PUSH_URL_PATH)
+ .withBody(json(
+ s"""{
+ | "@type": "StateChange",
+ | "changed": {
+ | "$ACCOUNT_ID": {
+ | "Mailbox": "$${json-unit.any-string}"
+ | }
+ | }
+ |}""".stripMargin)),
+ VerificationTimes.exactly(0))
+ }
+
+ @Test
+ def correctBehaviourShouldSuccessWhenEncryptionKeys(server: GuiceJamesServer, pushServer: ClientAndServer): Unit = {
+ // Setup mock-server for callback
+ val bodyRequestOnPushServer: AtomicReference[Array[Byte]] = new AtomicReference()
+
+ pushServer
+ .when(request
+ .withPath(PUSH_URL_PATH)
+ .withMethod("POST"))
+ .respond(new ExpectationResponseCallback() {
+ override def handle(httpRequest: HttpRequest): HttpResponse = {
+ bodyRequestOnPushServer.set(httpRequest.getBodyAsRawBytes)
+ response()
+ .withStatusCode(HttpStatus.SC_CREATED)
+ }
+ })
+
+ val uaKeyPair: KeyPair = EllipticCurves.generateKeyPair(CurveType.NIST_P256)
+ val uaPublicKey: ECPublicKey = uaKeyPair.getPublic.asInstanceOf[ECPublicKey]
+ val uaPrivateKey: ECPrivateKey = uaKeyPair.getPrivate.asInstanceOf[ECPrivateKey]
+ val authSecret: Array[Byte] = "secret123secret1".getBytes
+
+ val p256dh: String = Base64.getUrlEncoder.encodeToString(uaPublicKey.getEncoded)
+ val auth: String = Base64.getUrlEncoder.encodeToString(authSecret)
+
+ val pushSubscriptionId: String = `given`
+ .body(
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core"],
+ | "methodCalls": [
+ | [
+ | "PushSubscription/set",
+ | {
+ | "create": {
+ | "4f29": {
+ | "deviceClientId": "a889-ffea-910",
+ | "url": "${getPushServerUrl(pushServer)}",
+ | "types": ["Mailbox"],
+ | "keys": {
+ | "p256dh": "$p256dh",
+ | "auth": "$auth"
+ | }
+ | }
+ | }
+ | },
+ | "c1"
+ | ]
+ | ]
+ | }""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .extract()
+ .jsonPath()
+ .get("methodResponses[0][1].created.4f29.id")
+
+ // THEN a validation code is sent
+ awaitAtMostTenSeconds.untilAsserted { () =>
+ pushServer.verify(HttpRequest.request()
+ .withPath(PUSH_URL_PATH)
+ .withBody(not(json(
+ s"""{
+ | "@type": "PushVerification",
+ | "pushSubscriptionId": "$pushSubscriptionId",
+ | "verificationCode": "$${json-unit.any-string}"
+ |}""".stripMargin))),
+ VerificationTimes.atLeast(1))
+ }
+
+ val hybridDecrypt: WebPushHybridDecrypt = new WebPushHybridDecrypt.Builder()
+ .withAuthSecret(authSecret)
+ .withRecipientPublicKey(uaPublicKey)
+ .withRecipientPrivateKey(uaPrivateKey)
+ .build
+
+ val decryptBodyRequestOnPushServer: String = new String(hybridDecrypt.decrypt(bodyRequestOnPushServer.get(), null), StandardCharsets.UTF_8)
+
+ // GIVEN bob retrieves the validation code from the mock server
+ val verificationCode: String = Json.parse(decryptBodyRequestOnPushServer).asInstanceOf[JsObject]
+ .value("verificationCode")
+ .asInstanceOf[JsString]
+ .value
+
+ // WHEN bob updates the validation code via JMAP
+ updateValidateVerificationCode(pushSubscriptionId, verificationCode)
+
+ // WHEN bob receives a mail
+ sendEmailToBob(server)
+
+ // THEN bob has a stateChange on the push gateway
+ awaitAtMostTenSeconds.untilAsserted { () =>
+ pushServer.verify(HttpRequest.request()
+ .withPath(PUSH_URL_PATH),
+ VerificationTimes.atLeast(1))
+ }
+
+ assertThatJson(new String(hybridDecrypt.decrypt(bodyRequestOnPushServer.get(), null), StandardCharsets.UTF_8))
+ .isEqualTo(
+ s"""{
+ | "@type": "StateChange",
+ | "changed": {
+ | "$ACCOUNT_ID": {
+ | "Mailbox": "$${json-unit.ignore}"
+ | }
+ | }
+ |}""".stripMargin)
+ }
+}
diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JmapEventBusModule.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryWebPushTest.java
similarity index 52%
copy from server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JmapEventBusModule.java
copy to server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryWebPushTest.java
index 0fafb26..d0d6b5c 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JmapEventBusModule.java
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryWebPushTest.java
@@ -17,17 +17,27 @@
* under the License. *
****************************************************************/
-package org.apache.james.modules.protocols;
+package org.apache.james.jmap.rfc8621.memory;
-import org.apache.james.events.EventBus;
-import org.apache.james.jmap.InjectionKeys;
+import org.apache.james.ClockExtension;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.MemoryJamesServerMain;
+import org.apache.james.jmap.rfc8621.contract.PushServerExtension;
+import org.apache.james.jmap.rfc8621.contract.PushSubscriptionProbeModule;
+import org.apache.james.jmap.rfc8621.contract.WebPushContract;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.extension.RegisterExtension;
-import com.google.inject.AbstractModule;
-import com.google.inject.name.Names;
+public class MemoryWebPushTest implements WebPushContract {
-public class JmapEventBusModule extends AbstractModule {
- @Override
- protected void configure() {
- bind(EventBus.class).annotatedWith(Names.named(InjectionKeys.JMAP)).to(EventBus.class);
- }
+ @RegisterExtension
+ static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+ .server(configuration -> MemoryJamesServerMain.createServer(configuration)
+ .overrideWith(new TestJMAPServerModule(), new PushSubscriptionProbeModule()))
+ .extension(new ClockExtension())
+ .build();
+
+ @RegisterExtension
+ static PushServerExtension pushServerExtension = new PushServerExtension();
}
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org