You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2021/11/29 03:42:00 UTC
[james-project] branch master updated: JAMES-3534 Identity/set update should work on existing server-set Identities (#757)
This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push:
new f59163f JAMES-3534 Identity/set update should work on existing server-set Identities (#757)
f59163f is described below
commit f59163f5600bcd1210a0bb7c3ba3a01cbd790725
Author: vttran <vt...@linagora.com>
AuthorDate: Mon Nov 29 10:41:55 2021 +0700
JAMES-3534 Identity/set update should work on existing server-set Identities (#757)
---
.../identity/CassandraCustomIdentityDAO.scala | 11 +-
.../jmap/api/identity/CustomIdentityDAO.scala | 93 ++++++++--
.../memory/identity/MemoryCustomIdentityDAO.scala | 5 +-
.../jmap/api/identity/IdentityRepositoryTest.scala | 200 +++++++++++++++++++++
.../rfc8621/contract/IdentityGetContract.scala | 5 +
.../rfc8621/contract/IdentitySetContract.scala | 132 +++++++++++++-
6 files changed, 420 insertions(+), 26 deletions(-)
diff --git a/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityDAO.scala b/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityDAO.scala
index 1e7f347..bd21f1e 100644
--- a/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityDAO.scala
+++ b/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityDAO.scala
@@ -22,7 +22,6 @@ package org.apache.james.jmap.cassandra.identity
import com.datastax.driver.core.querybuilder.QueryBuilder
import com.datastax.driver.core.querybuilder.QueryBuilder.{bindMarker, insertInto, select}
import com.datastax.driver.core.{BoundStatement, PreparedStatement, Row, Session, UDTValue}
-import javax.inject.Inject
import org.apache.james.backends.cassandra.init.CassandraTypesProvider
import org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor
import org.apache.james.core.{MailAddress, Username}
@@ -34,6 +33,7 @@ import org.apache.james.jmap.cassandra.utils.EmailAddressTupleUtil
import reactor.core.publisher.Mono
import reactor.core.scala.publisher.{SFlux, SMono}
+import javax.inject.Inject
import scala.jdk.javaapi.CollectionConverters
case class CassandraCustomIdentityDAO @Inject()(session: Session,
@@ -66,12 +66,13 @@ case class CassandraCustomIdentityDAO @Inject()(session: Session,
.where(QueryBuilder.eq(USER, bindMarker(USER)))
.and(QueryBuilder.eq(ID, bindMarker(ID))))
- override def save(user: Username, creationRequest: IdentityCreationRequest): SMono[Identity] = {
- val id = IdentityId.generate
- SMono.just(id)
+ override def save(user: Username, creationRequest: IdentityCreationRequest): SMono[Identity] =
+ save(user, IdentityId.generate, creationRequest)
+
+ override def save(user: Username, identityId: IdentityId, creationRequest: IdentityCreationRequest): SMono[Identity] =
+ SMono.just(identityId)
.map(creationRequest.asIdentity)
.flatMap(identity => insert(user, identity))
- }
override def list(user: Username): SFlux[Identity] =
SFlux.fromPublisher(executor.executeRows(selectAllStatement.bind().setString(USER, user.asString()))
diff --git a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala
index 55e88e9..9b1a2a2 100644
--- a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala
+++ b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala
@@ -21,10 +21,11 @@ package org.apache.james.jmap.api.identity
import java.nio.charset.StandardCharsets
import java.util.UUID
+import javax.inject.Inject
import com.google.common.collect.ImmutableList
-import javax.inject.Inject
import org.apache.james.core.{MailAddress, Username}
+import org.apache.james.jmap.api.identity.IdentityWithOrigin.IdentityWithOrigin
import org.apache.james.jmap.api.model.{EmailAddress, ForbiddenSendFromException, HtmlSignature, Identity, IdentityId, IdentityName, MayDeleteIdentity, TextSignature}
import org.apache.james.rrt.api.CanSendFrom
import org.apache.james.user.api.UsersRepository
@@ -36,11 +37,11 @@ import scala.jdk.CollectionConverters._
import scala.util.Try
case class IdentityCreationRequest(name: Option[IdentityName],
- email: MailAddress,
- replyTo: Option[List[EmailAddress]],
- bcc: Option[List[EmailAddress]],
- textSignature: Option[TextSignature],
- htmlSignature: Option[HtmlSignature]) {
+ email: MailAddress,
+ replyTo: Option[List[EmailAddress]],
+ bcc: Option[List[EmailAddress]],
+ textSignature: Option[TextSignature],
+ htmlSignature: Option[HtmlSignature]) {
def asIdentity(id: IdentityId): Identity = Identity(id, name.getOrElse(IdentityName.DEFAULT), email, replyTo, bcc, textSignature.getOrElse(TextSignature.DEFAULT), htmlSignature.getOrElse(HtmlSignature.DEFAULT), mayDelete = MayDeleteIdentity(true))
}
@@ -63,20 +64,31 @@ case class IdentityHtmlSignatureUpdate(htmlSignature: HtmlSignature) extends Ide
override def update(identity: Identity): Identity = identity.copy(htmlSignature = htmlSignature)
}
-case class IdentityUpdateRequest(name: Option[IdentityNameUpdate],
- replyTo: Option[IdentityReplyToUpdate],
- bcc: Option[IdentityBccUpdate],
- textSignature: Option[IdentityTextSignatureUpdate],
- htmlSignature: Option[IdentityHtmlSignatureUpdate]) extends IdentityUpdate {
+case class IdentityUpdateRequest(name: Option[IdentityNameUpdate] = None,
+ replyTo: Option[IdentityReplyToUpdate] = None,
+ bcc: Option[IdentityBccUpdate] = None,
+ textSignature: Option[IdentityTextSignatureUpdate] = None,
+ htmlSignature: Option[IdentityHtmlSignatureUpdate] = None) extends IdentityUpdate {
def update(identity: Identity): Identity =
List(name, replyTo, bcc, textSignature, htmlSignature)
.flatten
.foldLeft(identity)((acc, update) => update.update(acc))
+
+ def asCreationRequest(email: MailAddress): IdentityCreationRequest =
+ IdentityCreationRequest(
+ name = name.map(_.name),
+ email = email,
+ replyTo = replyTo.flatMap(_.replyTo),
+ bcc = bcc.flatMap(_.bcc),
+ textSignature = textSignature.map(_.textSignature),
+ htmlSignature = htmlSignature.map(_.htmlSignature))
}
trait CustomIdentityDAO {
def save(user: Username, creationRequest: IdentityCreationRequest): Publisher[Identity]
+ def save(user: Username, identityId: IdentityId, creationRequest: IdentityCreationRequest): Publisher[Identity]
+
def list(user: Username): Publisher[Identity]
def update(user: Username, identityId: IdentityId, identityUpdate: IdentityUpdate): Publisher[Unit]
@@ -123,13 +135,32 @@ class IdentityRepository @Inject()(customIdentityDao: CustomIdentityDAO, identit
SMono.error(ForbiddenSendFromException(creationRequest.email))
}
- def list(user: Username): Publisher[Identity] = SFlux.merge(Seq(
- customIdentityDao.list(user),
- SMono.fromCallable(() => identityFactory.listIdentities(user))
- .subscribeOn(Schedulers.elastic())
- .flatMapMany(SFlux.fromIterable)))
+ def list(user: Username): Publisher[Identity] = {
+ val customIdentities: SFlux[IdentityWithOrigin] = SFlux.fromPublisher(customIdentityDao.list(user))
+ .map(IdentityWithOrigin.fromCustom)
- def update(user: Username, identityId: IdentityId, identityUpdate: IdentityUpdate): Publisher[Unit] = customIdentityDao.update(user, identityId, identityUpdate)
+ val serverSetIdentities: SFlux[IdentityWithOrigin] = SMono.fromCallable(() => identityFactory.listIdentities(user))
+ .subscribeOn(Schedulers.elastic())
+ .flatMapMany(SFlux.fromIterable)
+ .map(IdentityWithOrigin.fromServerSet)
+
+ SFlux.merge(Seq(customIdentities, serverSetIdentities))
+ .groupBy(_.identity.id)
+ .flatMap(_.reduce(IdentityWithOrigin.merge))
+ .map(_.identity)
+ }
+
+ def update(user: Username, identityId: IdentityId, identityUpdateRequest: IdentityUpdateRequest): Publisher[Unit] =
+ SMono.fromPublisher(customIdentityDao.update(user, identityId, identityUpdateRequest))
+ .onErrorResume {
+ case error: IdentityNotFoundException =>
+ SFlux.fromIterable(identityFactory.listIdentities(user))
+ .filter(identity => identity.id.equals(identityId))
+ .next()
+ .flatMap(identity => SMono.fromPublisher(customIdentityDao.save(user, identityId, identityUpdateRequest.asCreationRequest(identity.email))))
+ .switchIfEmpty(SMono.error(error))
+ .`then`()
+ }
def delete(username: Username, ids: Seq[IdentityId]): Publisher[Unit] =
SMono.just(ids)
@@ -145,4 +176,30 @@ class IdentityRepository @Inject()(customIdentityDao: CustomIdentityDAO, identit
}
case class IdentityNotFoundException(id: IdentityId) extends RuntimeException(s"$id could not be found")
-case class IdentityForbiddenDeleteException(id: IdentityId) extends IllegalArgumentException(s"User do not have permission to delete $id")
\ No newline at end of file
+case class IdentityForbiddenDeleteException(id: IdentityId) extends IllegalArgumentException(s"User do not have permission to delete $id")
+
+object IdentityWithOrigin {
+ sealed trait IdentityWithOrigin {
+ def identity: Identity
+
+ def merge(other: IdentityWithOrigin): IdentityWithOrigin
+ }
+
+ case class CustomIdentityOrigin(inputIdentity: Identity) extends IdentityWithOrigin {
+ override def identity: Identity = inputIdentity
+
+ override def merge(other: IdentityWithOrigin): IdentityWithOrigin = this
+ }
+
+ case class ServerSetIdentityOrigin(inputIdentity: Identity) extends IdentityWithOrigin {
+ override def identity: Identity = inputIdentity
+
+ override def merge(other: IdentityWithOrigin): IdentityWithOrigin = other
+ }
+
+ def fromCustom(identity: Identity): IdentityWithOrigin = CustomIdentityOrigin(identity)
+
+ def fromServerSet(identity: Identity): IdentityWithOrigin = ServerSetIdentityOrigin(identity)
+
+ def merge(value1: IdentityWithOrigin, value2: IdentityWithOrigin): IdentityWithOrigin = value1.merge(value2)
+}
\ No newline at end of file
diff --git a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/memory/identity/MemoryCustomIdentityDAO.scala b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/memory/identity/MemoryCustomIdentityDAO.scala
index 3b7b5db..c5edbe6 100644
--- a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/memory/identity/MemoryCustomIdentityDAO.scala
+++ b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/memory/identity/MemoryCustomIdentityDAO.scala
@@ -32,7 +32,10 @@ class MemoryCustomIdentityDAO extends CustomIdentityDAO {
private val table: Table[Username, IdentityId, Identity] = HashBasedTable.create
override def save(user: Username, creationRequest: IdentityCreationRequest): Publisher[Identity] =
- SMono.fromCallable(() => IdentityId.generate)
+ save(user, IdentityId.generate, creationRequest)
+
+ override def save(user: Username, identityId: IdentityId, creationRequest: IdentityCreationRequest): Publisher[Identity] =
+ SMono.just(identityId)
.map(creationRequest.asIdentity)
.doOnNext(identity => table.put(user, identity.id, identity))
diff --git a/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/identity/IdentityRepositoryTest.scala b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/identity/IdentityRepositoryTest.scala
new file mode 100644
index 0000000..e476396
--- /dev/null
+++ b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/identity/IdentityRepositoryTest.scala
@@ -0,0 +1,200 @@
+/****************************************************************
+ * 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.api.identity
+
+import org.apache.james.core.{MailAddress, Username}
+import org.apache.james.jmap.api.identity.IdentityRepositoryTest.{BOB, CREATION_REQUEST, IDENTITY1, UPDATE_REQUEST}
+import org.apache.james.jmap.api.model.{EmailAddress, EmailerName, ForbiddenSendFromException, HtmlSignature, Identity, IdentityId, IdentityName, MayDeleteIdentity, TextSignature}
+import org.apache.james.jmap.memory.identity.MemoryCustomIdentityDAO
+import org.assertj.core.api.Assertions.{assertThat, assertThatCode, assertThatThrownBy}
+import org.junit.jupiter.api.{BeforeEach, Test}
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.{mock, when}
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+import scala.jdk.CollectionConverters._
+
+object IdentityRepositoryTest {
+ val BOB: Username = Username.of("bob@domain.tld")
+
+ val CREATION_REQUEST: IdentityCreationRequest = IdentityCreationRequest(name = Some(IdentityName("Bob (custom address)")),
+ email = BOB.asMailAddress(),
+ replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss")), new MailAddress("boss@domain.tld")))),
+ bcc = Some(List(EmailAddress(Some(EmailerName("My Boss 2")), new MailAddress("boss2@domain.tld")))),
+ textSignature = Some(TextSignature("text signature")),
+ htmlSignature = Some(HtmlSignature("html signature")))
+
+ val UPDATE_REQUEST: IdentityUpdateRequest = IdentityUpdateRequest(
+ name = Some(IdentityNameUpdate(IdentityName("Bob (new name)"))),
+ replyTo = Some(IdentityReplyToUpdate(Some(List(EmailAddress(Some(EmailerName("My Boss (updated)")), new MailAddress("boss-updated@domain.tld")))))),
+ bcc = Some(IdentityBccUpdate(Some(List(EmailAddress(Some(EmailerName("My Boss 2 (updated)")), new MailAddress("boss-updated-2@domain.tld")))))),
+ textSignature = Some(IdentityTextSignatureUpdate(TextSignature("text 2 signature"))),
+ htmlSignature = Some(IdentityHtmlSignatureUpdate(HtmlSignature("html 2 signature"))))
+
+ val IDENTITY1: Identity = Identity(id = IdentityId.generate,
+ name = IdentityName(""),
+ email = BOB.asMailAddress(),
+ replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss 1")), new MailAddress("boss1@domain.tld")))),
+ bcc = Some(List(EmailAddress(Some(EmailerName("My Boss bcc 1")), new MailAddress("boss_bcc_1@domain.tld")))),
+ textSignature = TextSignature(""),
+ htmlSignature = HtmlSignature(""),
+ mayDelete = MayDeleteIdentity(false))
+
+ val IDENTITY2: Identity = Identity(id = IdentityId.generate,
+ name = IdentityName(""),
+ email = BOB.asMailAddress(),
+ replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss")), new MailAddress("boss@domain.tld")))),
+ bcc = Some(List(EmailAddress(Some(EmailerName("My Boss 2")), new MailAddress("boss2@domain.tld")))),
+ textSignature = TextSignature("text signature"),
+ htmlSignature = HtmlSignature("html signature"),
+ mayDelete = MayDeleteIdentity(true))
+
+}
+
+class IdentityRepositoryTest {
+
+ var testee: IdentityRepository = _
+ var identityFactory: DefaultIdentitySupplier = _
+ var customIdentityDAO: CustomIdentityDAO = _
+
+
+ @BeforeEach
+ def setUp(): Unit = {
+ customIdentityDAO = new MemoryCustomIdentityDAO()
+ identityFactory = mock(classOf[DefaultIdentitySupplier])
+ testee = new IdentityRepository(customIdentityDAO, identityFactory)
+ }
+
+ @Test
+ def saveShouldSuccess(): Unit = {
+ when(identityFactory.userCanSendFrom(any(), any())).thenReturn(true)
+ assertThatCode(() => SMono.fromPublisher(testee.save(BOB, CREATION_REQUEST)).block())
+ .doesNotThrowAnyException()
+ }
+
+ @Test
+ def saveShouldFailWhenUserCanNotSendFrom(): Unit = {
+ when(identityFactory.userCanSendFrom(any(), any())).thenReturn(false)
+ assertThatThrownBy(() => SMono.fromPublisher(testee.save(BOB, CREATION_REQUEST)).block())
+ .isInstanceOf(classOf[ForbiddenSendFromException])
+ }
+
+ @Test
+ def listShouldReturnCustomAndServerSetEntries(): Unit = {
+ val customIdentity: Identity = SMono.fromPublisher(customIdentityDAO.save(BOB, CREATION_REQUEST)).block()
+ when(identityFactory.listIdentities(BOB)).thenReturn(List(IDENTITY1))
+
+ assertThat(SFlux.fromPublisher(testee.list(BOB)).collectSeq().block().asJava)
+ .containsExactlyInAnyOrder(IDENTITY1, customIdentity)
+ }
+
+ @Test
+ def listShouldReturnCustomEntryWhenIdExistsInBothCustomAndServerSet(): Unit = {
+ val customIdentity: Identity = SMono.fromPublisher(customIdentityDAO.save(BOB, CREATION_REQUEST)).block()
+ val differentIdentityWithSameId: Identity = customIdentity.copy(name = IdentityName("different name"))
+ when(identityFactory.listIdentities(BOB)).thenReturn(List(differentIdentityWithSameId))
+
+ assertThat(SFlux.fromPublisher(testee.list(BOB)).collectSeq().block().asJava)
+ .containsExactlyInAnyOrder(customIdentity)
+ }
+
+ @Test
+ def updateShouldFailWhenIdNotFoundInBothCustomAndServerSetDAO(): Unit = {
+ when(identityFactory.listIdentities(BOB)).thenReturn(List())
+
+ assertThatThrownBy(() => SMono.fromPublisher(testee.update(BOB, IdentityId.generate, UPDATE_REQUEST)).block())
+ .isInstanceOf(classOf[IdentityNotFoundException])
+ }
+
+ @Test
+ def updateShouldSuccessWhenCustomNotFoundAndServerSetExists(): Unit = {
+ when(identityFactory.listIdentities(BOB)).thenReturn(List(IDENTITY1))
+
+ assertThatCode(() => SMono.fromPublisher(testee.update(BOB, IDENTITY1.id, UPDATE_REQUEST)).block())
+ .doesNotThrowAnyException()
+ }
+
+ @Test
+ def updateShouldSuccessWhenCustomExists(): Unit = {
+ when(identityFactory.listIdentities(BOB)).thenReturn(List())
+ val customIdentity: Identity = SMono.fromPublisher(customIdentityDAO.save(BOB, CREATION_REQUEST)).block()
+
+ assertThatCode(() => SMono.fromPublisher(testee.update(BOB, customIdentity.id, UPDATE_REQUEST)).block())
+ .doesNotThrowAnyException()
+ }
+
+ @Test
+ def updateShouldModifiedEntry(): Unit = {
+ when(identityFactory.listIdentities(BOB)).thenReturn(List())
+ val customIdentity: Identity = SMono.fromPublisher(customIdentityDAO.save(BOB, CREATION_REQUEST)).block()
+
+ assertThatCode(() => SMono.fromPublisher(testee.update(BOB, customIdentity.id, UPDATE_REQUEST)).block())
+ .doesNotThrowAnyException()
+
+ assertThat(SFlux(testee.list(BOB)).collectSeq().block().asJava)
+ .containsExactlyInAnyOrder(Identity(id = customIdentity.id,
+ name = IdentityName("Bob (new name)"),
+ email = BOB.asMailAddress(),
+ replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss (updated)")), new MailAddress("boss-updated@domain.tld")))),
+ bcc = Some(List(EmailAddress(Some(EmailerName("My Boss 2 (updated)")), new MailAddress("boss-updated-2@domain.tld")))),
+ textSignature = TextSignature("text 2 signature"),
+ htmlSignature = HtmlSignature("html 2 signature"),
+ mayDelete = MayDeleteIdentity(true)))
+ }
+
+ @Test
+ def updateShouldSuccessWhenMultiUpdateServerSetId(): Unit = {
+ when(identityFactory.listIdentities(BOB)).thenReturn(List(IDENTITY1))
+ SMono.fromPublisher(testee.update(BOB, IDENTITY1.id, UPDATE_REQUEST)).block()
+
+ assertThatCode(() => SMono.fromPublisher(testee.update(BOB, IDENTITY1.id, UPDATE_REQUEST.copy(name = Some(IdentityNameUpdate(IdentityName("Bob (3)")))))).block())
+ .doesNotThrowAnyException()
+
+ assertThat(SFlux(testee.list(BOB)).collectSeq().block().asJava)
+ .containsExactlyInAnyOrder(Identity(id = IDENTITY1.id,
+ name = IdentityName("Bob (3)"),
+ email = BOB.asMailAddress(),
+ replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss (updated)")), new MailAddress("boss-updated@domain.tld")))),
+ bcc = Some(List(EmailAddress(Some(EmailerName("My Boss 2 (updated)")), new MailAddress("boss-updated-2@domain.tld")))),
+ textSignature = TextSignature("text 2 signature"),
+ htmlSignature = HtmlSignature("html 2 signature"),
+ mayDelete = MayDeleteIdentity(true)))
+ }
+
+ @Test
+ def updateShouldSuccessWhenSecondPartialUpdateServerSetId(): Unit = {
+ when(identityFactory.listIdentities(BOB)).thenReturn(List(IDENTITY1))
+ SMono.fromPublisher(testee.update(BOB, IDENTITY1.id, UPDATE_REQUEST)).block()
+ val secondUpdateWithName: IdentityUpdateRequest = IdentityUpdateRequest(name = Some(IdentityNameUpdate(name = IdentityName("Bob (3)"))))
+
+ assertThatCode(() => SMono.fromPublisher(testee.update(BOB, IDENTITY1.id, secondUpdateWithName)).block())
+ .doesNotThrowAnyException()
+
+ assertThat(SFlux(testee.list(BOB)).collectSeq().block().asJava)
+ .containsExactlyInAnyOrder(Identity(id = IDENTITY1.id,
+ name = IdentityName("Bob (3)"),
+ email = BOB.asMailAddress(),
+ replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss (updated)")), new MailAddress("boss-updated@domain.tld")))),
+ bcc = Some(List(EmailAddress(Some(EmailerName("My Boss 2 (updated)")), new MailAddress("boss-updated-2@domain.tld")))),
+ textSignature = TextSignature("text 2 signature"),
+ htmlSignature = HtmlSignature("html 2 signature"),
+ mayDelete = MayDeleteIdentity(true)))
+ }
+}
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/IdentityGetContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentityGetContract.scala
index 69219e0..dadfbb6 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentityGetContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentityGetContract.scala
@@ -26,8 +26,11 @@ 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 javax.inject.Inject
import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
+import net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER
+import net.javacrumbs.jsonunit.core.internal.Options
import org.apache.http.HttpStatus.SC_OK
import org.apache.james.GuiceJamesServer
import org.apache.james.core.{MailAddress, Username}
@@ -198,6 +201,7 @@ trait IdentityGetContract {
.asString
assertThatJson(response)
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
.inPath("methodResponses[0][1]")
.isEqualTo(
s"""{
@@ -300,6 +304,7 @@ trait IdentityGetContract {
.asString
assertThatJson(response)
+ .withOptions(new Options(IGNORING_ARRAY_ORDER))
.inPath("methodResponses[0][1]")
.isEqualTo(
s"""{
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/IdentitySetContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
index f628c06..c2d0ec0 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
@@ -212,7 +212,6 @@ trait IdentitySetContract {
|}""".stripMargin
val response = `given`
- .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.body(request)
.when
.post
@@ -1164,7 +1163,6 @@ trait IdentitySetContract {
private def createNewIdentity(clientId: String): String =
`given`
- .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.body(
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:submission"],
@@ -1539,4 +1537,134 @@ trait IdentitySetContract {
|}""".stripMargin)
}
+
+ @Test
+ def updateShouldAcceptServerSetId(): Unit = {
+ val identityId: String = `given`
+ .body(
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:submission"],
+ | "methodCalls": [
+ | ["Identity/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": null
+ | }, "c2"
+ | ]
+ |
+ | ]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .extract()
+ .jsonPath()
+ .get("methodResponses[0][1].list[0].id")
+
+ val response: String = `given`
+ .body(
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:submission"
+ | ],
+ | "methodCalls": [
+ | [
+ | "Identity/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "update": {
+ | "$identityId": {
+ | "name": "NewName1",
+ | "replyTo": [
+ | {
+ | "name": "Difference Alice",
+ | "email": "alice2@domain.tld"
+ | }
+ | ],
+ | "bcc": [
+ | {
+ | "name": "Difference David",
+ | "email": "david2@domain.tld"
+ | }
+ | ],
+ | "textSignature": "Difference text signature",
+ | "htmlSignature": "<p>Difference html signature</p>"
+ | }
+ | }
+ | },
+ | "c1"
+ | ],
+ | [
+ | "Identity/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": [
+ | "$identityId"
+ | ]
+ | },
+ | "c2"
+ | ]
+ | ]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [
+ | "Identity/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+ | "updated": {
+ | "$identityId": { }
+ | }
+ | },
+ | "c1"
+ | ],
+ | [
+ | "Identity/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+ | "list": [
+ | {
+ | "id": "$identityId",
+ | "name": "NewName1",
+ | "email": "bob@domain.tld",
+ | "replyTo": [
+ | {
+ | "name": "Difference Alice",
+ | "email": "alice2@domain.tld"
+ | }
+ | ],
+ | "bcc": [
+ | {
+ | "name": "Difference David",
+ | "email": "david2@domain.tld"
+ | }
+ | ],
+ | "textSignature": "Difference text signature",
+ | "htmlSignature": "<p>Difference html signature</p>",
+ | "mayDelete": true
+ | }
+ | ]
+ | },
+ | "c2"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
}
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org