diff --git a/docs/modules/ROOT/pages/tmail-backend/jmap-extensions/calendarEventReply.adoc b/docs/modules/ROOT/pages/tmail-backend/jmap-extensions/calendarEventReply.adoc index e34bb8614d..cebec076f2 100644 --- a/docs/modules/ROOT/pages/tmail-backend/jmap-extensions/calendarEventReply.adoc +++ b/docs/modules/ROOT/pages/tmail-backend/jmap-extensions/calendarEventReply.adoc @@ -96,3 +96,102 @@ However, in the response properties, 'rejected' replace 'accepted', while 'notRe == CalendarEvent/maybe Similarly to CalendarEvent/accept, CalendarEvent/maybe function in a similar manner. However, in the response properties, 'maybe' replace 'accepted', while 'notMaybe' replace 'notAccepted'. + +== CalendarEventAttendance/get +This method allow clients to get the attendance status of a calendar event invitation. +The CalendarEventAttendance/get method takes the following arguments: + +- *accountId*: `Id` The id of the account to use. +- *blobIds*: `Id[]` The ids correspond to the blob of calendar event invitation files that the user wants to query for status. + +The response object contains the following arguments: + +- *accountId*: `Id` The id of the account used for the call. +- *accepted*: `Id[CalendarEvent[]]|null` A list of ids of the calendar events that were successfully accepted, or `null` if none. +- *rejected*: `Id[CalendarEvent[]]|null` A list of ids of the calendar events that were successfully rejected, or `null` if none. +- *tentativelyAccepted*: `Id[CalendarEvent[]]|null` A list of ids of the calendar events that were tentatively accepted successfully (user chose maybe as a response for his attendance), or `null` if none. +- *needsAction*: `Id[CalendarEvent[]]|null` A list of ids of the calendar events that were neither accepted, rejected nor tentatively accepted by user thus they need action, or `null` if none. +- *notFound*: `Id[]|null` A list of blob ids given that could not be found, or `null` if none. +- *notDone*: `Id[SetError]|null` A map of the blobId to a SetError object for each calendar event that failed to reply, or null if all successful. + +The associated `replySupportedLanguage` capability property is not needed for this method to function. + +Note: The sum of sizes of arrays `accepted`, `rejected`, `tentativelyAccepted`, `needsAction` and `notFound` must be equal to the size of `blobIds`, otherwise the server must return an error. + +== Example + +The client makes a request to get the attendance status of calendar event invitations `1_5` that was previously accepted and `1_3` that was rejected: + +.... +{ + "using": ["urn:ietf:params:jmap:core", "com:linagora:params:calendar:event"], + "methodCalls": [ + [ "CalendarEventAttendance/get", { + "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + "blobIds": ["1_5", "1_3"] + }, "c1"] + ] +} +.... + +The server responds: + +[source] +---- +[[ "CalendarEventAttendance/get", +{ + "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + "accepted": [ "1_5" ], + "rejected": [ "1_3" ], + "tentativelyAccepted": [], + "needsAction": [] +}, "c1" ]] +---- + +In the case that a blob id is not found or not accessible for current user, the server would respond: + +[source] +---- +[[ "CalendarEventAttendance/get", +{ + "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + "accepted": [], + "rejected": [], + "tentativelyAccepted": [], + "needsAction": [] + "notFound": ["0f9f65ab-dc7b-4146-850f-6e4881093965" ] +}, "c1" ]] +---- + +If the blob id was in an invalid format, the server would respond: + +[source] +---- +[[ "CalendarEventAttendance/get", +{ + "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + "accepted": [], + "rejected": [], + "tentativelyAccepted": [], + "needsAction": [], + notDone: { + "BAD_BLOB_ID": { + "type": "invalidArguments", + "description": "Invalid BlobId 'BAD_BLOB_ID'. Blob id needs to match this format: {message_id}_{partId1}_{partId2}_..." + } + } +}, "c1" ]] +---- + +If the number of blob ids in the request exceeds the limit (currently 16), the server would respond: + +---- +[ + "error", + { + "type": "requestTooLarge", + "description": "The number of ids requested by the client exceeds the maximum number the server is willing to process in a single method call" + }, + "c1" +] +---- diff --git a/tmail-backend/integration-tests/jmap/jmap-integration-tests-common/src/main/scala/com/linagora/tmail/james/common/LinagoraCalendarEventAttendanceGetMethodContract.scala b/tmail-backend/integration-tests/jmap/jmap-integration-tests-common/src/main/scala/com/linagora/tmail/james/common/LinagoraCalendarEventAttendanceGetMethodContract.scala new file mode 100644 index 0000000000..a931481097 --- /dev/null +++ b/tmail-backend/integration-tests/jmap/jmap-integration-tests-common/src/main/scala/com/linagora/tmail/james/common/LinagoraCalendarEventAttendanceGetMethodContract.scala @@ -0,0 +1,644 @@ +package com.linagora.tmail.james.common + +import com.linagora.tmail.james.common.LinagoraCalendarEventMethodContractUtilities.sendInvitationEmailToBobAndGetIcsBlobIds +import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT +import io.restassured.RestAssured.{`given`, requestSpecification} +import io.restassured.http.ContentType.JSON +import io.restassured.specification.RequestSpecification +import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson +import net.javacrumbs.jsonunit.core.Option +import net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER +import org.apache.http.HttpStatus.SC_OK +import org.apache.james.GuiceJamesServer +import org.apache.james.jmap.http.UserCredential +import org.apache.james.jmap.rfc8621.contract.Fixture._ +import org.apache.james.jmap.rfc8621.contract.probe.DelegationProbe +import org.apache.james.mailbox.model.MailboxPath +import org.apache.james.modules.MailboxProbeImpl +import org.apache.james.utils.DataProbeImpl +import org.junit.jupiter.api.{BeforeEach, Test} +import play.api.libs.json.Json + +trait LinagoraCalendarEventAttendanceGetMethodContract { + + @BeforeEach + def setUp(server: GuiceJamesServer): Unit = { + server.getProbe(classOf[DataProbeImpl]) + .fluent + .addDomain(_2_DOT_DOMAIN.asString) + .addDomain(DOMAIN.asString) + .addUser(BOB.asString, BOB_PASSWORD) + .addUser(ALICE.asString, ALICE_PASSWORD) + .addUser(ANDRE.asString, ANDRE_PASSWORD) + + requestSpecification = baseRequestSpecBuilder(server) + .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD))) + .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .build + + server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB)) + } + + @Test + def shouldReturnAccepted(server: GuiceJamesServer): Unit = { + val blobId: String = + sendInvitationEmailToBobAndGetIcsBlobIds(server, "emailWithAliceInviteBobIcsAttachment.eml", icsPartId = "3") + + acceptInvitation(blobId) + + val request: String = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "com:linagora:params:calendar:event"], + | "methodCalls": [[ + | "CalendarEventAttendance/get", + | { + | "accountId": "$ACCOUNT_ID", + | "blobIds": [ "$blobId" ] + | }, + | "c1"]] + |}""".stripMargin + + val response = + `given` + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .inPath("methodResponses[0]") + .isEqualTo( + s"""|[ + | "CalendarEventAttendance/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "accepted": ["$blobId"], + | "rejected": [], + | "tentativelyAccepted": [], + | "needsAction": [] + | }, + | "c1" + |]""".stripMargin + ) + } + + @Test + def shouldReturnRejected(server: GuiceJamesServer): Unit = { + val blobId: String = + sendInvitationEmailToBobAndGetIcsBlobIds(server, "emailWithAliceInviteBobIcsAttachment.eml", icsPartId = "3") + + rejectInvitation(blobId) + + val request: String = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "com:linagora:params:calendar:event"], + | "methodCalls": [[ + | "CalendarEventAttendance/get", + | { + | "accountId": "$ACCOUNT_ID", + | "blobIds": [ "$blobId" ] + | }, + | "c1"]] + |}""".stripMargin + + val response = + `given` + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .inPath("methodResponses[0]") + .isEqualTo( + s"""|[ + | "CalendarEventAttendance/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "accepted": [], + | "rejected": ["$blobId"], + | "tentativelyAccepted": [], + | "needsAction": [] + | }, + | "c1" + |]""".stripMargin + ) + } + + @Test + def shouldReturnMaybe(server: GuiceJamesServer): Unit = { + val blobId: String = + sendInvitationEmailToBobAndGetIcsBlobIds(server, "emailWithAliceInviteBobIcsAttachment.eml", icsPartId = "3") + + maybeInvitation(blobId) + + val request: String = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "com:linagora:params:calendar:event"], + | "methodCalls": [[ + | "CalendarEventAttendance/get", + | { + | "accountId": "$ACCOUNT_ID", + | "blobIds": [ "$blobId" ] + | }, + | "c1"]] + |}""".stripMargin + + val response = + `given` + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .inPath("methodResponses[0]") + .isEqualTo( + s"""|[ + | "CalendarEventAttendance/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "accepted": [], + | "rejected": [], + | "tentativelyAccepted": ["$blobId"], + | "needsAction": [] + | }, + | "c1" + |]""".stripMargin) + } + + @Test + def shouldReturnNeedsActionWhenNoEventAttendanceFlagAttachedToMail(server: GuiceJamesServer): Unit = { + val blobId: String = + sendInvitationEmailToBobAndGetIcsBlobIds(server, "emailWithAliceInviteBobIcsAttachment.eml", icsPartId = "3") + + val request: String = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "com:linagora:params:calendar:event"], + | "methodCalls": [[ + | "CalendarEventAttendance/get", + | { + | "accountId": "$ACCOUNT_ID", + | "blobIds": [ "$blobId" ] + | }, + | "c1"]] + |}""".stripMargin + + val response = + `given` + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .inPath("methodResponses[0]") + .isEqualTo( + s"""|[ + | "CalendarEventAttendance/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "accepted": [], + | "rejected": [], + | "tentativelyAccepted": [], + | "needsAction": ["$blobId"] + | }, + | "c1" + |]""".stripMargin) + } + + @Test + def shouldFailWhenNumberOfBlobIdsTooLarge(): Unit = { + val blobIds: List[String] = Range.inclusive(1, 999) + .map(_ + "") + .toList + val blobIdsJson = blobIdsAsJson(blobIds) + val request: String = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "com:linagora:params:calendar:event"], + | "methodCalls": [[ + | "CalendarEventAttendance/get", + | { + | "accountId": "$ACCOUNT_ID", + | "blobIds": $blobIdsJson + | }, + | "c1"]] + |}""".stripMargin + + val response: String = + `given` + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .inPath("methodResponses[0]") + .isEqualTo( + s"""[ + | "error", + | { + | "type": "requestTooLarge", + | "description": "The number of ids requested by the client exceeds the maximum number the server is willing to process in a single method call" + | }, + | "c1" + |]""".stripMargin) + } + + @Test + def shouldFailWhenWrongAccountId(): Unit = { + val request: String = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "com:linagora:params:calendar:event"], + | "methodCalls": [[ + | "CalendarEventAttendance/get", + | { + | "accountId": "unknownAccountId", + | "blobIds": [ "0f9f65ab-dc7b-4146-850f-6e4881093965" ] + | }, + | "c1"]] + |}""".stripMargin + + val response = + `given` + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .inPath("methodResponses[0]") + .isEqualTo( + s"""[ + | "error", + | { + | "type": "accountNotFound" + | }, + | "c1" + |]""".stripMargin) + } + + @Test + def shouldNotFoundWhenDoesNotHavePermission(server: GuiceJamesServer): Unit = { + val blobId: String = + sendInvitationEmailToBobAndGetIcsBlobIds(server, "emailWithAliceInviteBobIcsAttachment.eml", icsPartId = "3") + + val request: String = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "com:linagora:params:calendar:event"], + | "methodCalls": [[ + | "CalendarEventAttendance/get", + | { + | "accountId": "$ANDRE_ACCOUNT_ID", + | "blobIds": [ "$blobId" ] + | }, + | "c1"]] + |}""".stripMargin + + val response = + `given`(buildAndreRequestSpecification(server)) + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .inPath("methodResponses[0]") + .isEqualTo( + s"""|[ + | "CalendarEventAttendance/get", + | { + | "accountId": "$ANDRE_ACCOUNT_ID", + | "accepted": [], + | "rejected": [], + | "tentativelyAccepted": [], + | "needsAction": [], + | "notFound": ["$blobId"] + | }, + | "c1" + |]""".stripMargin) + } + + @Test + def shouldReturnUnknownMethodWhenMissingOneCapability(): Unit = { + val request: String = + s"""{ + | "using": ["urn:ietf:params:jmap:core"], + | "methodCalls": [[ + | "CalendarEventAttendance/get", + | { + | "accountId": "$ACCOUNT_ID", + | "blobIds": [ "123" ] + | }, + | "c1"]] + |}""".stripMargin + + val response = + `given` + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .inPath("methodResponses[0]") + .isEqualTo( + s"""[ + | "error", + | { + | "type": "unknownMethod", + | "description": "Missing capability(ies): com:linagora:params:calendar:event" + | }, + | "c1"]""".stripMargin) + } + + @Test + def shouldReturnUnknownMethodWhenMissingAllCapabilities(): Unit = { + val request: String = + s"""{ + | "using": [], + | "methodCalls": [[ + | "CalendarEventAttendance/get", + | { + | "accountId": "$ACCOUNT_ID", + | "blobIds": [ "123" ] + | }, + | "c1"]] + |}""".stripMargin + + val response = + `given` + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .inPath("methodResponses[0]") + .isEqualTo( + s"""[ + | "error", + | { + | "type": "unknownMethod", + | "description": "Missing capability(ies): urn:ietf:params:jmap:core, com:linagora:params:calendar:event" + | }, + | "c1"]""".stripMargin) + } + + @Test + def shouldSucceedWhenMixSeveralCases(server: GuiceJamesServer): Unit = { + val acceptedEventBlobId: String = + sendInvitationEmailToBobAndGetIcsBlobIds(server, "emailWithAliceInviteBobIcsAttachment.eml", icsPartId = "3") + acceptInvitation(acceptedEventBlobId) + + val rejectedEventBlobId: String = + sendInvitationEmailToBobAndGetIcsBlobIds(server, "emailWithAliceInviteBobIcsAttachment.eml", icsPartId = "3") + rejectInvitation(rejectedEventBlobId) + + val rejectedEventBlobId2: String = + sendInvitationEmailToBobAndGetIcsBlobIds(server, "emailWithAliceInviteBobIcsAttachment.eml", icsPartId = "3") + rejectInvitation(rejectedEventBlobId2) + + val needsActionEventBlobId: String = + sendInvitationEmailToBobAndGetIcsBlobIds(server, "emailWithAliceInviteBobIcsAttachment.eml", icsPartId = "3") + + val notFoundBlobId = "99999_99999" + + val request: String = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "com:linagora:params:calendar:event"], + | "methodCalls": [[ + | "CalendarEventAttendance/get", + | { + | "accountId": "$ACCOUNT_ID", + | "blobIds": [ "$acceptedEventBlobId", "$rejectedEventBlobId", "$needsActionEventBlobId", "$rejectedEventBlobId2", "$notFoundBlobId" ] + | }, + | "c1"]] + |}""".stripMargin + + val response = + `given` + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .when(Option.IGNORING_ARRAY_ORDER) + .inPath("methodResponses[0]") + .isEqualTo( + s"""|[ + | "CalendarEventAttendance/get", + | { + | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6", + | "accepted": ["$acceptedEventBlobId"], + | "rejected": ["$rejectedEventBlobId", "$rejectedEventBlobId2"], + | "tentativelyAccepted": [], + | "needsAction": ["$needsActionEventBlobId"], + | "notFound": ["$notFoundBlobId"] + | }, + | "c1" + |]""".stripMargin) + } + + @Test + def shouldSucceedWhenDelegated(server: GuiceJamesServer): Unit = { + server.getProbe(classOf[DelegationProbe]).addAuthorizedUser(BOB, ANDRE) + + val blobId: String = + sendInvitationEmailToBobAndGetIcsBlobIds(server, "emailWithAliceInviteBobIcsAttachment.eml", icsPartId = "3") + + acceptInvitation(blobId) + + val bobAccountId = ACCOUNT_ID + val request: String = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "com:linagora:params:calendar:event"], + | "methodCalls": [[ + | "CalendarEventAttendance/get", + | { + | "accountId": "$bobAccountId", + | "blobIds": [ "$blobId" ] + | }, + | "c1"]] + |}""".stripMargin + + val response = + `given`(buildAndreRequestSpecification(server)) + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + .extract + .body + .asString + + assertThatJson(response) + .withOptions(IGNORING_ARRAY_ORDER) + .inPath("methodResponses[0]") + .isEqualTo( + s"""[ + | "CalendarEventAttendance/get", + | { + | "accountId": "$ACCOUNT_ID", + | "accepted": ["$blobId"], + | "rejected": [], + | "tentativelyAccepted": [], + | "needsAction": [] + | }, + | "c1" + |]""".stripMargin) + } + + private def acceptInvitation(blobId: String) = { + val request: String = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "com:linagora:params:calendar:event"], + | "methodCalls": [[ + | "CalendarEvent/accept", + | { + | "accountId": "$ACCOUNT_ID", + | "blobIds": [ "$blobId" ] + | }, + | "c1"]] + |}""".stripMargin + + `given` + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + } + + private def rejectInvitation(blobId: String) = { + val request: String = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "com:linagora:params:calendar:event"], + | "methodCalls": [[ + | "CalendarEvent/reject", + | { + | "accountId": "$ACCOUNT_ID", + | "blobIds": [ "$blobId" ] + | }, + | "c1"]] + |}""".stripMargin + + `given` + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + } + + private def maybeInvitation(blobId: String) = { + val request: String = + s"""{ + | "using": [ + | "urn:ietf:params:jmap:core", + | "com:linagora:params:calendar:event"], + | "methodCalls": [[ + | "CalendarEvent/maybe", + | { + | "accountId": "$ACCOUNT_ID", + | "blobIds": [ "$blobId" ] + | }, + | "c1"]] + |}""".stripMargin + + `given` + .body(request) + .when + .post + .`then` + .statusCode(SC_OK) + .contentType(JSON) + } + + private def blobIdsAsJson(blobIds: List[String]) : String = + Json.stringify(Json.arr(blobIds)).replace("[[", "[").replace("]]", "]") + + private def buildAndreRequestSpecification(server: GuiceJamesServer): RequestSpecification = + baseRequestSpecBuilder(server) + .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD))) + .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER) + .build + +} diff --git a/tmail-backend/integration-tests/jmap/memory-jmap-integration-tests/src/test/java/com/linagora/tmail/james/MemoryLinagoraCalendarEventAttendanceGetMethodTest.java b/tmail-backend/integration-tests/jmap/memory-jmap-integration-tests/src/test/java/com/linagora/tmail/james/MemoryLinagoraCalendarEventAttendanceGetMethodTest.java new file mode 100644 index 0000000000..fde1a429d5 --- /dev/null +++ b/tmail-backend/integration-tests/jmap/memory-jmap-integration-tests/src/test/java/com/linagora/tmail/james/MemoryLinagoraCalendarEventAttendanceGetMethodTest.java @@ -0,0 +1,34 @@ +package com.linagora.tmail.james; + +import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT; + +import org.apache.james.JamesServerBuilder; +import org.apache.james.JamesServerExtension; +import org.apache.james.jmap.rfc8621.contract.probe.DelegationProbeModule; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.linagora.tmail.james.app.MemoryConfiguration; +import com.linagora.tmail.james.app.MemoryServer; +import com.linagora.tmail.james.common.LinagoraCalendarEventAcceptMethodContract; +import com.linagora.tmail.james.common.LinagoraCalendarEventAttendanceGetMethodContract; +import com.linagora.tmail.james.jmap.firebase.FirebaseModuleChooserConfiguration; +import com.linagora.tmail.module.LinagoraTestJMAPServerModule; + +import java.util.UUID; + +public class MemoryLinagoraCalendarEventAttendanceGetMethodTest implements + LinagoraCalendarEventAttendanceGetMethodContract { + + @RegisterExtension + static JamesServerExtension + jamesServerExtension = new JamesServerBuilder(tmpDir -> + MemoryConfiguration.builder() + .workingDirectory(tmpDir) + .configurationFromClasspath() + .usersRepository(DEFAULT) + .firebaseModuleChooserConfiguration(FirebaseModuleChooserConfiguration.DISABLED) + .build()) + .server(configuration -> MemoryServer.createServer(configuration) + .overrideWith(new LinagoraTestJMAPServerModule(), new DelegationProbeModule())) + .build(); +} diff --git a/tmail-backend/jmap/extensions/src/main/java/com/linagora/tmail/james/jmap/EventAttendanceRepository.java b/tmail-backend/jmap/extensions/src/main/java/com/linagora/tmail/james/jmap/EventAttendanceRepository.java index f21a4b4f78..d0c7b9e610 100644 --- a/tmail-backend/jmap/extensions/src/main/java/com/linagora/tmail/james/jmap/EventAttendanceRepository.java +++ b/tmail-backend/jmap/extensions/src/main/java/com/linagora/tmail/james/jmap/EventAttendanceRepository.java @@ -4,14 +4,14 @@ import org.apache.james.core.Username; import org.apache.james.jmap.mail.BlobIds; -import org.apache.james.mailbox.model.MessageId; import org.reactivestreams.Publisher; +import com.linagora.tmail.james.jmap.method.CalendarEventAttendanceResults; import com.linagora.tmail.james.jmap.model.CalendarEventReplyResults; import com.linagora.tmail.james.jmap.model.LanguageLocation; public interface EventAttendanceRepository { - Publisher getAttendanceStatus(Username username, MessageId messageId); + Publisher getAttendanceStatus(Username username, BlobIds calendarEventBlobIds); Publisher setAttendanceStatus(Username username, AttendanceStatus attendanceStatus, BlobIds eventBlobIds, diff --git a/tmail-backend/jmap/extensions/src/main/java/com/linagora/tmail/james/jmap/MessagePartBlobId.java b/tmail-backend/jmap/extensions/src/main/java/com/linagora/tmail/james/jmap/MessagePartBlobId.java index 02eec2be54..c2fd672c0a 100644 --- a/tmail-backend/jmap/extensions/src/main/java/com/linagora/tmail/james/jmap/MessagePartBlobId.java +++ b/tmail-backend/jmap/extensions/src/main/java/com/linagora/tmail/james/jmap/MessagePartBlobId.java @@ -13,6 +13,9 @@ import reactor.util.function.Tuple2; import reactor.util.function.Tuples; +import scala.util.Failure; +import scala.util.Success; +import scala.util.Try; public final class MessagePartBlobId { private static final Pattern MESSAGE_PART_BLOB_ID_PATTERN = @@ -40,6 +43,14 @@ public MessagePartBlobId(String value) { .toList(); } + public static Try tryParse(String blobId) { + try { + return new Success<>(new MessagePartBlobId(blobId)); + } catch (Exception e) { + return new Failure<>(e); + } + } + public String getMessageId() { return messageId; } diff --git a/tmail-backend/jmap/extensions/src/main/java/com/linagora/tmail/james/jmap/StandaloneEventAttendanceRepository.java b/tmail-backend/jmap/extensions/src/main/java/com/linagora/tmail/james/jmap/StandaloneEventAttendanceRepository.java index 61b79a0df6..f32ff9dff6 100644 --- a/tmail-backend/jmap/extensions/src/main/java/com/linagora/tmail/james/jmap/StandaloneEventAttendanceRepository.java +++ b/tmail-backend/jmap/extensions/src/main/java/com/linagora/tmail/james/jmap/StandaloneEventAttendanceRepository.java @@ -8,6 +8,7 @@ import org.apache.james.core.Username; import org.apache.james.jmap.core.AccountId; +import org.apache.james.jmap.mail.BlobId; import org.apache.james.jmap.mail.BlobIds; import org.apache.james.mailbox.MailboxSession; import org.apache.james.mailbox.MessageIdManager; @@ -20,7 +21,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.linagora.tmail.james.jmap.method.CalendarEventAttendanceResults; +import com.linagora.tmail.james.jmap.method.CalendarEventAttendanceResults$; import com.linagora.tmail.james.jmap.method.CalendarEventReplyPerformer; +import com.linagora.tmail.james.jmap.method.EventAttendanceStatusEntry; import com.linagora.tmail.james.jmap.model.CalendarEventReplyRequest; import com.linagora.tmail.james.jmap.model.CalendarEventReplyResults; import com.linagora.tmail.james.jmap.model.LanguageLocation; @@ -48,21 +52,44 @@ public StandaloneEventAttendanceRepository(MessageIdManager messageIdManager, Se } @Override - public Publisher getAttendanceStatus(Username username, MessageId messageId) { - LOGGER.trace("Getting attendance status for user '{}' and message '{}'", username, messageId); + public Publisher getAttendanceStatus(Username username, BlobIds calendarEventBlobIds) { + LOGGER.trace("Getting attendance status for user '{}' and message '{}'", username, + calendarEventBlobIds); MailboxSession systemMailboxSession = sessionProvider.createSystemSession(username); - return getFlags(messageId, systemMailboxSession) - .flatMap(userFlags -> Mono.justOrEmpty(AttendanceStatus.fromMessageFlags(userFlags))) - .switchIfEmpty(handleMissingEventAttendanceFlag(messageId)); + return Flux.fromIterable(JavaConverters.seqAsJavaList(calendarEventBlobIds.value())) + .flatMap(blobId -> getAttendanceStatusFromEventBlob(blobId.value(), systemMailboxSession)) + .reduce(CalendarEventAttendanceResults$.MODULE$.empty(), CalendarEventAttendanceResults$.MODULE$::merge); + } + + private Mono getAttendanceStatusFromEventBlob(String blobId, MailboxSession systemMailboxSession) { + return extractMessageId(blobId) + .flatMap(messageId -> + Mono.fromDirect(messageIdManager.getMessagesReactive(List.of(messageId), FetchGroup.MINIMAL, systemMailboxSession)) + .flatMap(messageResult -> getAttendanceStatusFromMessage(messageResult, blobId)) + .defaultIfEmpty(CalendarEventAttendanceResults$.MODULE$.notFound(BlobId.of(blobId).get()))) + .onErrorResume(Exception.class, (error) -> + Mono.just(CalendarEventAttendanceResults$.MODULE$.notDone( + BlobId.of(blobId).get(), error, systemMailboxSession))); } - private Mono handleMissingEventAttendanceFlag(MessageId messageId) { + private Mono getAttendanceStatusFromMessage(MessageResult messageResult, String blobId) { + return Mono.just(messageResult.getFlags()) + .flatMap(userFlags -> + Mono.justOrEmpty(AttendanceStatus.fromMessageFlags(userFlags)) + .map(attendanceStatus -> + CalendarEventAttendanceResults$.MODULE$.done( + new EventAttendanceStatusEntry(blobId, attendanceStatus)))) + .defaultIfEmpty(handleMissingEventAttendanceFlag(blobId)); + } + + private CalendarEventAttendanceResults handleMissingEventAttendanceFlag(String blobId) { LOGGER.debug(""" - No event attendance flag found for message {}. + No event attendance flag found for blob: {}. Defaulting to NeedsAction - """, messageId); - return Mono.just(AttendanceStatus.NeedsAction); + """, blobId); + return CalendarEventAttendanceResults$.MODULE$.done( + new EventAttendanceStatusEntry(blobId, AttendanceStatus.NeedsAction)); } @Override @@ -72,7 +99,7 @@ public Publisher setAttendanceStatus(Username usernam MailboxSession systemMailboxSession = sessionProvider.createSystemSession(username); return Flux.fromIterable(JavaConverters.seqAsJavaList(calendarEventBlobIds.value())) - .map(blobId -> extractMessageId(blobId.value())) + .flatMap(blobId -> extractMessageId(blobId.value())) .onErrorContinue((throwable, o) -> LOGGER.debug("Failed to extract message id from blob id: {}", o, throwable)) .collectList() .flatMap(messageIds -> @@ -85,8 +112,12 @@ public Publisher setAttendanceStatus(Username usernam .then(tryToSendReplyEmail(username, calendarEventBlobIds, maybePreferredLanguage, systemMailboxSession, attendanceStatus)); } - private MessageId extractMessageId(String blobId) { - return messageIdFactory.fromString(new MessagePartBlobId(blobId).getMessageId()); + private Mono extractMessageId(String blobId) { + return Mono.fromCallable(() -> + MessagePartBlobId.tryParse(blobId) + .map(MessagePartBlobId::getMessageId) + .map(messageIdFactory::fromString) + .get()); } private Mono tryToSendReplyEmail(Username username, @@ -138,9 +169,4 @@ private Flux updateEventAttendanceFlags(MessageResult message, AttendanceS session) ); } - - private Flux getFlags(MessageId messageId, MailboxSession session) { - return Flux.from(messageIdManager.getMessagesReactive(List.of(messageId), FetchGroup.MINIMAL, session)) - .map(MessageResult::getFlags); - } } diff --git a/tmail-backend/jmap/extensions/src/main/scala/com/linagora/tmail/james/jmap/json/CalendarEventAttendanceSerializer.scala b/tmail-backend/jmap/extensions/src/main/scala/com/linagora/tmail/james/jmap/json/CalendarEventAttendanceSerializer.scala new file mode 100644 index 0000000000..16e7450ee4 --- /dev/null +++ b/tmail-backend/jmap/extensions/src/main/scala/com/linagora/tmail/james/jmap/json/CalendarEventAttendanceSerializer.scala @@ -0,0 +1,26 @@ +package com.linagora.tmail.james.jmap.json + +import com.linagora.tmail.james.jmap.method.{CalendarEventAttendanceGetRequest, CalendarEventAttendanceGetResponse, CalendarEventAttendanceResults} +import com.linagora.tmail.james.jmap.model.{CalendarEventNotDone, CalendarEventNotFound} +import org.apache.james.jmap.mail.{BlobId, BlobIds} +import play.api.libs.json.{JsResult, JsValue, Json, OWrites, Reads, Writes} + +object CalendarEventAttendanceSerializer { + private implicit val blobIdReads: Reads[BlobId] = Json.valueReads[BlobId] + private implicit val blobIdsReads: Reads[BlobIds] = Json.valueReads[BlobIds] + private implicit val blobIdWrites: Writes[BlobId] = Json.valueWrites[BlobId] + + private implicit val calendarEventNotFoundWrites: Writes[CalendarEventNotFound] = Json.valueWrites[CalendarEventNotFound] + private implicit val calendarEventNotDoneWrites: Writes[CalendarEventNotDone] = Json.valueWrites[CalendarEventNotDone] + + private implicit val calendarEventAttendanceGetRequestReads: Reads[CalendarEventAttendanceGetRequest] = Json.reads[CalendarEventAttendanceGetRequest] + + private implicit val calendarEventAttendanceGetResponseWrites: OWrites[CalendarEventAttendanceGetResponse] = Json.writes[CalendarEventAttendanceGetResponse] + + def deserializeEventAttendanceGetRequest(input: JsValue): JsResult[CalendarEventAttendanceGetRequest] = + Json.fromJson[CalendarEventAttendanceGetRequest](input) + + def serializeEventAttendanceGetResponse(response: CalendarEventAttendanceGetResponse): JsValue = + Json.toJson(response) + +} diff --git a/tmail-backend/jmap/extensions/src/main/scala/com/linagora/tmail/james/jmap/method/CalendarEventAttendanceGetMethod.scala b/tmail-backend/jmap/extensions/src/main/scala/com/linagora/tmail/james/jmap/method/CalendarEventAttendanceGetMethod.scala new file mode 100644 index 0000000000..fdd6579b9d --- /dev/null +++ b/tmail-backend/jmap/extensions/src/main/scala/com/linagora/tmail/james/jmap/method/CalendarEventAttendanceGetMethod.scala @@ -0,0 +1,136 @@ +package com.linagora.tmail.james.jmap.method + +import com.linagora.tmail.james.jmap.json.CalendarEventAttendanceSerializer +import com.linagora.tmail.james.jmap.method.CalendarEventAttendanceGetRequest.MAXIMUM_NUMBER_OF_BLOB_IDS +import com.linagora.tmail.james.jmap.method.CapabilityIdentifier.LINAGORA_CALENDAR +import com.linagora.tmail.james.jmap.model.{CalendarEventNotDone, CalendarEventNotFound, CalendarEventNotParsable, InvalidCalendarFileException} +import com.linagora.tmail.james.jmap.{AttendanceStatus, EventAttendanceRepository} +import eu.timepit.refined.auto._ +import eu.timepit.refined.refineV +import jakarta.inject.Inject +import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE} +import org.apache.james.jmap.core.Id.IdConstraint +import org.apache.james.jmap.core.Invocation.{Arguments, MethodName} +import org.apache.james.jmap.core.SetError.SetErrorDescription +import org.apache.james.jmap.core.{AccountId, Invocation, SessionTranslator, SetError} +import org.apache.james.jmap.json.ResponseSerializer +import org.apache.james.jmap.mail.{BlobId, BlobIds, RequestTooLargeException} +import org.apache.james.jmap.method.{InvocationWithContext, MethodRequiringAccountId, WithAccountId} +import org.apache.james.jmap.routes.SessionSupplier +import org.apache.james.mailbox.MailboxSession +import org.apache.james.metrics.api.MetricFactory +import org.reactivestreams.Publisher +import play.api.libs.json.JsObject +import reactor.core.scala.publisher.SMono + +class CalendarEventAttendanceGetMethod @Inject()(val eventAttendanceRepository: EventAttendanceRepository, + val metricFactory: MetricFactory, + val sessionSupplier: SessionSupplier, + val sessionTranslator: SessionTranslator) extends MethodRequiringAccountId[CalendarEventAttendanceGetRequest] { + override val methodName: Invocation.MethodName = MethodName("CalendarEventAttendance/get") + + override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE, LINAGORA_CALENDAR) + + override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: CalendarEventAttendanceGetRequest): Publisher[InvocationWithContext] = + SMono.fromDirect(eventAttendanceRepository.getAttendanceStatus(mailboxSession.getUser, request.blobIds)) + .map(result => CalendarEventAttendanceGetResponse.from(request.accountId, result)) + .map(response => Invocation( + methodName, + Arguments(CalendarEventAttendanceSerializer.serializeEventAttendanceGetResponse(response).as[JsObject]), + invocation.invocation.methodCallId)) + .map(InvocationWithContext(_, invocation.processingContext)) + + override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[Exception, CalendarEventAttendanceGetRequest] = + CalendarEventAttendanceSerializer.deserializeEventAttendanceGetRequest(invocation.arguments.value) + .asEither.left.map(ResponseSerializer.asException) + .flatMap(_.validate()) +} + +object CalendarEventAttendanceGetRequest { + val MAXIMUM_NUMBER_OF_BLOB_IDS: Int = 16 +} + +case class CalendarEventAttendanceGetRequest(accountId: AccountId, + blobIds: BlobIds) extends WithAccountId { + def validate(): Either[Exception, CalendarEventAttendanceGetRequest] = + validateBlobIdsSize + + private def validateBlobIdsSize: Either[RequestTooLargeException, CalendarEventAttendanceGetRequest] = + if (blobIds.value.length > MAXIMUM_NUMBER_OF_BLOB_IDS) { + Left(RequestTooLargeException("The number of ids requested by the client exceeds the maximum number the server is willing to process in a single method call")) + } else { + scala.Right(this) + } +} + +object CalendarEventAttendanceGetResponse { + def from(accountId: AccountId, results: CalendarEventAttendanceResults): CalendarEventAttendanceGetResponse = { + val (accepted, rejected, tentativelyAccepted, needsAction) = + results.done.foldLeft((List.empty[BlobId], List.empty[BlobId], List.empty[BlobId], List.empty[BlobId])) { + case ((accepted, rejected, tentativelyAccepted, needsAction), entry) => + val refinedBlobId = refineV[IdConstraint](entry.blobId).fold( + _ => BlobId("invalid"), // Fallback for invalid values + validBlobId => BlobId(validBlobId) + ) + entry.eventAttendanceStatus match { + case AttendanceStatus.Accepted => (refinedBlobId :: accepted, rejected, tentativelyAccepted, needsAction) + case AttendanceStatus.Declined => (accepted, refinedBlobId :: rejected, tentativelyAccepted, needsAction) + case AttendanceStatus.Tentative => (accepted, rejected, refinedBlobId :: tentativelyAccepted, needsAction) + case AttendanceStatus.NeedsAction => (accepted, rejected, tentativelyAccepted, refinedBlobId :: needsAction) + } + } + + CalendarEventAttendanceGetResponse( + accountId, + accepted, + rejected, + tentativelyAccepted, + needsAction, + results.notFound, + results.notDone) + } +} + +case class CalendarEventAttendanceGetResponse(accountId: AccountId, + accepted: List[BlobId] = List(), + rejected: List[BlobId] = List(), + tentativelyAccepted: List[BlobId] = List(), + needsAction: List[BlobId] = List(), + notFound: Option[CalendarEventNotFound] = Option.empty, + notDone: Option[CalendarEventNotDone] = Option.empty) extends WithAccountId + +object CalendarEventAttendanceResults { + def merge(r1: CalendarEventAttendanceResults, r2: CalendarEventAttendanceResults): CalendarEventAttendanceResults = + CalendarEventAttendanceResults( + done = r1.done ++ r2.done, + notFound = r1.notFound.orElse(r2.notFound), + notDone = r1.notDone.orElse(r2.notDone)) + + def notFound(blobId: BlobId): CalendarEventAttendanceResults = CalendarEventAttendanceResults(notFound = Some(CalendarEventNotFound(Set(blobId.value)))) + + def empty: CalendarEventAttendanceResults = CalendarEventAttendanceResults() + + def done(eventAttendanceEntry: EventAttendanceStatusEntry): CalendarEventAttendanceResults = + CalendarEventAttendanceResults(List(eventAttendanceEntry)) + + def notDone(notParsable: CalendarEventNotParsable): CalendarEventAttendanceResults = + CalendarEventAttendanceResults(notDone = Some(CalendarEventNotDone(notParsable.asSetErrorMap))) + + def notDone(blobId: BlobId, throwable: Throwable, mailboxSession: MailboxSession): CalendarEventAttendanceResults = + CalendarEventAttendanceResults(notDone = Some(CalendarEventNotDone(Map(blobId.value -> asSetError(throwable, mailboxSession)))), done = List(), notFound = None) + + private def asSetError(throwable: Throwable, mailboxSession: MailboxSession): SetError = throwable match { + case _: InvalidCalendarFileException | _: IllegalArgumentException => + // LOGGER.info("Error when generate reply mail for {}: {}", mailboxSession.getUser.asString(), throwable.getMessage) + SetError.invalidPatch(SetErrorDescription(throwable.getMessage)) + case _ => + // LOGGER.error("serverFail to generate reply mail for {}", mailboxSession.getUser.asString(), throwable) + SetError.serverFail(SetErrorDescription(throwable.getMessage)) + } +} + +case class CalendarEventAttendanceResults(done: List[EventAttendanceStatusEntry] = List(), + notFound: Option[CalendarEventNotFound] = Option.empty, + notDone: Option[CalendarEventNotDone] = Option.empty) + +case class EventAttendanceStatusEntry(blobId: String, eventAttendanceStatus: AttendanceStatus) diff --git a/tmail-backend/jmap/extensions/src/main/scala/com/linagora/tmail/james/jmap/method/CalendarEventParseMethod.scala b/tmail-backend/jmap/extensions/src/main/scala/com/linagora/tmail/james/jmap/method/CalendarEventParseMethod.scala index 6e8ffdb89b..fda686d3a4 100644 --- a/tmail-backend/jmap/extensions/src/main/scala/com/linagora/tmail/james/jmap/method/CalendarEventParseMethod.scala +++ b/tmail-backend/jmap/extensions/src/main/scala/com/linagora/tmail/james/jmap/method/CalendarEventParseMethod.scala @@ -58,6 +58,9 @@ class CalendarEventMethodModule extends AbstractModule { Multibinder.newSetBinder(binder(), classOf[Method]) .addBinding() .to(classOf[CalendarEventMaybeMethod]) + Multibinder.newSetBinder(binder(), classOf[Method]) + .addBinding() + .to(classOf[CalendarEventAttendanceGetMethod]) bind(classOf[CalendarEventReplyPerformer]).in(Scopes.SINGLETON) bind(classOf[CalendarEventReplySupportedLanguage]).in(Scopes.SINGLETON) diff --git a/tmail-backend/jmap/extensions/src/test/scala/com/linagora/tmail/james/jmap/StandaloneEventAttendanceRepositoryTest.scala b/tmail-backend/jmap/extensions/src/test/scala/com/linagora/tmail/james/jmap/StandaloneEventAttendanceRepositoryTest.scala index 7cfbc0c704..f2c38b0503 100644 --- a/tmail-backend/jmap/extensions/src/test/scala/com/linagora/tmail/james/jmap/StandaloneEventAttendanceRepositoryTest.scala +++ b/tmail-backend/jmap/extensions/src/test/scala/com/linagora/tmail/james/jmap/StandaloneEventAttendanceRepositoryTest.scala @@ -3,7 +3,7 @@ package com.linagora.tmail.james.jmap import java.util import java.util.Optional -import com.linagora.tmail.james.jmap.method.CalendarEventReplyPerformer +import com.linagora.tmail.james.jmap.method.{CalendarEventAttendanceResults, CalendarEventReplyPerformer, EventAttendanceStatusEntry} import com.linagora.tmail.james.jmap.model.CalendarEventReplyRequest import jakarta.mail.Flags import net.fortuna.ical4j.model.parameter.PartStat @@ -51,22 +51,30 @@ class StandaloneEventAttendanceRepositoryTest { def givenAcceptedFlagIsLinkedToMailGetAttendanceStatusShouldReturnAccepted(): Unit = { val flags: Flags = new Flags("$accepted") val messageId: MessageId = createMessage(flags) - assertThat(Mono.from(testee.getAttendanceStatus(mailbox.getUser, messageId)).block) - .isEqualTo(AttendanceStatus.Accepted) + val blobId = createFakeCalendaerEventBlobId(messageId) + val blobIds = BlobIds(Seq(blobId.value)) + assertThat(Mono.from(testee.getAttendanceStatus(mailbox.getUser, blobIds)).block()) + .isEqualTo(done(blobId, AttendanceStatus.Accepted)) } @Test def givenRejectedFlagIsLinkedToMailGetAttendanceStatusShouldReturnDeclined(): Unit = { val flags: Flags = new Flags("$rejected") val messageId: MessageId = createMessage(flags) - assertThat(Mono.from(testee.getAttendanceStatus(mailbox.getUser, messageId)).block).isEqualTo(AttendanceStatus.Declined) + val blobId = createFakeCalendaerEventBlobId(messageId) + val blobIds = BlobIds(Seq(blobId.value)) + assertThat(Mono.from(testee.getAttendanceStatus(mailbox.getUser, blobIds)).block()) + .isEqualTo(done(blobId, AttendanceStatus.Declined)) } @Test def givenTentativelyAcceptedFlagIsLinkedToMailGetAttendanceStatusShouldReturnTentative(): Unit = { val flags: Flags = new Flags("$tentativelyaccepted") val messageId: MessageId = createMessage(flags) - assertThat(Mono.from(testee.getAttendanceStatus(mailbox.getUser, messageId)).block).isEqualTo(AttendanceStatus.Tentative) + val blobId = createFakeCalendaerEventBlobId(messageId) + val blobIds = BlobIds(Seq(blobId.value)) + assertThat(Mono.from(testee.getAttendanceStatus(mailbox.getUser, blobIds)).block()) + .isEqualTo(done(blobId, AttendanceStatus.Tentative)) } // It should also print a warning message @@ -75,24 +83,22 @@ class StandaloneEventAttendanceRepositoryTest { val flags: Flags = new Flags("$rejected") flags.add("$accepted") val messageId: MessageId = createMessage(flags) + val blobId = createFakeCalendaerEventBlobId(messageId) + val blobIds = BlobIds(Seq(blobId.value)) - - assertThat(util.List.of(AttendanceStatus.Accepted, AttendanceStatus.Declined)) - .contains(Mono.from(testee.getAttendanceStatus(mailbox.getUser, messageId)).block) + assertThat(util.List.of((blobId, AttendanceStatus.Accepted), done(blobId, AttendanceStatus.Declined)) + .contains(Mono.from(testee.getAttendanceStatus(mailbox.getUser, blobIds)).block())) } @Test def getAttendanceStatusShouldFallbackToNeedsActionWhenNoFlagIsLinkedToMail(): Unit = { val flags: Flags = new Flags val messageId: MessageId = createMessage(flags) - assertThat(Mono.from(testee.getAttendanceStatus(mailbox.getUser, messageId)).block).isEqualTo(AttendanceStatus.NeedsAction) - } + val blobId = createFakeCalendaerEventBlobId(messageId) + val blobIds = BlobIds(Seq(blobId.value)) - @Test - def getAttendanceStatusShouldFallbackToNeedsActionWhenNoEventAttendanceFlagIsLinkedToMail(): Unit = { - val flags: Flags = new Flags(Flags.Flag.RECENT) - val messageId: MessageId = createMessage(flags) - assertThat(Mono.from(testee.getAttendanceStatus(mailbox.getUser, messageId)).block).isEqualTo(AttendanceStatus.NeedsAction) + assertThat(Mono.from(testee.getAttendanceStatus(mailbox.getUser, blobIds)).block()) + .isEqualTo(done(blobId, AttendanceStatus.NeedsAction)) } @Test @@ -199,4 +205,10 @@ class StandaloneEventAttendanceRepositoryTest { .build() new MessageIdManagerTestSystem(resources.getMessageIdManager, messageIdFactory, resources.getMailboxManager.getMapperFactory, resources.getMailboxManager) } + + private def done(blobId: BlobId, attendanceStatus: AttendanceStatus) = + CalendarEventAttendanceResults.done( + EventAttendanceStatusEntry( + blobId.value.value, + attendanceStatus)) }