Skip to content

Commit

Permalink
ISSUE-183 JMAP scenario for triaging downloads (#184)
Browse files Browse the repository at this point in the history
  • Loading branch information
vttranlina authored Jan 21, 2025
1 parent 312771a commit 2405fe0
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 6 deletions.
5 changes: 5 additions & 0 deletions sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@
# Integer value. Unit: Minutes. Force your run to terminate based on a duration limit. even if some virtual users are still running.
# Default is 180 minutes
MAX_DURATION=120

# Boolean value. It provided the configure for DownloadAttachmentScenario.
# Default is false
# When it is true, it will create new emails with attachments before downloading them.
#PROVISION_MAIL_WITH_ATTACHMENTS=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.apache.james.gatling.jmap.rfc8621

import org.apache.james.gatling.Fixture
import org.apache.james.gatling.jmap.rfc8621.scenari.DownloadAttachmentScenario

import scala.concurrent.duration._

class DownloadAttachmentIT extends JmapIT {
before {
users.foreach(server.sendMessage(Fixture.homer.username))
}

scenario((userFeederBuilder, recipientFeederBuilder) => {
new DownloadAttachmentScenario().generate(20 seconds, userFeederBuilder, recipientFeederBuilder,
provisionMailWithAttachments = Some(true))
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ object JmapEmail {
exec(queryEmails(queryParameters)
.check(JmapHttp.statusOk, JmapHttp.noError))

def queryEmails(queryParameters: JmapParameters = NO_PARAMETERS): HttpRequestBuilder = {
JmapHttp.apiCall("emailQuery")
def queryEmails(callName: String = "emailQuery", queryParameters: JmapParameters = NO_PARAMETERS): HttpRequestBuilder = {
JmapHttp.apiCall(callName)
.body(StringBody(
s"""{
| "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
Expand Down Expand Up @@ -60,6 +60,20 @@ object JmapEmail {
|""".stripMargin
}

def filterAttachments(limit: Int = 20): JmapParameters = {
s""",
|"filter": {
| "hasAttachment": true
|},
|"sort": [{
| "property": "receivedAt",
| "isAscending": false
|}],
|"position": 0,
|"limit": $limit
|""".stripMargin
}

def openpaasEmailQueryParameters(mailboxKey: String = "inboxID"): JmapParameters = {
s""",
|"filter": {
Expand Down Expand Up @@ -224,26 +238,38 @@ object JmapEmail {
|}""".stripMargin))
}

def submitEmails(recipientFeeder: RecipientFeederBuilder): ChainBuilder = {
def submitEmails(recipientFeeder: RecipientFeederBuilder, attachmentId: Option[String] = None): ChainBuilder = {
val mailFeeder = Iterator.continually(
Map(messageIdSessionParam -> MessageId().id,
subjectSessionParam -> Subject().subject,
textBodySessionParam -> TextBody().text))

feed(mailFeeder)
.feed(recipientFeeder)
.exec(submitEmail()
.exec(submitEmail(attachmentsJsonPart = attachmentId.map(JmapEmail.attachmentsJsonPart).getOrElse(""))
.check(JmapHttp.statusOk, JmapHttp.noError, JmapEmail.emailCreatedChecks(), JmapEmail.emailSubmittedChecks()))
}

def attachmentsJsonPart(attachmentId: String): String = {
s""" , "attachments": [
| {
| "blobId": "$attachmentId",
| "type":"text/plain",
| "charset":"UTF-8",
| "disposition": "attachment"
| }
| ]""".stripMargin
}

def submitEmail(title: RequestTitle = RequestTitle("submitEmails"),
accountId: String = "accountId",
username: String = "username",
mailboxId: String = "draftMailboxId",
messageId: String = "messageId",
recipient: String = "recipient",
subject: String = "subject",
textBody: String = "textBody"): HttpRequestBuilder =
textBody: String = "textBody",
attachmentsJsonPart: String = ""): HttpRequestBuilder =
JmapHttp.apiCall(title.title)
.body(StringBody(
s"""{
Expand All @@ -270,6 +296,7 @@ object JmapEmail {
| "value": "#{$textBody}"
| }
| }
| $attachmentsJsonPart
| }
| }
| }, "c1"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import io.gatling.core.check.jsonpath.{JsonPathCheckType, JsonPathOfType}
import io.gatling.http.HeaderNames
import io.gatling.http.Predef.{http, status}
import io.gatling.http.request.builder.HttpRequestBuilder
import org.apache.james.gatling.utils.RandomStringGenerator


object JmapHttp {
val CONTENT_TYPE_JSON_KEY: String = HeaderNames.ContentType.toString
val CONTENT_TYPE_JSON_VALUE: String = "application/json; charset=UTF-8"
val CONTENT_TYPE_TEXT_PLAIN: String = "text/plain"

val ACCEPT_JSON_KEY: String = HeaderNames.Accept.toString
val ACCEPT_JSON_VALUE: String = "application/json; jmapVersion=rfc-8621"
Expand All @@ -23,6 +25,18 @@ object JmapHttp {
.headers(JmapHttp.HEADERS_JSON)
.basicAuth("#{username}", "#{password}")

def download(callName: String = "Download", accountId: String = "#{accountId}", blobId: String = "#{blobId}"): HttpRequestBuilder =
http(callName)
.get(s"/download/$accountId/$blobId")
.headers(JmapHttp.HEADERS_JSON)
.basicAuth("#{username}", "#{password}")

def upload(callName: String = "Upload", accountId: String = "#{accountId}", body: String = RandomStringGenerator.randomAlphaString(1000)): HttpRequestBuilder =
http(callName)
.post(s"/upload/$accountId")
.body(StringBody(body))
.headers(Map(CONTENT_TYPE_JSON_KEY -> CONTENT_TYPE_TEXT_PLAIN, ACCEPT_JSON_KEY -> ACCEPT_JSON_VALUE))
.basicAuth("#{username}", "#{password}")

private val hasErrorPath: MultipleFind[JsonPathCheckType, JsonNode, String] with JsonPathOfType = jsonPath("$[?(@[0] == 'error')]")
val noError = hasErrorPath.notExists
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.apache.james.gatling.jmap.rfc8621

import fabricator.Words
import io.gatling.core.Predef._
import io.gatling.core.Predef.{StringBody, _}
import io.gatling.core.structure.ChainBuilder
import io.gatling.http.Predef._
import io.gatling.http.check.HttpCheck
Expand Down Expand Up @@ -99,6 +99,23 @@ object JmapMailbox {
}
.pause(5 second)

def provisionUsersWithMessagesAndAttachment(recipientFeeder: RecipientFeederBuilder, numberOfMessages: Int): ChainBuilder = {
val attachmentBodyLength = RandomStringGenerator.faker.random().nextInt(900, 11000)

def randomAttachmentBody = RandomStringGenerator.randomAlphaString(attachmentBodyLength)

exec(provisionSystemMailboxes())
.repeat(numberOfMessages, loopVariableName) {
exec(JmapHttp.upload(body = randomAttachmentBody)
.check(status.is(201), JmapHttp.noError)
.check(jsonPath("$.blobId").saveAs("uploadId")))
.pause(1 second, 2 seconds)
.exec(JmapEmail.submitEmails(recipientFeeder, attachmentId = Some("#{uploadId}")))
.pause(1 second, 2 seconds)
}
.pause(5 second)
}

def saveStateAs(key: String): HttpCheck = jsonPath(statePath).saveAs(key)

def saveNewStateAs(key: String): HttpCheck = jsonPath(newStatePath).saveAs(key)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.apache.james.gatling.jmap.rfc8621.scenari

import io.gatling.core.Predef._
import io.gatling.core.structure.ScenarioBuilder
import io.gatling.http.Predef._
import io.gatling.http.check.HttpCheck
import org.apache.james.gatling.control.RecipientFeeder.RecipientFeederBuilder
import org.apache.james.gatling.control.UserFeeder.UserFeederBuilder
import org.apache.james.gatling.jmap.rfc8621.scenari.DownloadAttachmentScenario.{emailGetAttachmentsProperties, emailWithAttachmentsIdsKey}
import org.apache.james.gatling.jmap.rfc8621.{JmapEmail, JmapHttp, JmapMailbox, SessionStep}

import scala.concurrent.duration._
import scala.util.Properties

object DownloadAttachmentScenario {
val emailGetAttachmentsProperties = "[\"attachments\"]"
val emailWithAttachmentsIdsKey = "emailWithAttachmentsIds"
}

class DownloadAttachmentScenario {

def generate(duration: Duration, userFeeder: UserFeederBuilder, recipientFeeder: RecipientFeederBuilder,
provisionMailWithAttachments: Option[Boolean] = None): ScenarioBuilder = {

val provisionMailWithAttachmentsValue: Boolean = provisionMailWithAttachments.getOrElse(Properties.envOrNone("PROVISION_MAIL_WITH_ATTACHMENTS") match {
case Some(value) => value.toBoolean
case _ => false
})

scenario("DownloadAttachmentScenario")
.feed(userFeeder)
.exec(SessionStep.retrieveAccountId)
.doIf(_ => provisionMailWithAttachmentsValue) {
exec(JmapMailbox.provisionUsersWithMessagesAndAttachment(recipientFeeder, numberOfMessages = 10))
}
.during(duration.toSeconds.toInt) {
exec(JmapEmail.queryEmails(callName = "emailQuery hasAttachment", JmapEmail.filterAttachments())
.check(JmapHttp.statusOk, JmapHttp.noError, nonEmptyListMessageIdsChecks()))
.doIf(session => !session(emailWithAttachmentsIdsKey).asOption[Seq[String]].forall(_.isEmpty)) {
pause(1 second, 2 seconds)
.exec(JmapEmail.getRandomEmails(properties = emailGetAttachmentsProperties, emailIdsKey = emailWithAttachmentsIdsKey)
.check(JmapHttp.statusOk, JmapHttp.noError, JmapEmail.nonEmptyEmailsChecks, nonEmptyBlobIdChecks()))
.pause(1 second, 2 seconds)
.exec(JmapHttp.download(callName = "Download Attachment")
.check(JmapHttp.statusOk))
}
}
}

private def nonEmptyListMessageIdsChecks(): HttpCheck =
jsonPath("$.methodResponses[0][1].ids[*]").findAll.optional.saveAs(emailWithAttachmentsIdsKey)

private def nonEmptyBlobIdChecks(): HttpCheck =
jsonPath("$.methodResponses[0][1].list[0].attachments[0].blobId").find.saveAs("blobId")
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.apache.james.gatling.simulation.jmap.rfc8621

import io.gatling.core.Predef._
import io.gatling.core.scenario.Simulation
import org.apache.james.gatling.jmap.rfc8621.scenari.DownloadAttachmentScenario
import org.apache.james.gatling.simulation.Configuration.UserCount
import org.apache.james.gatling.simulation.{Configuration, HttpSettings, UsersFeederWebAdminFactory}

class DownloadAttachmentSimulation extends Simulation {
private val scenario: DownloadAttachmentScenario = new DownloadAttachmentScenario()
private val feederFactory: UsersFeederWebAdminFactory = new UsersFeederWebAdminFactory(UserCount).initUsers

setUp(scenario.generate(Configuration.ScenarioDuration, feederFactory.userFeeder(), feederFactory.recipientFeeder())
.inject(atOnceUsers(Configuration.UserCount)))
.protocols(HttpSettings.httpProtocol)
}

0 comments on commit 2405fe0

Please sign in to comment.