You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by GitBox <gi...@apache.org> on 2021/11/03 10:54:43 UTC

[GitHub] [james-project] vttranlina opened a new pull request #736: JAMES-3539 JMAP webpush integration test

vttranlina opened a new pull request #736:
URL: https://github.com/apache/james-project/pull/736


   ref: https://github.com/linagora/james-project/issues/4419
   Jira: https://issues.apache.org/jira/browse/JAMES-3539


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r743638526



##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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
+  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": "$${json-unit.ignore-element}"

Review comment:
       Yes because the types of the push subscription is only `["Mailbox"]` (I asked myself the exact same question)




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] vttranlina commented on pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
vttranlina commented on pull request #736:
URL: https://github.com/apache/james-project/pull/736#issuecomment-958912088


   I will create `ClockExtension` in James common for reusable. 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] vttranlina commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
vttranlina commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r742617480



##########
File path: server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java
##########
@@ -129,6 +130,7 @@ protected void configure() {
 
         Multibinder.newSetBinder(binder(), EventListener.GroupEventListener.class).addBinding().to(PropagateLookupRightListener.class);
         Multibinder.newSetBinder(binder(), EventListener.ReactiveGroupEventListener.class).addBinding().to(MailboxChangeListener.class);
+        Multibinder.newSetBinder(binder(), EventListener.ReactiveGroupEventListener.class).addBinding().to(PushListener.class);

Review comment:
       `server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java` is the regular event bus?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] vttranlina commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
vttranlina commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r743350918



##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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
+  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": "$${json-unit.ignore-element}"

Review comment:
       For a new email, the push listener sent one message looks like:
   {
     "@type": "StateChange",
     "changed": {
       "a3123": {
         "Mailbox": "d35ecb040aab"
       }, 
       // ...
     }
   }
   
   is this normal?

##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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
+  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": "$${json-unit.ignore-element}"

Review comment:
       For a new email, the push listener sent one message looks like:
   ```
   {
     "@type": "StateChange",
     "changed": {
       "a3123": {
         "Mailbox": "d35ecb040aab"
       }, 
       // ...
     }
   }
   ```
   is this normal?

##########
File path: 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);

Review comment:
       I registered it in `org.apache.james.modules.event.JMAPEventBusModule`  (distributed package)
   
   ```
       @ProvidesIntoSet
       InitializationOperation workQueue(@Named(InjectionKeys.JMAP) RabbitMQEventBus instance, PushListener pushListener) {
           return InitilizationOperationBuilder
               .forClass(RabbitMQEventBus.class)
               .init(() -> {
                   instance.start();
                   instance.register(pushListener);
               });
       }
   ```
   
   It also works, but I don't sure it is good code convention

##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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
+  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": "$${json-unit.ignore-element}"

Review comment:
       For a new email, the push listener sent one message looks like:
   {
     "@type": "StateChange",
     "changed": {
       "a3123": {
         "Mailbox": "d35ecb040aab"
       }, 
       // ...
     }
   }
   
   is this normal?

##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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
+  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": "$${json-unit.ignore-element}"

Review comment:
       For a new email, the push listener sent one message looks like:
   ```
   {
     "@type": "StateChange",
     "changed": {
       "a3123": {
         "Mailbox": "d35ecb040aab"
       }, 
       // ...
     }
   }
   ```
   is this normal?

##########
File path: 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);

Review comment:
       I registered it in `org.apache.james.modules.event.JMAPEventBusModule`  (distributed package)
   
   ```
       @ProvidesIntoSet
       InitializationOperation workQueue(@Named(InjectionKeys.JMAP) RabbitMQEventBus instance, PushListener pushListener) {
           return InitilizationOperationBuilder
               .forClass(RabbitMQEventBus.class)
               .init(() -> {
                   instance.start();
                   instance.register(pushListener);
               });
       }
   ```
   
   It also works, but I don't sure it is good code convention




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa commented on pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa commented on pull request #736:
URL: https://github.com/apache/james-project/pull/736#issuecomment-961594222


   (Rebase conflicts)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa merged pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa merged pull request #736:
URL: https://github.com/apache/james-project/pull/736


   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r742496796



##########
File path: server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java
##########
@@ -129,6 +130,7 @@ protected void configure() {
 
         Multibinder.newSetBinder(binder(), EventListener.GroupEventListener.class).addBinding().to(PropagateLookupRightListener.class);
         Multibinder.newSetBinder(binder(), EventListener.ReactiveGroupEventListener.class).addBinding().to(MailboxChangeListener.class);
+        Multibinder.newSetBinder(binder(), EventListener.ReactiveGroupEventListener.class).addBinding().to(PushListener.class);

Review comment:
       We need to register this on the JMAP event bus (not on the regular event bus)
   
    -> With memory the regular event bus and the JMAP event buses are the same
     -> But not for the distributed server.
     
   Thus, in its current version I expect those tests to break for the distributed version.
   
   Can we add the tests for the distributed version?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] vttranlina commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
vttranlina commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r742616515



##########
File path: server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java
##########
@@ -129,6 +130,7 @@ protected void configure() {
 
         Multibinder.newSetBinder(binder(), EventListener.GroupEventListener.class).addBinding().to(PropagateLookupRightListener.class);
         Multibinder.newSetBinder(binder(), EventListener.ReactiveGroupEventListener.class).addBinding().to(MailboxChangeListener.class);
+        Multibinder.newSetBinder(binder(), EventListener.ReactiveGroupEventListener.class).addBinding().to(PushListener.class);

Review comment:
       I registered it at `server/container/guice/protocols/jmap/src/main/java/org/apache/james/modules/protocols/JmapEventBusModule.java` 
   ```java
    Multibinder.newSetBinder(binder(), EventListener.ReactiveGroupEventListener.class).addBinding().to(PushListener.class);
    ```
   But It still not working
   




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r742909434



##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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
+  def correctBehaviourShouldSuccess(server: GuiceJamesServer, pushServer: ClientAndServer): Unit = {

Review comment:
       ```suggestion
     @Test
     @Tag(CategoryTags.BASIC_FEATURE)
     def correctBehaviourShouldSuccess(server: GuiceJamesServer, pushServer: ClientAndServer): Unit = {
   ```




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] vttranlina commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
vttranlina commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r743351526



##########
File path: 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);

Review comment:
       I registered it in `org.apache.james.modules.event.JMAPEventBusModule`  (distributed package)
   
   ```
       @ProvidesIntoSet
       InitializationOperation workQueue(@Named(InjectionKeys.JMAP) RabbitMQEventBus instance, PushListener pushListener) {
           return InitilizationOperationBuilder
               .forClass(RabbitMQEventBus.class)
               .init(() -> {
                   instance.start();
                   instance.register(pushListener);
               });
       }
   ```
   
   It also works, but I don't sure it is good code convention




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] vttranlina commented on pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
vttranlina commented on pull request #736:
URL: https://github.com/apache/james-project/pull/736#issuecomment-958912088






-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r742892338



##########
File path: 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);

Review comment:
       Replace this by
   
   ```
       @ProvidesIntoSet
       InitializationOperation registerPushListener(@Named(InjectionKeys.JMAP) EventBus jmapEventBus, PushListener pushListener) {
           return InitilizationOperationBuilder
               .forClass(PushListener.class)
               .init(() -> jmapEventBUs.register(pushListener));
       }
   ```




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa commented on pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa commented on pull request #736:
URL: https://github.com/apache/james-project/pull/736#issuecomment-961594222


   (Rebase conflicts)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa commented on pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa commented on pull request #736:
URL: https://github.com/apache/james-project/pull/736#issuecomment-961594222


   (Rebase conflicts)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] vttranlina commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
vttranlina commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r743350918



##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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
+  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": "$${json-unit.ignore-element}"

Review comment:
       For a new email, the push listener sent one message looks like:
   {
     "@type": "StateChange",
     "changed": {
       "a3123": {
         "Mailbox": "d35ecb040aab"
       }, 
       // ...
     }
   }
   
   is this normal?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r742909103



##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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
+  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": "$${json-unit.ignore-element}"

Review comment:
       ```suggestion
                |        "$ACCOUNT_ID": {"Email":"$${json-unit.ignore-element}","EmailDelivery":"$${json-unit.ignore-element}","Mailbox":"$${json-unit.ignore-element}"}
   ```
   
   Is likely the correct format for StateChange
   
   CF https://jmap.io/spec-core.html#the-statechange-object
   
   ```
   {
     "@type": "StateChange",
     "changed": {
       "a3123": {
         "Email": "d35ecb040aab",
         "EmailDelivery": "428d565f2440",
         "CalendarEvent": "87accfac587a"
       }, 
       // ...
     }
   }
   ```




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] Arsnael commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
Arsnael commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r742640768



##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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
+  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": "$${json-unit.ignore-element}"
+             |    }
+             |}""".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": "$${json-unit.ignore-element}"
+           |    }
+           |}""".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])

Review comment:
       Don't forget to use the revoke method instead of the probe (and to remove the revoke method you created on the probe) when it's merged : https://github.com/apache/james-project/pull/726
   
   It's stable and approved, just need a green build now to merge it...

##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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
+  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": "$${json-unit.ignore-element}"
+             |    }
+             |}""".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": "$${json-unit.ignore-element}"
+           |    }
+           |}""".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])
+    pushSubscriptionProbe.revoke(BOB, PushSubscriptionId(UUID.fromString(pushSubscriptionId)))
+
+    // 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": "$${json-unit.ignore-element}"
+           |    }
+           |}""".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": "$${json-unit.ignore-element}"
+           |    }
+           |}""".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.getEncoder.encodeToString(uaPublicKey.getEncoded)
+    val auth: String = Base64.getEncoder.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

Review comment:
       indent

##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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
+  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": "$${json-unit.ignore-element}"
+             |    }
+             |}""".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": "$${json-unit.ignore-element}"
+           |    }
+           |}""".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])
+    pushSubscriptionProbe.revoke(BOB, PushSubscriptionId(UUID.fromString(pushSubscriptionId)))
+
+    // 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": "$${json-unit.ignore-element}"
+           |    }
+           |}""".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": "$${json-unit.ignore-element}"
+           |    }
+           |}""".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.getEncoder.encodeToString(uaPublicKey.getEncoded)
+    val auth: String = Base64.getEncoder.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`

Review comment:
       indent




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r743638526



##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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
+  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": "$${json-unit.ignore-element}"

Review comment:
       Yes because the types of the push subscription is only `["Mailbox"]` (I asked myself the exact same question)




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] vttranlina commented on pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
vttranlina commented on pull request #736:
URL: https://github.com/apache/james-project/pull/736#issuecomment-958912088


   I will create `ClockExtension` in James common for reusable. 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r742498439



##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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, bodyRequestOnPushServer: AtomicReference[String]): Unit =
+    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)
+        }
+      })

Review comment:
       I prefer to return a string and keep the mutability with the AtomicReference a (very) local implementation detail.
   
   Something like :
   
   ```suggestion
     private def setupPushServerCallback(pushServer: ClientAndServer): String = {
       val bodyRequestOnPushServer: AtomicReference[String] = new AtomicReference()
       // ...
      }
   ```
   
   Or at the very least we could return the atomic ref:
   
   ```suggestion
     private def setupPushServerCallback(pushServer: ClientAndServer): AtomicReference[String] = {
       val bodyRequestOnPushServer: AtomicReference[String] = new AtomicReference()
       // ...
      }
   ```

##########
File path: server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryWebPushTest.java
##########
@@ -0,0 +1,76 @@
+/****************************************************************
+ * 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 java.time.Clock;
+import java.time.Instant;
+import java.time.ZonedDateTime;
+
+import org.apache.james.GuiceModuleTestExtension;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.MemoryJamesServerMain;
+import org.apache.james.jmap.pushsubscription.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.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 org.junit.jupiter.api.extension.RegisterExtension;
+
+import com.google.inject.Module;
+
+public class MemoryWebPushTest implements WebPushContract {
+    public static 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;
+        }
+
+        @Override
+        public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
+            return clock;
+        }
+    }

Review comment:
       Should be in WebPushContract ?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] vttranlina commented on pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
vttranlina commented on pull request #736:
URL: https://github.com/apache/james-project/pull/736#issuecomment-960454527


   PushListener doesn't receive any `StateChangeEvent` event when distributed tests. (work with memory tests)
   I wrong something?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r742499775



##########
File path: server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryWebPushTest.java
##########
@@ -0,0 +1,76 @@
+/****************************************************************
+ * 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 java.time.Clock;
+import java.time.Instant;
+import java.time.ZonedDateTime;
+
+import org.apache.james.GuiceModuleTestExtension;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.MemoryJamesServerMain;
+import org.apache.james.jmap.pushsubscription.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.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 org.junit.jupiter.api.extension.RegisterExtension;
+
+import com.google.inject.Module;
+
+public class MemoryWebPushTest implements WebPushContract {
+    public static 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;
+        }
+
+        @Override
+        public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
+            return clock;
+        }
+    }

Review comment:
       Should be in WebPushContract ?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r742496796



##########
File path: server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/draft/JMAPModule.java
##########
@@ -129,6 +130,7 @@ protected void configure() {
 
         Multibinder.newSetBinder(binder(), EventListener.GroupEventListener.class).addBinding().to(PropagateLookupRightListener.class);
         Multibinder.newSetBinder(binder(), EventListener.ReactiveGroupEventListener.class).addBinding().to(MailboxChangeListener.class);
+        Multibinder.newSetBinder(binder(), EventListener.ReactiveGroupEventListener.class).addBinding().to(PushListener.class);

Review comment:
       We need to register this on the JMAP event bus (not on the regular event bus)




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa commented on pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa commented on pull request #736:
URL: https://github.com/apache/james-project/pull/736#issuecomment-960474337


   > I wrong something?
   
   https://github.com/apache/james-project/pull/736#discussion_r742496796


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] vttranlina commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
vttranlina commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r743350918



##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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
+  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": "$${json-unit.ignore-element}"

Review comment:
       For a new email, the push listener sent one message looks like:
   ```
   {
     "@type": "StateChange",
     "changed": {
       "a3123": {
         "Mailbox": "d35ecb040aab"
       }, 
       // ...
     }
   }
   ```
   is this normal?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [james-project] chibenwa commented on a change in pull request #736: JAMES-3539 JMAP webpush integration test

Posted by GitBox <gi...@apache.org>.
chibenwa commented on a change in pull request #736:
URL: https://github.com/apache/james-project/pull/736#discussion_r743638526



##########
File path: 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,508 @@
+/****************************************************************
+ * 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.api.model.PushSubscriptionId
+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.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, 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.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.{Base64, UUID}
+
+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
+  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": "$${json-unit.ignore-element}"

Review comment:
       Yes because the types of the push subscription is only `["Mailbox"]` (I asked myself the exact same question)




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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