Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 43 additions & 8 deletions core/src/main/scala/org/elasticmq/Limits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ trait Limits {
def queueNameLengthLimit: Limit[Int]
def batchSizeLimit: Limit[Int]
def numberOfMessagesLimit: Limit[RangeLimit[Int]]
def nonEmptyAttributesLimit: Limit[Boolean]
def bodyValidCharactersLimit: Limit[List[RangeLimit[Int]]]
def numberAttributeValueLimit: Limit[RangeLimit[BigDecimal]]
def messageWaitTimeLimit: Limit[RangeLimit[Long]]
Expand All @@ -17,6 +18,7 @@ case object StrictSQSLimits extends Limits {
override val queueNameLengthLimit: Limit[Int] = LimitedValue(80)
override val batchSizeLimit: Limit[Int] = LimitedValue(10)
override val numberOfMessagesLimit: Limit[RangeLimit[Int]] = LimitedValue(RangeLimit(1, 10))
override val nonEmptyAttributesLimit: Limit[Boolean] = LimitedValue(true)
override val bodyValidCharactersLimit: Limit[List[RangeLimit[Int]]] = LimitedValue(
List(
RangeLimit(0x9, 0x9),
Expand All @@ -37,6 +39,7 @@ case object RelaxedSQSLimits extends Limits {
override val queueNameLengthLimit: Limit[Int] = NoLimit
override val batchSizeLimit: Limit[Int] = NoLimit
override val numberOfMessagesLimit: Limit[RangeLimit[Int]] = NoLimit
override val nonEmptyAttributesLimit: Limit[Boolean] = NoLimit
override val bodyValidCharactersLimit: Limit[List[RangeLimit[Int]]] = NoLimit
override val numberAttributeValueLimit: Limit[RangeLimit[BigDecimal]] = NoLimit
override val messageWaitTimeLimit: Limit[RangeLimit[Long]] = NoLimit
Expand Down Expand Up @@ -68,25 +71,57 @@ object Limits {
"ReadCountOutOfRange"
)

def verifyMessageStringAttribute(stringAttribute: String, limits: Limits): Either[String, Unit] =
def verifyMessageStringAttribute(
attributeName: String,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't it be name-value, as that's the usual order? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I have changed it to name-value order :)

attributeValue: String,
limits: Limits
): Either[String, Unit] =
for {
_ <- validateWhenLimitAvailable(limits.nonEmptyAttributesLimit)(
shouldValidate => if (shouldValidate) attributeValue.nonEmpty else true,
s"Attribute '$attributeName' must contain a non-empty value of type 'String'"
)
_ <- validateCharactersAndLength(attributeValue, limits)
} yield ()

def verifyMessageBody(body: String, limits: Limits): Either[String, Unit] =
for {
_ <- validateWhenLimitAvailable(limits.nonEmptyAttributesLimit)(
shouldValidate => if (shouldValidate) body.nonEmpty else true,
"The request must contain the parameter MessageBody."
)
_ <- validateCharactersAndLength(body, limits)
} yield ()

private def validateCharactersAndLength(stringValue: String, limits: Limits): Either[String, Unit] =
for {
_ <- validateWhenLimitAvailable(limits.bodyValidCharactersLimit)(
rangeLimits =>
stringAttribute
stringValue
.codePoints()
.iterator
.asScala
.forall(codePoint => rangeLimits.exists(range => range.isBetween(codePoint))),
"InvalidMessageContents"
)
_ <- verifyMessageLength(stringAttribute.length, limits)
_ <- verifyMessageLength(stringValue.length, limits)
} yield ()

def verifyMessageNumberAttribute(stringNumberValue: String, limits: Limits): Either[String, Unit] = {
validateWhenLimitAvailable(limits.numberAttributeValueLimit)(
limit => Try(BigDecimal(stringNumberValue)).toOption.exists(value => limit.isBetween(value)),
s"Number attribute value $stringNumberValue should be in range (-10**128..10**126)"
)
def verifyMessageNumberAttribute(
stringNumberValue: String,
attributeName: String,
limits: Limits
): Either[String, Unit] = {
for {
_ <- validateWhenLimitAvailable(limits.nonEmptyAttributesLimit)(
shouldValidate => if (shouldValidate) stringNumberValue.nonEmpty else true,
s"Attribute '$attributeName' must contain a non-empty value of type 'Number'"
)
_ <- validateWhenLimitAvailable(limits.numberAttributeValueLimit)(
limit => Try(BigDecimal(stringNumberValue)).toOption.exists(value => limit.isBetween(value)),
s"Number attribute value $stringNumberValue should be in range (-10**128..10**126)"
)
} yield ()
}

def verifyMessageWaitTime(messageWaitTime: Long, limits: Limits): Either[String, Unit] = {
Expand Down
96 changes: 78 additions & 18 deletions core/src/test/scala/org/elasticmq/LimitsTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,64 +59,81 @@ class LimitsTest extends AnyWordSpec with Matchers with EitherValues {
"Validation of message string attribute in strict mode" should {
"pass if string attribute contains only allowed characters" in {
val testString = List(0x9, 0xa, 0xd, 0x21, 0xe005, 0x10efff).map(_.toChar).mkString
Limits.verifyMessageStringAttribute(testString, StrictSQSLimits) shouldBe Right(())
Limits.verifyMessageStringAttribute("attribute1", testString, StrictSQSLimits) shouldBe Right(())
}

"pass if the string is empty" in {
Limits.verifyMessageStringAttribute("", StrictSQSLimits) shouldBe Right(())
"fail if the string is empty" in {
Limits.verifyMessageStringAttribute("attribute1", "", StrictSQSLimits) shouldBe Left(
"Attribute 'attribute1' must contain a non-empty value of type 'String'"
)
}

"fail if string contains any not allowed character" in {
val testString = List(0x9, 0xa, 0xd, 0x21, 0xe005, 0x19, 0x10efff).map(_.toChar).mkString
val error = Limits.verifyMessageStringAttribute(testString, StrictSQSLimits).left.value
val error = Limits.verifyMessageStringAttribute("attribute1", testString, StrictSQSLimits).left.value
error shouldBe "InvalidMessageContents"
}
}

"Validation of message string attribute in relaxed mode" should {
"pass if string attribute contains only allowed characters" in {
val testString = List(0x9, 0xa, 0xd, 0x21, 0xe005, 0x10efff).map(_.toChar).mkString
Limits.verifyMessageStringAttribute(testString, RelaxedSQSLimits) shouldBe Right(())
Limits.verifyMessageStringAttribute("attribute1", testString, RelaxedSQSLimits) shouldBe Right(())
}

"pass if the string is empty" in {
Limits.verifyMessageStringAttribute("", RelaxedSQSLimits) shouldBe Right(())
Limits.verifyMessageStringAttribute("attribute1", "", RelaxedSQSLimits) shouldBe Right(())
}

"pass if string contains any not allowed character" in {
val testString = List(0x9, 0xa, 0xd, 0x21, 0xe005, 0x19, 0x10efff).map(_.toChar).mkString
Limits.verifyMessageStringAttribute(testString, RelaxedSQSLimits) shouldBe Right(())
Limits.verifyMessageStringAttribute("attribute1", testString, RelaxedSQSLimits) shouldBe Right(())
}
}

"Validation of message number attribute in strict mode" should {
"pass if the number is between the limits (-10^128 - 10^126)" in {
Limits.verifyMessageNumberAttribute(BigDecimal(10).pow(126).toString(), StrictSQSLimits) shouldBe Right(())
Limits.verifyMessageNumberAttribute((-BigDecimal(10).pow(128)).toString(), StrictSQSLimits) shouldBe Right(())
Limits.verifyMessageNumberAttribute(BigDecimal(0).toString(), StrictSQSLimits) shouldBe Right(())
Limits.verifyMessageNumberAttribute(
BigDecimal(10).pow(126).toString(),
"numAttribute",
StrictSQSLimits
) shouldBe Right(())
Limits.verifyMessageNumberAttribute(
(-BigDecimal(10).pow(128)).toString(),
"numAttribute",
StrictSQSLimits
) shouldBe Right(())
Limits.verifyMessageNumberAttribute(BigDecimal(0).toString(), "numAttribute", StrictSQSLimits) shouldBe Right(())
Limits.verifyMessageNumberAttribute(
BigDecimal(Random.nextDouble()).toString(),
"numAttribute",
StrictSQSLimits
) shouldBe Right(
()
)
}

"fail if the number is an empty string" in {
val emptyStringNumber = ""
val error = Limits.verifyMessageNumberAttribute(emptyStringNumber, "numAttribute", StrictSQSLimits)
error shouldBe Left("Attribute 'numAttribute' must contain a non-empty value of type 'Number'")
}

"fail if the number is bigger than the upper bound" in {
val overUpperBound = BigDecimal(10, MathContext.UNLIMITED).pow(126) + BigDecimal(0.1)
val error = Limits.verifyMessageNumberAttribute(overUpperBound.toString, StrictSQSLimits)
val error = Limits.verifyMessageNumberAttribute(overUpperBound.toString, "numAttribute", StrictSQSLimits)
error shouldBe Left(s"Number attribute value $overUpperBound should be in range (-10**128..10**126)")
}

"fail if the number is below the lower bound" in {
val belowLowerBound = -BigDecimal(10, MathContext.UNLIMITED).pow(128) - BigDecimal(0.1)
val error =
Limits.verifyMessageNumberAttribute(belowLowerBound.toString, StrictSQSLimits)
Limits.verifyMessageNumberAttribute(belowLowerBound.toString, "numAttribute", StrictSQSLimits)
error shouldBe Left(s"Number attribute value $belowLowerBound should be in range (-10**128..10**126)")
}

"fail if the number can't be parsed" in {
val error = Limits.verifyMessageNumberAttribute("12312312a", StrictSQSLimits).left.value
val error = Limits.verifyMessageNumberAttribute("12312312a", "numAttribute", StrictSQSLimits).left.value
error shouldBe s"Number attribute value 12312312a should be in range (-10**128..10**126)"
}
}
Expand All @@ -125,11 +142,19 @@ class LimitsTest extends AnyWordSpec with Matchers with EitherValues {
"always pass the validation" in {
val belowLowerBound = -BigDecimal(10).pow(128) - BigDecimal(0.1)
val overUpperBound = BigDecimal(10).pow(126) + BigDecimal(0.1)
Limits.verifyMessageNumberAttribute(belowLowerBound.toString, RelaxedSQSLimits) shouldBe Right(())
Limits.verifyMessageNumberAttribute(BigDecimal(10).pow(126).toString(), RelaxedSQSLimits) shouldBe Right(())
Limits.verifyMessageNumberAttribute((-BigDecimal(10).pow(128)).toString(), RelaxedSQSLimits) shouldBe Right(())
Limits.verifyMessageNumberAttribute(overUpperBound.toString, RelaxedSQSLimits) shouldBe Right(())
Limits.verifyMessageNumberAttribute("12312312a", RelaxedSQSLimits) shouldBe Right(())
Limits.verifyMessageNumberAttribute(belowLowerBound.toString, "numAttribute", RelaxedSQSLimits) shouldBe Right(())
Limits.verifyMessageNumberAttribute(
BigDecimal(10).pow(126).toString(),
"numAttribute",
RelaxedSQSLimits
) shouldBe Right(())
Limits.verifyMessageNumberAttribute(
(-BigDecimal(10).pow(128)).toString(),
"numAttribute",
RelaxedSQSLimits
) shouldBe Right(())
Limits.verifyMessageNumberAttribute(overUpperBound.toString, "numAttribute", RelaxedSQSLimits) shouldBe Right(())
Limits.verifyMessageNumberAttribute("12312312a", "numAttribute", RelaxedSQSLimits) shouldBe Right(())
}
}

Expand Down Expand Up @@ -232,4 +257,39 @@ class LimitsTest extends AnyWordSpec with Matchers with EitherValues {
error shouldBe "InvalidParameterValue"
}
}

"Validation of message body in strict mode" should {
"pass if string attribute contains only allowed characters" in {
val testString = List(0x9, 0xa, 0xd, 0x21, 0xe005, 0x10efff).map(_.toChar).mkString
Limits.verifyMessageBody(testString, StrictSQSLimits) shouldBe Right(())
}

"fail if the string is empty" in {
Limits.verifyMessageBody("", StrictSQSLimits) shouldBe Left(
"The request must contain the parameter MessageBody."
)
}

"fail if string contains any not allowed character" in {
val testString = List(0x9, 0xa, 0xd, 0x21, 0xe005, 0x19, 0x10efff).map(_.toChar).mkString
val error = Limits.verifyMessageBody(testString, StrictSQSLimits).left.value
error shouldBe "InvalidMessageContents"
}
}

"Validation of message body in relaxed mode" should {
"pass if string attribute contains only allowed characters" in {
val testString = List(0x9, 0xa, 0xd, 0x21, 0xe005, 0x10efff).map(_.toChar).mkString
Limits.verifyMessageBody(testString, RelaxedSQSLimits) shouldBe Right(())
}

"pass if the string is empty" in {
Limits.verifyMessageBody("", RelaxedSQSLimits) shouldBe Right(())
}

"pass if string contains any not allowed character" in {
val testString = List(0x9, 0xa, 0xd, 0x21, 0xe005, 0x19, 0x10efff).map(_.toChar).mkString
Limits.verifyMessageBody(testString, RelaxedSQSLimits) shouldBe Right(())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,14 @@ class AmazonJavaSdkTestSuite extends SqsClientServerCommunication with Matchers
result.isLeft should be(true)
}

test("should return an error if strict & message body is empty") {
strictOnlyShouldThrowException { cli =>
val queueUrl = cli.createQueue(new CreateQueueRequest("testQueue1")).getQueueUrl

cli.sendMessage(new SendMessageRequest(queueUrl, ""))
}
}

test("should return an error if strict & sending an invalid character") {
strictOnlyShouldThrowException { cli =>
// Given
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.elasticmq.rest.sqs

import com.amazonaws.services.sqs.model._
import org.scalatest.OptionValues
import org.scalatest.matchers.should.Matchers

import scala.collection.JavaConverters.{collectionAsScalaIterableConverter, mapAsJavaMapConverter}

class MessageAttributesTests extends SqsClientServerCommunication with Matchers with OptionValues {

test("Sending message with empty attribute value and String data type should result in error") {
val queueUrl = client.createQueue(new CreateQueueRequest("testQueue1")).getQueueUrl

val ex = the[AmazonSQSException] thrownBy client.sendMessage(
new SendMessageRequest(queueUrl, "Message 1")
.addMessageAttributesEntry("attribute1", new MessageAttributeValue().withStringValue("").withDataType("String"))
)

ex.getMessage should include("Attribute 'attribute1' must contain a non-empty value of type 'String")
}

test("Sending message with empty attribute value and Number data type should result in error") {
val queueUrl = client.createQueue(new CreateQueueRequest("testQueue1")).getQueueUrl

val ex = the[AmazonSQSException] thrownBy client.sendMessage(
new SendMessageRequest(queueUrl, "Message 1")
.addMessageAttributesEntry("attribute1", new MessageAttributeValue().withStringValue("").withDataType("Number"))
)

ex.getMessage should include("Attribute 'attribute1' must contain a non-empty value of type 'Number")
}

test("Sending message with empty attribute type should result in error") {
val queueUrl = client.createQueue(new CreateQueueRequest("testQueue1")).getQueueUrl

val ex = the[AmazonSQSException] thrownBy client.sendMessage(
new SendMessageRequest(queueUrl, "Message 1")
.addMessageAttributesEntry("attribute1", new MessageAttributeValue().withStringValue("value").withDataType(""))
)

ex.getMessage should include("Attribute 'attribute1' must contain a non-empty attribute type")
}

test(
"Sending message in batch should result in accepting only those messages that do not have empty message attributes"
) {
val queueUrl = client.createQueue(new CreateQueueRequest("testQueue1")).getQueueUrl

val resp = client.sendMessageBatch(
new SendMessageBatchRequest(queueUrl).withEntries(
new SendMessageBatchRequestEntry("1", "Message 1").withMessageAttributes(
Map(
"attribute1" -> new MessageAttributeValue().withStringValue("value1").withDataType("String")
).asJava
),
new SendMessageBatchRequestEntry("2", "Message 2").withMessageAttributes(
Map(
"attribute1" -> new MessageAttributeValue().withStringValue("").withDataType("String")
).asJava
),
new SendMessageBatchRequestEntry("3", "Message 3").withMessageAttributes(
Map(
"attribute1" -> new MessageAttributeValue().withStringValue("value1").withDataType("String")
).asJava
)
)
)

resp.getSuccessful should have size 2

val receiveMessageResponse =
client.receiveMessage(new ReceiveMessageRequest().withQueueUrl(queueUrl).withMaxNumberOfMessages(3))
receiveMessageResponse.getMessages.asScala should have size 2
receiveMessageResponse.getMessages.asScala
.map(message => message.getBody)
.foreach(messageBody => {
messageBody should (be("Message 1") or be("Message 3"))
})
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,20 @@ trait SendMessageDirectives { this: ElasticMQDirectives with SQSLimitsModule =>
val strValue =
parameters("MessageAttribute." + i + ".Value.StringValue")
Limits
.verifyMessageStringAttribute(strValue, sqsLimits)
.verifyMessageStringAttribute(name, strValue, sqsLimits)
.fold(error => throw new SQSException(error), identity)
StringMessageAttribute(strValue, customDataType)
case "Number" =>
val strValue =
parameters("MessageAttribute." + i + ".Value.StringValue")
Limits
.verifyMessageNumberAttribute(strValue, sqsLimits)
.verifyMessageNumberAttribute(strValue, name, sqsLimits)
.fold(error => throw new SQSException(error), identity)
NumberMessageAttribute(strValue, customDataType)
case "Binary" =>
BinaryMessageAttribute.fromBase64(parameters("MessageAttribute." + i + ".Value.BinaryValue"), customDataType)
case "" =>
throw new SQSException(s"Attribute '$name' must contain a non-empty attribute type")
case _ =>
throw new Exception("Currently only handles String, Number and Binary typed attributes")
}
Expand Down Expand Up @@ -124,7 +126,7 @@ trait SendMessageDirectives { this: ElasticMQDirectives with SQSLimitsModule =>
val messageAttributes = getMessageAttributes(parameters)
val messageSystemAttributes = getMessageSystemAttributes(parameters)

Limits.verifyMessageStringAttribute(body, sqsLimits).fold(error => throw new SQSException(error), identity)
Limits.verifyMessageBody(body, sqsLimits).fold(error => throw new SQSException(error), identity)

val messageGroupId = parameters.get(MessageGroupIdParameter) match {
// MessageGroupId is only supported for FIFO queues
Expand Down