This is an automated email from the ASF dual-hosted git repository. rcordier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 34175c390e230b215fd494c3fa8cd5dece6c715f Author: Quan Tran <[email protected]> AuthorDate: Wed Oct 27 17:46:35 2021 +0700 JAMES-3539 Cassandra implement for PushSubscriptionRepository --- server/data/data-jmap-cassandra/pom.xml | 5 + .../CassandraPushSubscriptionDAO.java | 175 +++++++++++++++++++++ .../CassandraPushSubscriptionModule.java | 57 +++++++ .../CassandraPushSubscriptionRepository.java | 146 +++++++++++++++++ .../tables/CassandraPushSubscriptionTable.java | 34 ++++ .../CassandraPushSubscriptionRepositoryTest.java | 62 ++++++++ .../james/jmap/api/model/PushSubscription.scala | 2 + .../PushSubscriptionRepositoryContract.scala | 63 +++++++- 8 files changed, 543 insertions(+), 1 deletion(-) diff --git a/server/data/data-jmap-cassandra/pom.xml b/server/data/data-jmap-cassandra/pom.xml index ec5a898..e5b0db7 100644 --- a/server/data/data-jmap-cassandra/pom.xml +++ b/server/data/data-jmap-cassandra/pom.xml @@ -92,6 +92,11 @@ </dependency> <dependency> <groupId>${james.groupId}</groupId> + <artifactId>james-server-testing</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> <artifactId>metrics-tests</artifactId> <scope>test</scope> </dependency> diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionDAO.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionDAO.java new file mode 100644 index 0000000..99b222d --- /dev/null +++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionDAO.java @@ -0,0 +1,175 @@ +/**************************************************************** + * 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.cassandra.pushsubscription; + +import static com.datastax.driver.core.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.driver.core.querybuilder.QueryBuilder.delete; +import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; +import static com.datastax.driver.core.querybuilder.QueryBuilder.insertInto; +import static com.datastax.driver.core.querybuilder.QueryBuilder.select; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.DEVICE_CLIENT_ID; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.ENCRYPT_AUTH_SECRET; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.ENCRYPT_PUBLIC_KEY; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.EXPIRES; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.ID; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.TABLE_NAME; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.TYPES; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.URL; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.USER; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.VALIDATED; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.VERIFICATION_CODE; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Date; +import java.util.Set; + +import javax.inject.Inject; + +import org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor; +import org.apache.james.core.Username; +import org.apache.james.jmap.api.change.TypeStateFactory; +import org.apache.james.jmap.api.model.DeviceClientId; +import org.apache.james.jmap.api.model.PushSubscription; +import org.apache.james.jmap.api.model.PushSubscriptionExpiredTime; +import org.apache.james.jmap.api.model.PushSubscriptionId; +import org.apache.james.jmap.api.model.PushSubscriptionKeys; +import org.apache.james.jmap.api.model.PushSubscriptionServerURL; +import org.apache.james.jmap.api.model.TypeName; +import org.apache.james.jmap.api.model.VerificationCode; + +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; +import com.google.common.collect.ImmutableSet; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import scala.Option; +import scala.collection.immutable.Seq; +import scala.jdk.javaapi.CollectionConverters; +import scala.jdk.javaapi.OptionConverters; + +public class CassandraPushSubscriptionDAO { + private final TypeStateFactory typeStateFactory; + private final CassandraAsyncExecutor executor; + private final PreparedStatement insert; + private final PreparedStatement selectAll; + private final PreparedStatement deleteOne; + + @Inject + public CassandraPushSubscriptionDAO(Session session, TypeStateFactory typeStateFactory) { + executor = new CassandraAsyncExecutor(session); + + insert = session.prepare(insertInto(TABLE_NAME) + .value(USER, bindMarker(USER)) + .value(DEVICE_CLIENT_ID, bindMarker(DEVICE_CLIENT_ID)) + .value(ID, bindMarker(ID)) + .value(EXPIRES, bindMarker(EXPIRES)) + .value(TYPES, bindMarker(TYPES)) + .value(URL, bindMarker(URL)) + .value(VERIFICATION_CODE, bindMarker(VERIFICATION_CODE)) + .value(ENCRYPT_PUBLIC_KEY, bindMarker(ENCRYPT_PUBLIC_KEY)) + .value(ENCRYPT_AUTH_SECRET, bindMarker(ENCRYPT_AUTH_SECRET)) + .value(VALIDATED, bindMarker(VALIDATED))); + + selectAll = session.prepare(select() + .from(TABLE_NAME) + .where(eq(USER, bindMarker(USER)))); + + deleteOne = session.prepare(delete() + .from(TABLE_NAME) + .where(eq(USER, bindMarker(USER))) + .and(eq(DEVICE_CLIENT_ID, bindMarker(DEVICE_CLIENT_ID)))); + + this.typeStateFactory = typeStateFactory; + } + + public Mono<PushSubscription> insert(Username username, PushSubscription subscription) { + Set<String> typeNames = CollectionConverters.asJava(subscription.types() + .map(TypeName::asString) + .toSet()); + Instant utcInstant = subscription.expires().value().withZoneSameInstant(ZoneOffset.UTC).toInstant(); + + BoundStatement insertSubscription = insert.bind() + .setString(USER, username.asString()) + .setString(DEVICE_CLIENT_ID, subscription.deviceClientId()) + .setUUID(ID, subscription.id().value()) + .setTimestamp(EXPIRES, Date.from(utcInstant)) + .setSet(TYPES, typeNames) + .setString(URL, subscription.url().value().toString()) + .setString(VERIFICATION_CODE, subscription.verificationCode()) + .setBool(VALIDATED, subscription.validated()); + + OptionConverters.toJava(subscription.keys()) + .map(keys -> insertSubscription.setString(ENCRYPT_PUBLIC_KEY, keys.p256dh()) + .setString(ENCRYPT_AUTH_SECRET, keys.auth())); + + return executor.executeVoid(insertSubscription) + .thenReturn(subscription); + } + + public Flux<PushSubscription> selectAll(Username username) { + return executor.executeRows(selectAll.bind().setString(USER, username.asString())) + .map(this::toPushSubscription); + } + + public Mono<Void> deleteOne(Username username, String deviceClientId) { + return executor.executeVoid(deleteOne.bind() + .setString(USER, username.asString()) + .setString(DEVICE_CLIENT_ID, deviceClientId)); + } + + private PushSubscription toPushSubscription(Row row) { + return PushSubscription.apply( + PushSubscriptionId.apply(row.getUUID(ID)), + DeviceClientId.apply(row.getString(DEVICE_CLIENT_ID)), + PushSubscriptionServerURL.from(row.getString(URL)).get(), + toKeys(row), + VerificationCode.apply(row.getString(VERIFICATION_CODE)), + row.getBool(VALIDATED), + toExpires(row), + toTypes(row)); + } + + private Option<PushSubscriptionKeys> toKeys(Row row) { + String p256dh = row.getString(ENCRYPT_PUBLIC_KEY); + String auth = row.getString(ENCRYPT_AUTH_SECRET); + if (p256dh == null && auth == null) { + return Option.empty(); + } else { + return Option.apply(PushSubscriptionKeys.apply(p256dh, auth)); + } + } + + private PushSubscriptionExpiredTime toExpires(Row row) { + return PushSubscriptionExpiredTime.apply( + ZonedDateTime.ofInstant(row.getTimestamp(EXPIRES).toInstant(), ZoneOffset.UTC)); + } + + private Seq<TypeName> toTypes(Row row) { + return CollectionConverters.asScala(row.getSet(TYPES, String.class).stream() + .map(string -> typeStateFactory.parse(string).right().get()) + .collect(ImmutableSet.toImmutableSet())) + .toSeq(); + } +} \ No newline at end of file diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionModule.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionModule.java new file mode 100644 index 0000000..49e1065 --- /dev/null +++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionModule.java @@ -0,0 +1,57 @@ +/**************************************************************** + * 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.cassandra.pushsubscription; + +import static com.datastax.driver.core.DataType.cboolean; +import static com.datastax.driver.core.DataType.frozenSet; +import static com.datastax.driver.core.DataType.text; +import static com.datastax.driver.core.DataType.timestamp; +import static com.datastax.driver.core.DataType.uuid; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.DEVICE_CLIENT_ID; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.ENCRYPT_AUTH_SECRET; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.ENCRYPT_PUBLIC_KEY; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.EXPIRES; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.ID; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.TYPES; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.URL; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.USER; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.VALIDATED; +import static org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable.VERIFICATION_CODE; + +import org.apache.james.backends.cassandra.components.CassandraModule; +import org.apache.james.jmap.cassandra.pushsubscription.tables.CassandraPushSubscriptionTable; + +public interface CassandraPushSubscriptionModule { + CassandraModule MODULE = CassandraModule.builder() + .table(CassandraPushSubscriptionTable.TABLE_NAME) + .comment("Hold user push subscriptions data") + .statement(statement -> statement + .addPartitionKey(USER, text()) + .addClusteringColumn(DEVICE_CLIENT_ID, text()) + .addColumn(ID, uuid()) + .addColumn(EXPIRES, timestamp()) + .addColumn(TYPES, frozenSet(text())) + .addColumn(URL, text()) + .addColumn(VERIFICATION_CODE, text()) + .addColumn(ENCRYPT_PUBLIC_KEY, text()) + .addColumn(ENCRYPT_AUTH_SECRET, text()) + .addColumn(VALIDATED, cboolean())) + .build(); +} \ No newline at end of file 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 new file mode 100644 index 0000000..a6d2c16 --- /dev/null +++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionRepository.java @@ -0,0 +1,146 @@ +/**************************************************************** + * 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.cassandra.pushsubscription; + +import static org.apache.james.jmap.api.pushsubscription.PushSubscriptionHelpers.evaluateExpiresTime; +import static org.apache.james.jmap.api.pushsubscription.PushSubscriptionHelpers.isInThePast; +import static org.apache.james.jmap.api.pushsubscription.PushSubscriptionHelpers.isInvalidPushSubscriptionKey; +import static org.apache.james.jmap.api.pushsubscription.PushSubscriptionHelpers.isNotOutdatedSubscription; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.Optional; +import java.util.Set; + +import javax.inject.Inject; + +import org.apache.james.core.Username; +import org.apache.james.jmap.api.model.DeviceClientIdInvalidException; +import org.apache.james.jmap.api.model.ExpireTimeInvalidException; +import org.apache.james.jmap.api.model.InvalidPushSubscriptionKeys; +import org.apache.james.jmap.api.model.PushSubscription; +import org.apache.james.jmap.api.model.PushSubscriptionCreationRequest; +import org.apache.james.jmap.api.model.PushSubscriptionExpiredTime; +import org.apache.james.jmap.api.model.PushSubscriptionId; +import org.apache.james.jmap.api.model.PushSubscriptionNotFoundException; +import org.apache.james.jmap.api.model.TypeName; +import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository; +import org.reactivestreams.Publisher; + +import reactor.core.publisher.Mono; +import scala.jdk.javaapi.CollectionConverters; +import scala.jdk.javaapi.OptionConverters; + +public class CassandraPushSubscriptionRepository implements PushSubscriptionRepository { + private final CassandraPushSubscriptionDAO dao; + private final Clock clock; + + @Inject + public CassandraPushSubscriptionRepository(CassandraPushSubscriptionDAO dao, Clock clock) { + this.dao = dao; + this.clock = clock; + } + + @Override + public Publisher<PushSubscription> save(Username username, PushSubscriptionCreationRequest request) { + return Mono.just(request) + .handle((req, sink) -> { + if (isInThePast(req.expires(), clock)) { + sink.error(new ExpireTimeInvalidException(req.expires().get().value(), "expires must be greater than now")); + } + if (!isUniqueDeviceClientId(username, req.deviceClientId())) { + sink.error(new DeviceClientIdInvalidException(req.deviceClientId(), "deviceClientId must be unique")); + } + if (isInvalidPushSubscriptionKey(req.keys())) { + sink.error(new InvalidPushSubscriptionKeys(req.keys().get())); + } + }) + .thenReturn(PushSubscription.from(request, + evaluateExpiresTime(OptionConverters.toJava(request.expires().map(PushSubscriptionExpiredTime::value)), + clock))) + .flatMap(subscription -> dao.insert(username, subscription).thenReturn(subscription)); + } + + @Override + public Publisher<Void> updateExpireTime(Username username, PushSubscriptionId id, ZonedDateTime newExpire) { + return Mono.just(newExpire) + .handle((inputTime, sink) -> { + if (newExpire.isBefore(ZonedDateTime.now(clock))) { + sink.error(new ExpireTimeInvalidException(inputTime, "expires must be greater than now")); + } + }) + .then(retrieveByPushSubscriptionId(username, id) + .flatMap(subscription -> dao.insert(username, + subscription.withExpires(evaluateExpiresTime(Optional.of(newExpire), clock)))) + .switchIfEmpty(Mono.error(() -> new PushSubscriptionNotFoundException(id))) + .then()); + } + + @Override + public Publisher<Void> updateTypes(Username username, PushSubscriptionId id, Set<TypeName> types) { + return retrieveByPushSubscriptionId(username, id) + .map(subscription -> subscription.withTypes(CollectionConverters.asScala(types).toSeq())) + .flatMap(newPushSubscription -> dao.insert(username, newPushSubscription)) + .switchIfEmpty(Mono.error(() -> new PushSubscriptionNotFoundException(id))) + .then(); + } + + @Override + public Publisher<Void> validateVerificationCode(Username username, PushSubscriptionId id) { + return retrieveByPushSubscriptionId(username, id) + .map(PushSubscription::verified) + .flatMap(newPushSubscription -> dao.insert(username, newPushSubscription)) + .switchIfEmpty(Mono.error(() -> new PushSubscriptionNotFoundException(id))) + .then(); + } + + @Override + public Publisher<Void> revoke(Username username, PushSubscriptionId id) { + return Mono.from(retrieveByPushSubscriptionId(username, id)) + .flatMap(subscription -> dao.deleteOne(username, subscription.deviceClientId())) + .switchIfEmpty(Mono.empty()); + } + + @Override + public Publisher<PushSubscription> get(Username username, Set<PushSubscriptionId> ids) { + return dao.selectAll(username) + .filter(subscription -> ids.contains(subscription.id())) + .filter(subscription -> isNotOutdatedSubscription(subscription, clock)); + } + + @Override + public Publisher<PushSubscription> list(Username username) { + return dao.selectAll(username) + .filter(subscription -> isNotOutdatedSubscription(subscription, clock)); + } + + private Mono<PushSubscription> retrieveByPushSubscriptionId(Username username, PushSubscriptionId id) { + return dao.selectAll(username).filter(subscription -> subscription.id().equals(id)).next(); + } + + private boolean isUniqueDeviceClientId(Username username, String deviceClientId) { + return Boolean.TRUE.equals(dao.selectAll(username) + .filter(subscription -> subscription.deviceClientId().equals(deviceClientId)) + .count() + .map(value -> value == 0) + .block()); + } + +} \ No newline at end of file diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/pushsubscription/tables/CassandraPushSubscriptionTable.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/pushsubscription/tables/CassandraPushSubscriptionTable.java new file mode 100644 index 0000000..7d56857 --- /dev/null +++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/pushsubscription/tables/CassandraPushSubscriptionTable.java @@ -0,0 +1,34 @@ +/**************************************************************** + * 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.cassandra.pushsubscription.tables; + +public interface CassandraPushSubscriptionTable { + String TABLE_NAME = "push_subscription"; + String USER = "user"; + String DEVICE_CLIENT_ID = "device_client_id"; + String ID = "id"; + String EXPIRES = "expires"; + String TYPES = "types"; + String URL = "url"; + String VERIFICATION_CODE = "verification_code"; + String ENCRYPT_PUBLIC_KEY = "encrypt_public_key"; + String ENCRYPT_AUTH_SECRET = "encrypt_auth_secret"; + String VALIDATED = "validated"; +} \ No newline at end of file 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 new file mode 100644 index 0000000..b411746 --- /dev/null +++ b/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/pushsubscription/CassandraPushSubscriptionRepositoryTest.java @@ -0,0 +1,62 @@ +/**************************************************************** + * 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.cassandra.pushsubscription; + +import org.apache.james.backends.cassandra.CassandraCluster; +import org.apache.james.backends.cassandra.CassandraClusterExtension; +import org.apache.james.backends.cassandra.components.CassandraModule; +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 org.junit.jupiter.api.extension.RegisterExtension; + +import scala.jdk.javaapi.CollectionConverters; + + +public class CassandraPushSubscriptionRepositoryTest implements PushSubscriptionRepositoryContract { + static final CassandraModule MODULE = CassandraPushSubscriptionModule.MODULE; + + UpdatableTickingClock clock; + PushSubscriptionRepository pushSubscriptionRepository; + + @RegisterExtension + static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(MODULE); + + @BeforeEach + void setup(CassandraCluster cassandra) { + clock = new UpdatableTickingClock(PushSubscriptionRepositoryContract.NOW()); + pushSubscriptionRepository = new CassandraPushSubscriptionRepository( + new CassandraPushSubscriptionDAO(cassandra.getConf(), + new TypeStateFactory(CollectionConverters.asJava(PushSubscriptionRepositoryContract.TYPE_NAME_SET()))), + clock); + } + + @Override + public UpdatableTickingClock clock() { + return clock; + } + + @Override + public PushSubscriptionRepository testee() { + return pushSubscriptionRepository; + } +} 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 a88a5f8..6d7b7ac 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 @@ -30,6 +30,8 @@ import com.google.crypto.tink.subtle.EllipticCurves import scala.util.Try +import scala.util.Try + object PushSubscriptionId { def generate(): PushSubscriptionId = PushSubscriptionId(UUID.randomUUID) } 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 cf6d28c..7c87498 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 @@ -23,7 +23,7 @@ import java.net.URL import java.time.{Clock, Instant, ZoneId, ZonedDateTime} import org.apache.james.core.Username -import org.apache.james.jmap.api.model.{DeviceClientId, DeviceClientIdInvalidException, ExpireTimeInvalidException, PushSubscription, PushSubscriptionCreationRequest, PushSubscriptionExpiredTime, PushSubscriptionId, PushSubscriptionNotFoundException, PushSubscriptionServerURL, State, TypeName} +import org.apache.james.jmap.api.model.{DeviceClientId, DeviceClientIdInvalidException, ExpireTimeInvalidException, InvalidPushSubscriptionKeys, PushSubscription, PushSubscriptionCreationRequest, PushSubscriptionExpiredTime, PushSubscriptionId, PushSubscriptionKeys, PushSubscriptionNotFoundException, PushSubscriptionServerURL, State, TypeName} import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepositoryContract.{ALICE, INVALID_EXPIRE, MAX_EXPIRE, VALID_EXPIRE} import org.apache.james.utils.UpdatableTickingClock import org.assertj.core.api.Assertions.{assertThat, assertThatCode, assertThatThrownBy} @@ -61,6 +61,7 @@ case class CustomState(value: String) extends State { } object PushSubscriptionRepositoryContract { + val TYPE_NAME_SET: Set[TypeName] = Set(CustomTypeName1, CustomTypeName2) val NOW: Instant = Instant.parse("2021-10-25T07:05:39.160Z") val ZONE_ID: ZoneId = ZoneId.of("UTC") val CLOCK: Clock = Clock.fixed(NOW, ZONE_ID) @@ -370,5 +371,65 @@ trait PushSubscriptionRepositoryContract { assertThatThrownBy(() => SMono.fromPublisher(testee.validateVerificationCode(ALICE, randomId)).block()) .isInstanceOf(classOf[PushSubscriptionNotFoundException]) } + + @Test + def saveSubscriptionWithFullKeyPairShouldSucceed(): Unit = { + val fullKeyPair = Some(PushSubscriptionKeys.apply(p256dh = "p256h", auth = "auth")) + val validRequest = PushSubscriptionCreationRequest( + deviceClientId = DeviceClientId("1"), + url = PushSubscriptionServerURL(new URL("https://example.com/push")), + types = Seq(CustomTypeName1), + keys = fullKeyPair) + + val pushSubscriptionId1 = SMono.fromPublisher(testee.save(ALICE, validRequest)).block().id + + val pushSubscriptions = SFlux.fromPublisher(testee.get(ALICE, Set(pushSubscriptionId1).asJava)).collectSeq().block() + + assertThat(pushSubscriptions.map(_.keys).toList.asJava).containsExactlyInAnyOrder(fullKeyPair) + } + + @Test + def saveSubscriptionWithNoneKeyPairShouldSucceed(): Unit = { + val emptyKeyPair = None + val validRequest = PushSubscriptionCreationRequest( + deviceClientId = DeviceClientId("1"), + url = PushSubscriptionServerURL(new URL("https://example.com/push")), + types = Seq(CustomTypeName1), + keys = emptyKeyPair) + val pushSubscriptionId1 = SMono.fromPublisher(testee.save(ALICE, validRequest)).block().id + + val pushSubscriptions = SFlux.fromPublisher(testee.get(ALICE, Set(pushSubscriptionId1).asJava)).collectSeq().block() + + assertThat(pushSubscriptions.map(_.keys).toList.asJava).containsExactlyInAnyOrder(emptyKeyPair) + } + + @Test + def saveSubscriptionWithEmptyP256hKeyShouldFail(): Unit = { + val emptyP256hKey = Some(PushSubscriptionKeys.apply(p256dh = "", auth = "auth")) + + val validRequest = PushSubscriptionCreationRequest( + deviceClientId = DeviceClientId("1"), + url = PushSubscriptionServerURL(new URL("https://example.com/push")), + types = Seq(CustomTypeName1), + keys = emptyP256hKey) + + assertThatThrownBy(() => SMono.fromPublisher(testee.save(ALICE, validRequest)).block()) + .isInstanceOf(classOf[InvalidPushSubscriptionKeys]) + } + + @Test + def saveSubscriptionWithEmptyAuthKeyShouldFail(): Unit = { + val emptyAuthKey = Some(PushSubscriptionKeys.apply(p256dh = "p256dh", auth = "")) + + val validRequest = PushSubscriptionCreationRequest( + deviceClientId = DeviceClientId("1"), + url = PushSubscriptionServerURL(new URL("https://example.com/push")), + types = Seq(CustomTypeName1), + keys = emptyAuthKey) + + assertThatThrownBy(() => SMono.fromPublisher(testee.save(ALICE, validRequest)).block()) + .isInstanceOf(classOf[InvalidPushSubscriptionKeys]) + } + } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
