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

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


The following commit(s) were added to refs/heads/master by this push:
     new b4c8c62f80 JAMES-3539 Types field in push subscription should accept 
null value (#2723)
b4c8c62f80 is described below

commit b4c8c62f80d8b7ffe131f0904639486aa05000b1
Author: Trần Hồng Quân <55171818+quantranhong1...@users.noreply.github.com>
AuthorDate: Tue May 20 09:45:22 2025 +0700

    JAMES-3539 Types field in push subscription should accept null value (#2723)
    
    According to the rfc 
https://datatracker.ietf.org/doc/html/rfc8620#section-7.2 PushSubscription/set 
should accept a null value for types, and in this case register the push ith 
all the types available on the server.
    
    We need to fix this to be RFC compliant.
---
 .../CassandraPushSubscriptionRepository.java       |   8 +-
 .../CassandraPushSubscriptionRepositoryTest.java   |   3 +-
 .../PostgresPushSubscriptionRepository.java        |   3 +-
 .../MemoryPushSubscriptionRepository.java          |   8 +-
 .../james/jmap/api/change/TypeStateFactory.scala   |   2 +-
 .../james/jmap/api/model/PushSubscription.scala    |  15 +-
 .../MemoryPushSubscriptionRepositoryTest.java      |   6 +-
 .../PushDeleteUserDataTaskStepTest.scala           |   9 +-
 .../PushSubscriptionRepositoryContract.scala       |  52 +++---
 .../DistributedPushSubscriptionSetMethodTest.java  |   3 +-
 .../PushSubscriptionSetMethodContract.scala        | 174 ++++++++++++++++++---
 .../rfc8621/contract/probe/TypeStateProbe.scala}   |  75 ++++-----
 .../MemoryPushSubscriptionSetMethodTest.java       |   3 +-
 .../PostgresPushSubscriptionSetMethodTest.java     |   2 +
 .../james/jmap/core/PushSubscriptionSet.scala      |   9 +-
 .../method/PushSubscriptionUpdatePerformer.scala   |   5 +-
 .../jmap/pushsubscription/PushListenerTest.scala   |  27 ++--
 .../memory/MemoryUserDeletionIntegrationTest.java  |   2 +-
 18 files changed, 281 insertions(+), 125 deletions(-)

diff --git 
a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionRepository.java
 
b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionRepository.java
index c17dfc807f..c9962a28e1 100644
--- 
a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionRepository.java
+++ 
b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionRepository.java
@@ -31,6 +31,7 @@ import java.util.Set;
 import jakarta.inject.Inject;
 
 import org.apache.james.core.Username;
+import org.apache.james.jmap.api.change.TypeStateFactory;
 import org.apache.james.jmap.api.model.DeviceClientIdInvalidException;
 import org.apache.james.jmap.api.model.ExpireTimeInvalidException;
 import org.apache.james.jmap.api.model.InvalidPushSubscriptionKeys;
@@ -50,17 +51,20 @@ import scala.jdk.javaapi.OptionConverters;
 public class CassandraPushSubscriptionRepository implements 
PushSubscriptionRepository {
     private final CassandraPushSubscriptionDAO dao;
     private final Clock clock;
+    private final TypeStateFactory typeStateFactory;
 
     @Inject
-    public CassandraPushSubscriptionRepository(CassandraPushSubscriptionDAO 
dao, Clock clock) {
+    public CassandraPushSubscriptionRepository(CassandraPushSubscriptionDAO 
dao, Clock clock, TypeStateFactory typeStateFactory) {
         this.dao = dao;
         this.clock = clock;
+        this.typeStateFactory = typeStateFactory;
     }
 
     @Override
     public Publisher<PushSubscription> save(Username username, 
PushSubscriptionCreationRequest request) {
         PushSubscription pushSubscription = PushSubscription.from(request,
-            
evaluateExpiresTime(OptionConverters.toJava(request.expires().map(PushSubscriptionExpiredTime::value)),
 clock));
+            
evaluateExpiresTime(OptionConverters.toJava(request.expires().map(PushSubscriptionExpiredTime::value)),
 clock),
+            typeStateFactory);
 
         return isDuplicatedDeviceClientId(username, request.deviceClientId())
             .handle((isDuplicated, sink) -> {
diff --git 
a/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionRepositoryTest.java
 
b/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionRepositoryTest.java
index 24d5a1aa0c..1420d63779 100644
--- 
a/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionRepositoryTest.java
+++ 
b/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionRepositoryTest.java
@@ -47,7 +47,8 @@ public class CassandraPushSubscriptionRepositoryTest 
implements PushSubscription
         pushSubscriptionRepository = new CassandraPushSubscriptionRepository(
             new CassandraPushSubscriptionDAO(cassandra.getConf(),
                 new 
TypeStateFactory(CollectionConverters.asJava(PushSubscriptionRepositoryContract.TYPE_NAME_SET()))),
-            clock);
+            clock,
+            new 
TypeStateFactory(CollectionConverters.asJava(PushSubscriptionRepositoryContract.TYPE_NAME_SET())));
     }
 
     @Override
diff --git 
a/server/data/data-jmap-postgres/src/main/java/org/apache/james/jmap/postgres/pushsubscription/PostgresPushSubscriptionRepository.java
 
b/server/data/data-jmap-postgres/src/main/java/org/apache/james/jmap/postgres/pushsubscription/PostgresPushSubscriptionRepository.java
index 9e48a2d421..0a6a342cf2 100644
--- 
a/server/data/data-jmap-postgres/src/main/java/org/apache/james/jmap/postgres/pushsubscription/PostgresPushSubscriptionRepository.java
+++ 
b/server/data/data-jmap-postgres/src/main/java/org/apache/james/jmap/postgres/pushsubscription/PostgresPushSubscriptionRepository.java
@@ -68,7 +68,8 @@ public class PostgresPushSubscriptionRepository implements 
PushSubscriptionRepos
         return validateCreationRequest(request)
             .then(Mono.defer(() -> {
                 PushSubscription pushSubscription = 
PushSubscription.from(request,
-                    
evaluateExpiresTime(OptionConverters.toJava(request.expires().map(PushSubscriptionExpiredTime::value)),
 clock));
+                    
evaluateExpiresTime(OptionConverters.toJava(request.expires().map(PushSubscriptionExpiredTime::value)),
 clock),
+                    typeStateFactory);
 
                 return pushSubscriptionDAO.save(username, pushSubscription)
                     .thenReturn(pushSubscription);
diff --git 
a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepository.java
 
b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepository.java
index 034d0e97d6..9ef8ccb279 100644
--- 
a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepository.java
+++ 
b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepository.java
@@ -32,6 +32,7 @@ import java.util.Set;
 import jakarta.inject.Inject;
 
 import org.apache.james.core.Username;
+import org.apache.james.jmap.api.change.TypeStateFactory;
 import org.apache.james.jmap.api.model.DeviceClientIdInvalidException;
 import org.apache.james.jmap.api.model.ExpireTimeInvalidException;
 import org.apache.james.jmap.api.model.InvalidPushSubscriptionKeys;
@@ -55,11 +56,13 @@ import scala.jdk.javaapi.OptionConverters;
 public class MemoryPushSubscriptionRepository implements 
PushSubscriptionRepository {
     private final Table<Username, PushSubscriptionId, PushSubscription> table;
     private final Clock clock;
+    private final TypeStateFactory typeStateFactory;
 
     @Inject
-    public MemoryPushSubscriptionRepository(Clock clock) {
+    public MemoryPushSubscriptionRepository(Clock clock, TypeStateFactory 
typeStateFactory) {
         this.clock = clock;
         this.table = HashBasedTable.create();
+        this.typeStateFactory = typeStateFactory;
     }
 
     @Override
@@ -78,7 +81,8 @@ public class MemoryPushSubscriptionRepository implements 
PushSubscriptionReposit
             })
             .thenReturn(PushSubscription.from(request,
                 
evaluateExpiresTime(OptionConverters.toJava(request.expires().map(PushSubscriptionExpiredTime::value)),
-                    clock)))
+                    clock),
+                    typeStateFactory))
             .doOnNext(pushSubscription -> table.put(username, 
pushSubscription.id(), pushSubscription));
     }
 
diff --git 
a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/change/TypeStateFactory.scala
 
b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/change/TypeStateFactory.scala
index a2c49af270..47e5f94e8d 100644
--- 
a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/change/TypeStateFactory.scala
+++ 
b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/change/TypeStateFactory.scala
@@ -27,7 +27,7 @@ import scala.jdk.CollectionConverters._
 
 case class TypeStateFactory @Inject()(setTypeName: java.util.Set[TypeName]) {
   val logger: Logger = LoggerFactory.getLogger(classOf[TypeStateFactory])
-  val all: scala.collection.mutable.Set[TypeName] = setTypeName.asScala
+  val all: Set[TypeName] = setTypeName.asScala.toSet
 
   def strictParse(string: String): Either[IllegalArgumentException, TypeName] =
     all.flatMap(_.parse(string))
diff --git 
a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/PushSubscription.scala
 
b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/PushSubscription.scala
index 011345df9f..9055beee4b 100644
--- 
a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/PushSubscription.scala
+++ 
b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/PushSubscription.scala
@@ -28,6 +28,7 @@ import java.util.{Base64, UUID}
 import com.google.crypto.tink.HybridEncrypt
 import com.google.crypto.tink.apps.webpush.WebPushHybridEncrypt
 import com.google.crypto.tink.subtle.EllipticCurves
+import org.apache.james.jmap.api.change.TypeStateFactory
 import 
org.apache.james.jmap.api.model.ExpireTimeInvalidException.TIME_FORMATTER
 
 import scala.util.Try
@@ -96,7 +97,7 @@ case class PushSubscriptionCreationRequest(deviceClientId: 
DeviceClientId,
                                            url: PushSubscriptionServerURL,
                                            keys: Option[PushSubscriptionKeys] 
= None,
                                            expires: 
Option[PushSubscriptionExpiredTime] = None,
-                                           types: Seq[TypeName]) {
+                                           types: Option[Seq[TypeName]] = 
None) {
 
   def validate: Either[IllegalArgumentException, 
PushSubscriptionCreationRequest] =
     for {
@@ -107,10 +108,9 @@ case class PushSubscriptionCreationRequest(deviceClientId: 
DeviceClientId,
     }
 
   private def validateTypes: Either[IllegalArgumentException, 
PushSubscriptionCreationRequest] =
-    if (types.isEmpty) {
-      scala.Left(new IllegalArgumentException("types must not be empty"))
-    } else {
-      Right(this)
+    types match {
+      case Some(Seq()) => scala.Left(new IllegalArgumentException("types must 
not be empty"))
+      case _ => Right(this)
     }
 
   private def validateKeys: Either[IllegalArgumentException, 
PushSubscriptionCreationRequest] =
@@ -123,7 +123,8 @@ object PushSubscription {
   val EXPIRES_TIME_MAX_DAY: Int = 7
 
   def from(creationRequest: PushSubscriptionCreationRequest,
-           expireTime: PushSubscriptionExpiredTime): PushSubscription =
+           expireTime: PushSubscriptionExpiredTime,
+           typeStateFactory: TypeStateFactory): PushSubscription =
     PushSubscription(id = PushSubscriptionId.generate(),
       deviceClientId = creationRequest.deviceClientId,
       url = creationRequest.url,
@@ -131,7 +132,7 @@ object PushSubscription {
       verificationCode = VerificationCode.generate(),
       validated = !VALIDATED,
       expires = expireTime,
-      types = creationRequest.types)
+      types = creationRequest.types.getOrElse(typeStateFactory.all.toSeq))
 }
 
 case class PushSubscription(id: PushSubscriptionId,
diff --git 
a/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepositoryTest.java
 
b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepositoryTest.java
index 19c38c5ed4..3d06cacaf8 100644
--- 
a/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepositoryTest.java
+++ 
b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepositoryTest.java
@@ -19,11 +19,14 @@
 
 package org.apache.james.jmap.memory.pushsubscription;
 
+import org.apache.james.jmap.api.change.TypeStateFactory;
 import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository;
 import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepositoryContract;
 import org.apache.james.utils.UpdatableTickingClock;
 import org.junit.jupiter.api.BeforeEach;
 
+import scala.jdk.javaapi.CollectionConverters;
+
 public class MemoryPushSubscriptionRepositoryTest implements 
PushSubscriptionRepositoryContract {
     UpdatableTickingClock clock;
     PushSubscriptionRepository pushSubscriptionRepository;
@@ -31,7 +34,8 @@ public class MemoryPushSubscriptionRepositoryTest implements 
PushSubscriptionRep
     @BeforeEach
     void setup() {
         clock = new 
UpdatableTickingClock(PushSubscriptionRepositoryContract.NOW());
-        pushSubscriptionRepository = new 
MemoryPushSubscriptionRepository(clock);
+        pushSubscriptionRepository = new 
MemoryPushSubscriptionRepository(clock,
+            new 
TypeStateFactory(CollectionConverters.asJava(PushSubscriptionRepositoryContract.TYPE_NAME_SET())));
     }
 
     @Override
diff --git 
a/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushDeleteUserDataTaskStepTest.scala
 
b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushDeleteUserDataTaskStepTest.scala
index c039ca8d83..0a557fa5e6 100644
--- 
a/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushDeleteUserDataTaskStepTest.scala
+++ 
b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushDeleteUserDataTaskStepTest.scala
@@ -19,9 +19,10 @@
 
 package org.apache.james.jmap.api.pushsubscription
 
-import java.net.{URI, URL}
+import java.net.URI
 import java.time.Clock
 
+import org.apache.james.jmap.api.change.TypeStateFactory
 import org.apache.james.jmap.api.identity.CustomIdentityDAOContract.bob
 import org.apache.james.jmap.api.model.{DeviceClientId, 
PushSubscriptionCreationRequest, PushSubscriptionServerURL}
 import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepositoryContract.ALICE
@@ -31,13 +32,15 @@ import org.junit.jupiter.api.{BeforeEach, Test}
 import reactor.core.publisher.Flux
 import reactor.core.scala.publisher.SMono
 
+import scala.jdk.javaapi.CollectionConverters
+
 class PushDeleteUserDataTaskStepTest {
   var pushSubscriptionRepository: PushSubscriptionRepository = _
   var testee: PushDeleteUserDataTaskStep = _
 
   @BeforeEach
   def beforeEach(): Unit = {
-    pushSubscriptionRepository = new 
MemoryPushSubscriptionRepository(Clock.systemUTC())
+    pushSubscriptionRepository = new 
MemoryPushSubscriptionRepository(Clock.systemUTC(), 
TypeStateFactory(CollectionConverters.asJava(PushSubscriptionRepositoryContract.TYPE_NAME_SET)))
     testee = new PushDeleteUserDataTaskStep(pushSubscriptionRepository)
   }
 
@@ -52,7 +55,7 @@ class PushDeleteUserDataTaskStepTest {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     SMono.fromPublisher(pushSubscriptionRepository.save(ALICE, 
validRequest)).block().id
 
     SMono(testee.deleteUserData(ALICE)).block()
diff --git 
a/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushSubscriptionRepositoryContract.scala
 
b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushSubscriptionRepositoryContract.scala
index 7b2caba10c..ff3d2ee50a 100644
--- 
a/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushSubscriptionRepositoryContract.scala
+++ 
b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/pushsubscription/PushSubscriptionRepositoryContract.scala
@@ -80,7 +80,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block().id
     val singleRecordSaved = SFlux.fromPublisher(testee.get(ALICE, 
Set(pushSubscriptionId).asJava)).count().block()
 
@@ -92,7 +92,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block().id
     val newSavedSubscription = SFlux.fromPublisher(testee.get(ALICE, 
Set(pushSubscriptionId).asJava)).blockFirst().get
 
@@ -105,7 +105,7 @@ trait PushSubscriptionRepositoryContract {
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
       expires = Some(PushSubscriptionExpiredTime(VALID_EXPIRE.plusDays(8))),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
request)).block().id
     val newSavedSubscription = SFlux.fromPublisher(testee.get(ALICE, 
Set(pushSubscriptionId).asJava)).blockFirst().get
 
@@ -118,7 +118,7 @@ trait PushSubscriptionRepositoryContract {
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
       expires = Some(PushSubscriptionExpiredTime(INVALID_EXPIRE)),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
 
     assertThatThrownBy(() => SMono.fromPublisher(testee.save(ALICE, 
invalidRequest)).block())
       .isInstanceOf(classOf[ExpireTimeInvalidException])
@@ -129,13 +129,13 @@ trait PushSubscriptionRepositoryContract {
     val firstRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     SMono.fromPublisher(testee.save(ALICE, firstRequest)).block()
 
     val secondRequestWithDuplicatedDeviceClientId = 
PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
 
     assertThatThrownBy(() => SMono.fromPublisher(testee.save(ALICE, 
secondRequestWithDuplicatedDeviceClientId)).block())
       .isInstanceOf(classOf[DeviceClientIdInvalidException])
@@ -146,7 +146,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block().id
 
     assertThatThrownBy(() => 
SMono.fromPublisher(testee.updateExpireTime(ALICE, pushSubscriptionId, 
INVALID_EXPIRE)).block())
@@ -158,7 +158,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block().id
     SMono.fromPublisher(testee.updateExpireTime(ALICE, pushSubscriptionId, 
MAX_EXPIRE.plusDays(1))).block()
 
@@ -179,7 +179,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block().id
     SMono.fromPublisher(testee.updateExpireTime(ALICE, pushSubscriptionId, 
VALID_EXPIRE)).block()
 
@@ -192,7 +192,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block().id
     val fixedExpires = SMono.fromPublisher(testee.updateExpireTime(ALICE, 
pushSubscriptionId, MAX_EXPIRE.plusDays(1))).block()
 
@@ -204,7 +204,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block().id
 
     val newTypes: Set[TypeName] = Set(CustomTypeName1, CustomTypeName2)
@@ -236,7 +236,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block().id
     val singleRecordSaved = SFlux.fromPublisher(testee.get(ALICE, 
Set(pushSubscriptionId).asJava)).count().block()
     assertThat(singleRecordSaved).isEqualTo(1)
@@ -252,7 +252,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block().id
     val singleRecordSaved = SFlux.fromPublisher(testee.get(ALICE, 
Set(pushSubscriptionId).asJava)).count().block()
     assertThat(singleRecordSaved).isEqualTo(1)
@@ -285,12 +285,12 @@ trait PushSubscriptionRepositoryContract {
       deviceClientId = deviceClientId1,
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
       expires = Option(PushSubscriptionExpiredTime(VALID_EXPIRE)),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val validRequest2 = PushSubscriptionCreationRequest(
       deviceClientId = deviceClientId2,
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
       expires = Option(PushSubscriptionExpiredTime(VALID_EXPIRE)),
-      types = Seq(CustomTypeName2))
+      types = Some(Seq(CustomTypeName2)))
     val pushSubscriptionId1 = SMono.fromPublisher(testee.save(ALICE, 
validRequest1)).block().id
     val pushSubscriptionId2 = SMono.fromPublisher(testee.save(ALICE, 
validRequest2)).block().id
 
@@ -306,7 +306,7 @@ trait PushSubscriptionRepositoryContract {
       deviceClientId = deviceClientId1,
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
       expires = Option(PushSubscriptionExpiredTime(VALID_EXPIRE)),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val pushSubscriptionId1 = SMono.fromPublisher(testee.save(ALICE, 
validRequest1)).block().id
     val pushSubscriptionId2 = PushSubscriptionId.generate()
 
@@ -321,7 +321,7 @@ trait PushSubscriptionRepositoryContract {
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
       expires = Option(PushSubscriptionExpiredTime(VALID_EXPIRE.plusDays(1))),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest1)).block().id
 
     clock.setInstant(VALID_EXPIRE.plusDays(2).toInstant)
@@ -338,11 +338,11 @@ trait PushSubscriptionRepositoryContract {
     val validRequest1 = PushSubscriptionCreationRequest(
       deviceClientId = deviceClientId1,
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val validRequest2 = PushSubscriptionCreationRequest(
       deviceClientId = deviceClientId2,
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName2))
+      types = Some(Seq(CustomTypeName2)))
     val pushSubscriptionId1: PushSubscriptionId = 
SMono.fromPublisher(testee.save(ALICE, validRequest1)).block().id
     val pushSubscriptionId2: PushSubscriptionId = 
SMono.fromPublisher(testee.save(ALICE, validRequest2)).block().id
 
@@ -360,7 +360,7 @@ trait PushSubscriptionRepositoryContract {
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
       expires = Option(PushSubscriptionExpiredTime(VALID_EXPIRE.plusDays(1))),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest1)).block().id
 
     clock.setInstant(VALID_EXPIRE.plusDays(2).toInstant)
@@ -375,7 +375,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block().id
     SMono.fromPublisher(testee.validateVerificationCode(ALICE, 
pushSubscriptionId)).block()
 
@@ -397,7 +397,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1),
+      types = Some(Seq(CustomTypeName1)),
       keys = fullKeyPair)
 
     val pushSubscriptionId1 = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block().id
@@ -413,7 +413,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1),
+      types = Some(Seq(CustomTypeName1)),
       keys = emptyKeyPair)
     val pushSubscriptionId1 = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block().id
 
@@ -429,7 +429,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL()),
-      types = Seq(CustomTypeName1),
+      types = Some(Seq(CustomTypeName1)),
       keys = emptyP256hKey)
 
     assertThatThrownBy(() => SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block())
@@ -443,7 +443,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new 
URI("https://example.com/push";).toURL),
-      types = Seq(CustomTypeName1),
+      types = Some(Seq(CustomTypeName1)),
       keys = emptyAuthKey)
 
     assertThatThrownBy(() => SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block())
@@ -455,7 +455,7 @@ trait PushSubscriptionRepositoryContract {
     val validRequest = PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("1"),
       url = PushSubscriptionServerURL(new URL("https://example.com/push";)),
-      types = Seq(CustomTypeName1))
+      types = Some(Seq(CustomTypeName1)))
 
     val pushSubscriptionId = SMono.fromPublisher(testee.save(ALICE, 
validRequest)).block().id
 
diff --git 
a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedPushSubscriptionSetMethodTest.java
 
b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedPushSubscriptionSetMethodTest.java
index 8df62b6da6..1e29afb110 100644
--- 
a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedPushSubscriptionSetMethodTest.java
+++ 
b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedPushSubscriptionSetMethodTest.java
@@ -30,6 +30,7 @@ import 
org.apache.james.jmap.pushsubscription.PushClientConfiguration;
 import org.apache.james.jmap.rfc8621.contract.PushServerExtension;
 import org.apache.james.jmap.rfc8621.contract.PushSubscriptionProbeModule;
 import 
org.apache.james.jmap.rfc8621.contract.PushSubscriptionSetMethodContract;
+import org.apache.james.jmap.rfc8621.contract.probe.TypeStateModule;
 import org.apache.james.modules.AwsS3BlobStoreExtension;
 import org.apache.james.modules.RabbitMQExtension;
 import org.apache.james.modules.TestJMAPServerModule;
@@ -54,7 +55,7 @@ public class DistributedPushSubscriptionSetMethodTest 
implements PushSubscriptio
         .extension(new AwsS3BlobStoreExtension())
         .extension(new ClockExtension())
         .server(configuration -> 
CassandraRabbitMQJamesServerMain.createServer(configuration)
-            .overrideWith(new TestJMAPServerModule(), new 
PushSubscriptionProbeModule())
+            .overrideWith(new TestJMAPServerModule(), new 
PushSubscriptionProbeModule(), new TypeStateModule())
             .overrideWith(binder -> 
binder.bind(PushClientConfiguration.class).toInstance(PushClientConfiguration.UNSAFE_DEFAULT())))
         .build();
 
diff --git 
a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala
 
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala
index 053d2fbf28..6f8bc99a40 100644
--- 
a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala
+++ 
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala
@@ -36,6 +36,7 @@ import com.google.inject.multibindings.Multibinder
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured.{`given`, requestSpecification}
 import io.restassured.http.ContentType.JSON
+import io.restassured.path.json.JsonPath
 import jakarta.inject.Inject
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER
@@ -50,6 +51,7 @@ import org.apache.james.jmap.core.UTCDate
 import org.apache.james.jmap.http.UserCredential
 import 
org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, 
BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
 import 
org.apache.james.jmap.rfc8621.contract.PushSubscriptionSetMethodContract.TIME_FORMATTER
+import org.apache.james.jmap.rfc8621.contract.probe.TypeStateProbe
 import org.apache.james.utils.{DataProbeImpl, GuiceProbe, 
UpdatableTickingClock}
 import org.assertj.core.api.Assertions.assertThat
 import org.assertj.core.api.SoftAssertions
@@ -72,7 +74,7 @@ class PushSubscriptionProbe 
@Inject()(pushSubscriptionRepository: PushSubscripti
     SMono(pushSubscriptionRepository.save(username, 
PushSubscriptionCreationRequest(
       deviceClientId = deviceId,
       url = url,
-      types = types)))
+      types = Some(types))))
       .block()
 
   def retrievePushSubscription(username: Username, id: PushSubscriptionId): 
PushSubscription =
@@ -257,6 +259,128 @@ trait PushSubscriptionSetMethodContract {
       .containsExactlyInAnyOrder(MailboxTypeName, EmailTypeName)
   }
 
+  @Test
+  def updateWithMissingTypesPropertyShouldNotUpdateTypes(server: 
GuiceJamesServer): Unit = {
+    val probe = server.getProbe(classOf[PushSubscriptionProbe])
+    val pushSubscription = probe
+      .createPushSubscription(username = BOB,
+        url = PushSubscriptionServerURL(new 
URI("https://example.com/push/?device=X8980fc&client=12c6d086";).toURL),
+        deviceId = DeviceClientId("12c6d086"),
+        types = Seq(MailboxTypeName))
+
+    val validExpiresString = 
UTCDate(ZonedDateTime.now().plusDays(1)).asUTC.format(TIME_FORMATTER)
+    val request: String =
+      s"""{
+         |    "using": ["urn:ietf:params:jmap:core"],
+         |    "methodCalls": [
+         |      [
+         |        "PushSubscription/set",
+         |        {
+         |            "update": {
+         |                "${pushSubscription.id.serialise}": {
+         |                 "expires": "$validExpiresString"
+         |                }
+         |              }
+         |        },
+         |        "c1"
+         |      ]
+         |    ]
+         |  }""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |        [
+           |            "PushSubscription/set",
+           |            {
+           |                "updated": {
+           |                    "${pushSubscription.id.serialise}": {}
+           |                }
+           |            },
+           |            "c1"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+
+    // Types are not updated
+    assertThat(probe.retrievePushSubscription(BOB, 
pushSubscription.id).types.asJava)
+      .containsOnly(MailboxTypeName)
+  }
+
+  @Test
+  def updateShouldAcceptNullTypesAndUpdateToAllTypes(server: 
GuiceJamesServer): Unit = {
+    val subscriptionProbe = server.getProbe(classOf[PushSubscriptionProbe])
+    val typeStateProbe = server.getProbe(classOf[TypeStateProbe])
+    val pushSubscription = subscriptionProbe
+      .createPushSubscription(username = BOB,
+        url = PushSubscriptionServerURL(new 
URI("https://example.com/push/?device=X8980fc&client=12c6d086";).toURL),
+        deviceId = DeviceClientId("12c6d086"),
+        types = Seq(MailboxTypeName))
+
+    val request: String =
+      s"""{
+         |    "using": ["urn:ietf:params:jmap:core"],
+         |    "methodCalls": [
+         |      [
+         |        "PushSubscription/set",
+         |        {
+         |            "update": {
+         |                "${pushSubscription.id.serialise}": {
+         |                  "types": null
+         |                }
+         |              }
+         |        },
+         |        "c1"
+         |      ]
+         |    ]
+         |  }""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |        [
+           |            "PushSubscription/set",
+           |            {
+           |                "updated": {
+           |                    "${pushSubscription.id.serialise}": {}
+           |                }
+           |            },
+           |            "c1"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+
+    // All types are registered
+    assertThat(subscriptionProbe.retrievePushSubscription(BOB, 
pushSubscription.id).types.asJava)
+      .containsExactlyInAnyOrderElementsOf(typeStateProbe.typesNames())
+  }
+
   @Test
   def updateShouldRejectUnknownTypes(server: GuiceJamesServer): Unit = {
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
@@ -438,25 +562,31 @@ trait PushSubscriptionSetMethodContract {
   }
 
   @Test
-  def setMethodShouldNotCreatedWhenMissingTypesPropertyInCreationRequest(): 
Unit = {
+  def setCreateShouldAcceptNullTypesPropertyInCreationRequest(server: 
GuiceJamesServer, pushServer: ClientAndServer): Unit = {
+    val subscriptionProbe = server.getProbe(classOf[PushSubscriptionProbe])
+    val typeStateProbe = server.getProbe(classOf[TypeStateProbe])
+
     val request: String =
-      """{
-        |    "using": ["urn:ietf:params:jmap:core"],
+      s"""{
+        |    "using": [
+        |        "urn:ietf:params:jmap:core"
+        |    ],
         |    "methodCalls": [
-        |      [
-        |        "PushSubscription/set",
-        |        {
-        |            "create": {
-        |                "4f29": {
-        |                  "deviceClientId": "a889-ffea-910",
-        |                  "url": 
"https://example.com/push/?device=X8980fc&client=12c6d086";
+        |        [
+        |            "PushSubscription/set",
+        |            {
+        |                "create": {
+        |                    "4f29": {
+        |                        "deviceClientId": "a889-ffea-910",
+        |                        "url": "${getPushServerUrl(pushServer)}",
+        |                        "expires": 
"${UTCDate(ZonedDateTime.now().plusDays(1)).asUTC.format(TIME_FORMATTER)}"
+        |                    }
         |                }
-        |              }
-        |        },
-        |        "c1"
-        |      ]
+        |            },
+        |            "c1"
+        |        ]
         |    ]
-        |  }""".stripMargin
+        |}""".stripMargin
 
     val response: String = `given`
       .body(request)
@@ -477,10 +607,10 @@ trait PushSubscriptionSetMethodContract {
            |        [
            |            "PushSubscription/set",
            |            {
-           |                "notCreated": {
+           |                "created": {
            |                    "4f29": {
-           |                        "type": "invalidArguments",
-           |                        "description": "Missing '/types' property"
+           |                        "id": "$${json-unit.ignore}",
+           |                        "expires": "$${json-unit.ignore}"
            |                    }
            |                }
            |            },
@@ -488,6 +618,12 @@ trait PushSubscriptionSetMethodContract {
            |        ]
            |    ]
            |}""".stripMargin)
+
+    // All types are registered
+    val subscriptionId: PushSubscriptionId = 
PushSubscriptionId.parse(JsonPath.from(response)
+      .getString("methodResponses[0][1].created.4f29.id")).toOption.get
+    assertThat(subscriptionProbe.retrievePushSubscription(BOB, 
subscriptionId).types.asJava)
+      .containsExactlyInAnyOrderElementsOf(typeStateProbe.typesNames())
   }
 
   @Test
diff --git 
a/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepositoryTest.java
 
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/probe/TypeStateProbe.scala
similarity index 51%
copy from 
server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepositoryTest.java
copy to 
server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/probe/TypeStateProbe.scala
index 19c38c5ed4..85cce3c696 100644
--- 
a/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/pushsubscription/MemoryPushSubscriptionRepositoryTest.java
+++ 
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/probe/TypeStateProbe.scala
@@ -1,46 +1,39 @@
-/******************************************************************
- * 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.                                             *
- ******************************************************************/
+/****************************************************************
+ * 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.memory.pushsubscription;
+package org.apache.james.jmap.rfc8621.contract.probe
 
-import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository;
-import 
org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepositoryContract;
-import org.apache.james.utils.UpdatableTickingClock;
-import org.junit.jupiter.api.BeforeEach;
+import com.google.inject.AbstractModule
+import com.google.inject.multibindings.Multibinder
+import jakarta.inject.Inject
+import org.apache.james.jmap.api.change.TypeStateFactory
+import org.apache.james.jmap.api.model.TypeName
+import org.apache.james.utils.GuiceProbe
 
-public class MemoryPushSubscriptionRepositoryTest implements 
PushSubscriptionRepositoryContract {
-    UpdatableTickingClock clock;
-    PushSubscriptionRepository pushSubscriptionRepository;
-
-    @BeforeEach
-    void setup() {
-        clock = new 
UpdatableTickingClock(PushSubscriptionRepositoryContract.NOW());
-        pushSubscriptionRepository = new 
MemoryPushSubscriptionRepository(clock);
-    }
-
-    @Override
-    public UpdatableTickingClock clock() {
-        return clock;
-    }
+class TypeStateModule extends AbstractModule {
+  override def configure(): Unit =
+    Multibinder.newSetBinder(binder(), classOf[GuiceProbe])
+      .addBinding()
+      .to(classOf[TypeStateProbe])
+}
 
-    @Override
-    public PushSubscriptionRepository testee() {
-        return pushSubscriptionRepository;
-    }
+class TypeStateProbe @Inject()(typeStateFactory: TypeStateFactory) extends 
GuiceProbe {
+  def typesNames(): java.util.Set[TypeName] =
+    typeStateFactory.setTypeName
 }
diff --git 
a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryPushSubscriptionSetMethodTest.java
 
b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryPushSubscriptionSetMethodTest.java
index 96f12a6cf1..e88a3c4ee3 100644
--- 
a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryPushSubscriptionSetMethodTest.java
+++ 
b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryPushSubscriptionSetMethodTest.java
@@ -30,6 +30,7 @@ import 
org.apache.james.jmap.pushsubscription.PushClientConfiguration;
 import org.apache.james.jmap.rfc8621.contract.PushServerExtension;
 import org.apache.james.jmap.rfc8621.contract.PushSubscriptionProbeModule;
 import 
org.apache.james.jmap.rfc8621.contract.PushSubscriptionSetMethodContract;
+import org.apache.james.jmap.rfc8621.contract.probe.TypeStateModule;
 import org.apache.james.modules.TestJMAPServerModule;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
@@ -43,7 +44,7 @@ public class MemoryPushSubscriptionSetMethodTest implements 
PushSubscriptionSetM
             .build())
         .extension(new ClockExtension())
         .server(configuration -> 
MemoryJamesServerMain.createServer(configuration)
-            .overrideWith(new TestJMAPServerModule(), new 
PushSubscriptionProbeModule())
+            .overrideWith(new TestJMAPServerModule(), new 
PushSubscriptionProbeModule(), new TypeStateModule())
             .overrideWith(binder -> 
binder.bind(PushClientConfiguration.class).toInstance(PushClientConfiguration.UNSAFE_DEFAULT())))
         .build();
 
diff --git 
a/server/protocols/jmap-rfc-8621-integration-tests/postgres-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/postgres/PostgresPushSubscriptionSetMethodTest.java
 
b/server/protocols/jmap-rfc-8621-integration-tests/postgres-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/postgres/PostgresPushSubscriptionSetMethodTest.java
index 06ba0f85e9..54132d42cb 100644
--- 
a/server/protocols/jmap-rfc-8621-integration-tests/postgres-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/postgres/PostgresPushSubscriptionSetMethodTest.java
+++ 
b/server/protocols/jmap-rfc-8621-integration-tests/postgres-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/postgres/PostgresPushSubscriptionSetMethodTest.java
@@ -32,6 +32,7 @@ import 
org.apache.james.jmap.pushsubscription.PushClientConfiguration;
 import org.apache.james.jmap.rfc8621.contract.PushServerExtension;
 import org.apache.james.jmap.rfc8621.contract.PushSubscriptionProbeModule;
 import 
org.apache.james.jmap.rfc8621.contract.PushSubscriptionSetMethodContract;
+import org.apache.james.jmap.rfc8621.contract.probe.TypeStateModule;
 import org.apache.james.modules.RabbitMQExtension;
 import org.apache.james.modules.TestJMAPServerModule;
 import org.apache.james.modules.blobstore.BlobStoreConfiguration;
@@ -58,6 +59,7 @@ public class PostgresPushSubscriptionSetMethodTest implements 
PushSubscriptionSe
         .server(configuration -> 
PostgresJamesServerMain.createServer(configuration)
             .overrideWith(new TestJMAPServerModule())
             .overrideWith(new PushSubscriptionProbeModule())
+            .overrideWith(new TypeStateModule())
             .overrideWith(binder -> 
binder.bind(PushClientConfiguration.class).toInstance(PushClientConfiguration.UNSAFE_DEFAULT())))
         .build();
 
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala
index 9125567f01..6b1b5b07a9 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala
@@ -35,7 +35,7 @@ import org.apache.james.jmap.core.Properties.toProperties
 import org.apache.james.jmap.core.SetError.SetErrorDescription
 import org.apache.james.jmap.mail.{InvalidPropertyException, 
InvalidUpdateException, PatchUpdateValidationException, 
UnsupportedPropertyUpdatedException}
 import org.apache.james.jmap.method.{SetRequest, WithoutAccountId}
-import play.api.libs.json.{JsArray, JsObject, JsString, JsValue}
+import play.api.libs.json.{JsArray, JsNull, JsObject, JsString, JsValue}
 
 import scala.util.{Failure, Success, Try}
 
@@ -133,7 +133,8 @@ object TypesUpdate {
       .map(js => parseType(js, typeStateFactory))
       .sequence
       .map(_.toSet)
-      .map(TypesUpdate(_))
+      .map(types => TypesUpdate(Some(types)))
+    case JsNull => Right(TypesUpdate(None))
     case _ => Left(InvalidUpdateException("types", "Expecting an array of JSON 
strings as an argument"))
   }
   def parseType(jsValue: JsValue, typeStateFactory: TypeStateFactory): 
Either[PatchUpdateValidationException, TypeName] = jsValue match {
@@ -156,7 +157,7 @@ object ExpiresUpdate {
 
 sealed trait Update
 case class VerificationCodeUpdate(newVerificationCode: VerificationCode) 
extends Update
-case class TypesUpdate(types: Set[TypeName]) extends Update
+case class TypesUpdate(types: Option[Set[TypeName]]) extends Update
 case class ExpiresUpdate(newExpires: UTCDate) extends Update
 
 object ValidatedPushSubscriptionPatchObject {
@@ -166,7 +167,7 @@ object ValidatedPushSubscriptionPatchObject {
 }
 
 case class ValidatedPushSubscriptionPatchObject(verificationCodeUpdate: 
Option[VerificationCode],
-                                                typesUpdate: 
Option[Set[TypeName]],
+                                                typesUpdate: 
Option[Option[Set[TypeName]]],
                                                 expiresUpdate: 
Option[PushSubscriptionExpiredTime]) {
   val shouldUpdate: Boolean = verificationCodeUpdate.isDefined || 
typesUpdate.isDefined || expiresUpdate.isDefined
 
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionUpdatePerformer.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionUpdatePerformer.scala
index 22a65d5f91..9dd11d25f4 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionUpdatePerformer.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionUpdatePerformer.scala
@@ -130,9 +130,12 @@ class PushSubscriptionUpdatePerformer 
@Inject()(pushSubscriptionRepository: Push
       
SMono.error[PushSubscriptionUpdateResult](WrongVerificationCodeException())
     }
 
-  private def updateTypes(pushSubscription: PushSubscription, types: 
Set[TypeName], mailboxSession: MailboxSession): 
SMono[PushSubscriptionUpdateResult] =
+  private def updateTypes(pushSubscription: PushSubscription, maybeTypes: 
Option[Set[TypeName]], mailboxSession: MailboxSession): 
SMono[PushSubscriptionUpdateResult] = {
+    val types: Set[TypeName] = maybeTypes.getOrElse(typeStateFactory.all)
+
     SMono(pushSubscriptionRepository.updateTypes(mailboxSession.getUser, 
pushSubscription.id, types.asJava))
       .`then`(SMono.just(PushSubscriptionUpdateSuccess(pushSubscription.id)))
+  }
 
   private def updateExpires(pushSubscription: PushSubscription, inputExpires: 
PushSubscriptionExpiredTime, mailboxSession: MailboxSession): 
SMono[PushSubscriptionUpdateResult] =
     SMono(pushSubscriptionRepository.updateExpireTime(mailboxSession.getUser, 
pushSubscription.id, inputExpires.value))
diff --git 
a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala
 
b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala
index 53ef0a4c7a..d1c4f3b9e3 100644
--- 
a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala
@@ -66,7 +66,8 @@ class PushListenerTest {
   def setUp(): Unit = {
     val pushSerializer = 
PushSerializer(TypeStateFactory(ImmutableSet.of[TypeName](MailboxTypeName, 
EmailTypeName, EmailDeliveryTypeName)))
 
-    pushSubscriptionRepository = new 
MemoryPushSubscriptionRepository(Clock.systemUTC())
+    pushSubscriptionRepository = new 
MemoryPushSubscriptionRepository(Clock.systemUTC(),
+      TypeStateFactory(ImmutableSet.of[TypeName](MailboxTypeName, 
EmailTypeName, EmailDeliveryTypeName)))
     webPushClient = mock(classOf[WebPushClient])
     delegationStore = new MemoryDelegationStore()
     testee = new PushListener(pushSubscriptionRepository, webPushClient, 
pushSerializer, delegationStore, Clock.systemUTC())
@@ -87,7 +88,7 @@ class PushListenerTest {
     SMono(pushSubscriptionRepository.save(bob, PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("junit"),
       url = url,
-      types = Seq(MailboxTypeName, EmailTypeName)))).block()
+      types = Some(Seq(MailboxTypeName, EmailTypeName))))).block()
 
     SMono(testee.reactiveEvent(StateChangeEvent(EventId.random(), bob,
       Map(EmailTypeName -> UuidState(UUID.randomUUID()))))).block()
@@ -100,7 +101,7 @@ class PushListenerTest {
     val id = SMono(pushSubscriptionRepository.save(bob, 
PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("junit"),
       url = url,
-      types = Seq(EmailDeliveryTypeName)))).block().id
+      types = Some(Seq(EmailDeliveryTypeName))))).block().id
     SMono(pushSubscriptionRepository.validateVerificationCode(bob, id)).block()
 
     SMono(testee.reactiveEvent(StateChangeEvent(EventId.random(), bob,
@@ -114,7 +115,7 @@ class PushListenerTest {
     val id = SMono(pushSubscriptionRepository.save(bob, 
PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("junit"),
       url = url,
-      types = Seq(EmailTypeName, MailboxTypeName)))).block().id
+      types = Some(Seq(EmailTypeName, MailboxTypeName))))).block().id
     SMono(pushSubscriptionRepository.validateVerificationCode(bob, id)).block()
 
     val state1 = UuidState(UUID.randomUUID())
@@ -136,7 +137,7 @@ class PushListenerTest {
     val bobSubscriptionId = SMono(pushSubscriptionRepository.save(bob, 
PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("junit1"),
       url = url,
-      types = Seq(EmailTypeName, MailboxTypeName)))).block().id
+      types = Some(Seq(EmailTypeName, MailboxTypeName))))).block().id
     SMono(pushSubscriptionRepository.validateVerificationCode(bob, 
bobSubscriptionId)).block()
 
     val state1 = UuidState(UUID.randomUUID())
@@ -152,12 +153,12 @@ class PushListenerTest {
     val bobSubscriptionId = SMono(pushSubscriptionRepository.save(bob, 
PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("junit1"),
       url = url,
-      types = Seq(EmailTypeName, MailboxTypeName)))).block().id
+      types = Some(Seq(EmailTypeName, MailboxTypeName))))).block().id
     SMono(pushSubscriptionRepository.validateVerificationCode(bob, 
bobSubscriptionId)).block()
     val aliceSubscriptionId = SMono(pushSubscriptionRepository.save(alice, 
PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("junit2"),
       url = url,
-      types = Seq(EmailTypeName, MailboxTypeName)))).block().id
+      types = Some(Seq(EmailTypeName, MailboxTypeName))))).block().id
     SMono(pushSubscriptionRepository.validateVerificationCode(alice, 
aliceSubscriptionId)).block()
 
     val state1 = UuidState(UUID.randomUUID())
@@ -180,7 +181,7 @@ class PushListenerTest {
     val bobSubscriptionId = SMono(pushSubscriptionRepository.save(bob, 
PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("junit1"),
       url = url,
-      types = Seq(EmailTypeName, MailboxTypeName)))).block().id
+      types = Some(Seq(EmailTypeName, MailboxTypeName))))).block().id
     SMono(pushSubscriptionRepository.validateVerificationCode(bob, 
bobSubscriptionId)).block()
 
     val stateChangeBob = UuidState(UUID.randomUUID())
@@ -203,7 +204,7 @@ class PushListenerTest {
     val id = SMono(pushSubscriptionRepository.save(bob, 
PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("junit"),
       url = url,
-      types = Seq(EmailTypeName, MailboxTypeName)))).block().id
+      types = Some(Seq(EmailTypeName, MailboxTypeName))))).block().id
     SMono(pushSubscriptionRepository.validateVerificationCode(bob, id)).block()
 
     val state1 = UuidState(UUID.randomUUID())
@@ -227,7 +228,7 @@ class PushListenerTest {
     val id = SMono(pushSubscriptionRepository.save(bob, 
PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("junit"),
       url = url,
-      types = Seq(EmailDeliveryTypeName, EmailTypeName)))).block().id
+      types = Some(Seq(EmailDeliveryTypeName, EmailTypeName))))).block().id
     SMono(pushSubscriptionRepository.validateVerificationCode(bob, id)).block()
 
     val state1 = UuidState(UUID.randomUUID())
@@ -246,7 +247,7 @@ class PushListenerTest {
     val id = SMono(pushSubscriptionRepository.save(bob, 
PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("junit"),
       url = url,
-      types = Seq(EmailDeliveryTypeName, EmailTypeName)))).block().id
+      types = Some(Seq(EmailDeliveryTypeName, EmailTypeName))))).block().id
     SMono(pushSubscriptionRepository.validateVerificationCode(bob, id)).block()
 
     val state1 = UuidState(UUID.randomUUID())
@@ -264,7 +265,7 @@ class PushListenerTest {
     val id = SMono(pushSubscriptionRepository.save(bob, 
PushSubscriptionCreationRequest(
       deviceClientId = DeviceClientId("junit"),
       url = url,
-      types = Seq(EmailDeliveryTypeName, EmailTypeName)))).block().id
+      types = Some(Seq(EmailDeliveryTypeName, EmailTypeName))))).block().id
     SMono(pushSubscriptionRepository.validateVerificationCode(bob, id)).block()
 
     val state1 = UuidState(UUID.randomUUID())
@@ -296,7 +297,7 @@ class PushListenerTest {
       keys = Some(PushSubscriptionKeys(p256dh = 
Base64.getUrlEncoder.encodeToString(uaPublicKey.getEncoded),
         auth = Base64.getUrlEncoder.encodeToString(authSecret))),
       url = url,
-      types = Seq(EmailTypeName, MailboxTypeName)))).block().id
+      types = Some(Seq(EmailTypeName, MailboxTypeName))))).block().id
     SMono(pushSubscriptionRepository.validateVerificationCode(bob, id)).block()
 
     val state1 = UuidState(UUID.randomUUID())
diff --git 
a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUserDeletionIntegrationTest.java
 
b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUserDeletionIntegrationTest.java
index a421047f75..b6dc3e3a66 100644
--- 
a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUserDeletionIntegrationTest.java
+++ 
b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUserDeletionIntegrationTest.java
@@ -291,7 +291,7 @@ class MemoryUserDeletionIntegrationTest {
                 new PushSubscriptionServerURL(new 
URI("http://whatever/toto";).toURL()),
                 Option.empty(),
                 Option.empty(),
-                PushSubscriptionCreationRequest.noTypes()));
+                Option.apply(PushSubscriptionCreationRequest.noTypes())));
 
         String taskId = webAdminApi
             .queryParam("action", "deleteData")


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

Reply via email to