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