Repository: softwaremill/elasticmq (scala)

Issue:
Bug: Unexpected ` MissingQueryParamRejection(QueueUrl)` in SQS `SendMessage` when using a context path
When setting `node-address.context-path` to a non-empty value, calls to `SqsClient.sendMessage` stop working.  
Issue disappears if  `node-address.context-path = ""`

Calls to `SqsClient.getQueueUrl` still work, so I think the issue is in the way `QueueDirectives.scala#L46` is parsing the request.

Info from some debug logging:
```
Starting ElasticMQ server on http://localhost:34998/context                                                     
Slf4jLogger started                                                                                             
Started SQS rest server, bind address 0.0.0.0:34998, visible server address http://localhost:34998/context      
Started statistics rest server, bind address 0.0.0.0:34999                                                      
Creating queue QueueData(test-queue-1,MillisVisibilityTimeout(30000),PT0S,PT0S,2021-12-08T13:03:10.252Z,2021-12-08T13:03:10.252Z,None,false,false,None,None,Map())
Dec 08, 2021 1:03:12 PM TestClientClass getQueueUrlFromSqs
DEBUG: GetQueueUrlRequest(QueueName=test-queue-1)
Dec 08, 2021 1:03:13 PM TestClientClass getQueueUrlFromSqs
DEBUG: GetQueueUrlResponse(QueueUrl=http://localhost:34998/context/000000000000/test-queue-1)
Dec 08, 2021 1:03:13 PM TestClientClass addMessage
DEBUG: SendMessageRequest(QueueUrl=http://localhost:34998/context/000000000000/test-queue-1, MessageBody=This is the message)
Dec 08, 2021 1:03:13 PM TestClientClass addMessage
DEBUG: software.amazon.awssdk.services.sqs.model.SqsException: Invalid request: MissingQueryParamRejection(QueueName), MissingQueryParamRejection(QueueUrl); see the SQS docs. (Service: Sqs, Status Code: 400, Request ID: 00000000-0000-0000-0000-000000000000, Extended Request ID: null)                                                                                                              
```

Issue occurred using on:
- Java AdoptOpenJDK 1.8.0_292 Hotspot
- Maven 3.6.3
- org.elasticmq > elasticmq-server_2.13 > version 1.3.3

Pull request description:
fixes #585

Patch:
diff --git a/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/SQSRestServerBuilder.scala b/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/SQSRestServerBuilder.scala
index 5e9d4cc1f..66fa18249 100644
--- a/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/SQSRestServerBuilder.scala
+++ b/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/SQSRestServerBuilder.scala
@@ -137,6 +137,7 @@ case class TheSQSRestServerBuilder(
 
     val env = new QueueManagerActorModule
       with QueueURLModule
+      with ContextPathModule
       with SQSLimitsModule
       with BatchRequestsModule
       with ElasticMQDirectives
@@ -167,6 +168,7 @@ case class TheSQSRestServerBuilder(
       lazy val queueManagerActor = theQueueManagerActor
       lazy val sqsLimits = theLimits
       lazy val timeout = Timeout(21, TimeUnit.SECONDS) // see application.conf
+      lazy val contextPath = serverAddress.contextPath
 
       lazy val awsRegion: String = _awsRegion
       lazy val awsAccountId: String = _awsAccountId
@@ -448,6 +450,10 @@ trait QueueURLModule {
   }
 }
 
+trait ContextPathModule {
+  def contextPath: String
+}
+
 trait SQSLimitsModule {
   def sqsLimits: Limits
 }
diff --git a/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/directives/ElasticMQDirectives.scala b/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/directives/ElasticMQDirectives.scala
index ac8b8eba3..0819e25a0 100644
--- a/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/directives/ElasticMQDirectives.scala
+++ b/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/directives/ElasticMQDirectives.scala
@@ -1,7 +1,7 @@
 package org.elasticmq.rest.sqs.directives
 
 import akka.http.scaladsl.server.{Directives, Route}
-import org.elasticmq.rest.sqs.{ActorSystemModule, QueueManagerActorModule}
+import org.elasticmq.rest.sqs.{ActorSystemModule, ContextPathModule, QueueManagerActorModule, QueueURLModule}
 import org.elasticmq.util.Logging
 
 trait ElasticMQDirectives
@@ -11,6 +11,7 @@ trait ElasticMQDirectives
     with ExceptionDirectives
     with QueueDirectives
     with QueueManagerActorModule
+    with ContextPathModule
     with ActorSystemModule
     with AnyParamDirectives
     with RejectionDirectives
diff --git a/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/directives/QueueDirectives.scala b/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/directives/QueueDirectives.scala
index 6914037c2..b7680549a 100644
--- a/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/directives/QueueDirectives.scala
+++ b/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/directives/QueueDirectives.scala
@@ -2,6 +2,7 @@ package org.elasticmq.rest.sqs.directives
 
 import akka.actor.ActorRef
 import akka.http.scaladsl.model.Uri
+import akka.http.scaladsl.model.Uri.Path
 import akka.http.scaladsl.server.PathMatcher.{Matched, Unmatched}
 import akka.http.scaladsl.server._
 import org.elasticmq.QueueData
@@ -11,7 +12,12 @@ import org.elasticmq.rest.sqs.Constants._
 import org.elasticmq.rest.sqs._
 
 trait QueueDirectives {
-  this: Directives with QueueManagerActorModule with ActorSystemModule with FutureDirectives with AnyParamDirectives =>
+  this: Directives
+    with QueueManagerActorModule
+    with ContextPathModule
+    with ActorSystemModule
+    with FutureDirectives
+    with AnyParamDirectives =>
 
   def queueNameFromParams(p: AnyParams): Directive1[String] =
     p.requiredParam("QueueName")
@@ -44,10 +50,17 @@ trait QueueDirectives {
   private val accountIdRegex = "[a-zA-Z0-9]+".r
 
   private def queueNameFromRequest(p: AnyParams)(body: String => Route): Route = {
+    val pathDirective =
+      if (contextPath.nonEmpty)
+        pathPrefix(contextPath / accountIdRegex / Segment).tmap(_._2) |
+          pathPrefix(contextPath / QueueUrlContext / Segment)
+      else
+        pathPrefix(accountIdRegex / Segment).tmap(_._2) |
+          pathPrefix(QueueUrlContext / Segment)
+
     val queueNameDirective =
       checkOnlyOneSegmentInUri() |
-        pathPrefix(accountIdRegex / Segment).tmap(_._2) |
-        pathPrefix(QueueUrlContext / Segment) |
+        pathDirective |
         queueNameFromParams(p) |
         queueUrlFromParams(p).flatMap { queueUrl => getQueueNameFromQueueUrl(queueUrl) }
 
@@ -55,7 +68,13 @@ trait QueueDirectives {
   }
 
   private def getQueueNameFromQueueUrl(queueUrl: String): Directive1[String] = {
-    val matcher = Slash ~ accountIdRegex / "[^/]+".r
+
+    val matcher =
+      if (contextPath.nonEmpty)
+        separateOnSlashes(contextPath) / accountIdRegex / "[^/]+".r
+      else
+        Slash ~ accountIdRegex / "[^/]+".r
+
     matcher(Uri(queueUrl).path) match {
       case Matched(_, extractions) => provide(extractions._2): Directive1[String]
       case Unmatched =>
diff --git a/rest/rest-sqs/src/main/scala/org/elasticmq/rest/stats/StatisticsRestServerBuilder.scala b/rest/rest-sqs/src/main/scala/org/elasticmq/rest/stats/StatisticsRestServerBuilder.scala
index 4547e4d4e..a33a03544 100644
--- a/rest/rest-sqs/src/main/scala/org/elasticmq/rest/stats/StatisticsRestServerBuilder.scala
+++ b/rest/rest-sqs/src/main/scala/org/elasticmq/rest/stats/StatisticsRestServerBuilder.scala
@@ -20,7 +20,8 @@ case class TheStatisticsRestServerBuilder(
     interface: String,
     port: Int,
     _awsRegion: String,
-    _awsAccountId: String
+    _awsAccountId: String,
+    _contextPath: String
 ) extends Logging {
 
   /** @param _actorSystem
@@ -77,6 +78,7 @@ case class TheStatisticsRestServerBuilder(
       lazy val materializer = implicitMaterializer
       lazy val queueManagerActor = providedQueueManagerActor
       lazy val timeout = Timeout(21, TimeUnit.SECONDS) // see application.conf
+      lazy val contextPath = _contextPath
 
       lazy val awsRegion: String = _awsRegion
       lazy val awsAccountId: String = _awsAccountId
diff --git a/server/src/main/scala/org/elasticmq/server/ElasticMQServer.scala b/server/src/main/scala/org/elasticmq/server/ElasticMQServer.scala
index ab7f7eec8..8276fa32a 100644
--- a/server/src/main/scala/org/elasticmq/server/ElasticMQServer.scala
+++ b/server/src/main/scala/org/elasticmq/server/ElasticMQServer.scala
@@ -111,7 +111,8 @@ class ElasticMQServer(config: ElasticMQServerConfig) extends Logging {
         config.restStatisticsConfiguration.bindHostname,
         config.restStatisticsConfiguration.bindPort,
         config.awsRegion,
-        config.awsAccountId
+        config.awsAccountId,
+        config.nodeAddress.contextPath
       ).start()
 
       server.waitUntilStarted()


Test patch:
diff --git a/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesTest.scala b/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesTest.scala
index ee22df2bc..02eeab7d5 100644
--- a/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesTest.scala
+++ b/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesTest.scala
@@ -6,7 +6,7 @@ import akka.util.Timeout
 import org.elasticmq.actor.QueueManagerActor
 import org.elasticmq.actor.reply._
 import org.elasticmq.msg.CreateQueue
-import org.elasticmq.rest.sqs.{ActorSystemModule, QueueManagerActorModule}
+import org.elasticmq.rest.sqs.{ActorSystemModule, ContextPathModule, QueueManagerActorModule}
 import org.elasticmq.util.NowProvider
 import org.elasticmq.{MillisVisibilityTimeout, QueueData, StrictSQSLimits}
 import org.joda.time.{DateTime, Duration}
@@ -22,6 +22,7 @@ class QueueDirectivesTest
     with ScalatestRouteTest
     with Directives
     with QueueDirectives
+    with ContextPathModule
     with AnyParamDirectives
     with QueueManagerActorModule
     with FutureDirectives
@@ -35,6 +36,7 @@ class QueueDirectivesTest
   implicit lazy val actorSystem: ActorSystem = ActorSystem("lol")
   lazy val queueManagerActor: ActorRef =
     actorSystem.actorOf(Props(new QueueManagerActor(new NowProvider(), StrictSQSLimits, None)))
+  lazy val contextPath = ""
 
   "queueActorAndNameFromRequest" should "return correct queue name based on QueueName" in {
     val future = queueManagerActor ? CreateQueue(
diff --git a/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesWithContextPathTest.scala b/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesWithContextPathTest.scala
new file mode 100644
index 000000000..fa28a87b4
--- /dev/null
+++ b/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesWithContextPathTest.scala
@@ -0,0 +1,76 @@
+package org.elasticmq.rest.sqs.directives
+
+import akka.actor.{ActorRef, ActorSystem, Props}
+import akka.http.scaladsl.server.Directives
+import akka.http.scaladsl.testkit.ScalatestRouteTest
+import akka.util.Timeout
+import org.elasticmq.actor.QueueManagerActor
+import org.elasticmq.actor.reply._
+import org.elasticmq.msg.CreateQueue
+import org.elasticmq.rest.sqs.{ActorSystemModule, ContextPathModule, QueueManagerActorModule}
+import org.elasticmq.util.NowProvider
+import org.elasticmq.{MillisVisibilityTimeout, QueueData, StrictSQSLimits}
+import org.joda.time.{DateTime, Duration}
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+import scala.concurrent.duration._
+import scala.concurrent.{Await, ExecutionContextExecutor}
+
+class QueueDirectivesWithContextPathTest
+    extends AnyFlatSpec
+    with Matchers
+    with ScalatestRouteTest
+    with Directives
+    with QueueDirectives
+    with ContextPathModule
+    with AnyParamDirectives
+    with QueueManagerActorModule
+    with FutureDirectives
+    with ActorSystemModule
+    with ExceptionDirectives
+    with RespondDirectives {
+
+  private val maxDuration = 1.minute
+  implicit val timeout: Timeout = maxDuration
+  implicit val ec: ExecutionContextExecutor = actorSystem.dispatcher
+  implicit lazy val actorSystem: ActorSystem = ActorSystem("lol")
+  lazy val queueManagerActor: ActorRef =
+    actorSystem.actorOf(Props(new QueueManagerActor(new NowProvider(), StrictSQSLimits, None)))
+  lazy val contextPath = "/test-context"
+
+  "queueActorAndNameFromRequest" should "return correct queue name based on QueueName" in {
+    val future = queueManagerActor ? CreateQueue(
+      QueueData("lol", MillisVisibilityTimeout(1L), Duration.ZERO, Duration.ZERO, DateTime.now(), DateTime.now())
+    )
+    Await.result(future, maxDuration)
+    val route = {
+      queueActorAndNameFromRequest(
+        Map(
+          "QueueName" -> "lol",
+          "QueueUrl" -> "https://eu-central-1.queue.amazonaws.com/test-context/906175111765/lol"
+        )
+      ) { (_, name) => _.complete(name) }
+    }
+
+    Get("/test-context/906175111765/lol") ~> route ~> check {
+      responseAs[String] shouldEqual "lol"
+    }
+  }
+
+  "queueActorAndNameFromRequest" should "return correct queue name based on QueueUrl" in {
+    val future = queueManagerActor ? CreateQueue(
+      QueueData("lol", MillisVisibilityTimeout(1L), Duration.ZERO, Duration.ZERO, DateTime.now(), DateTime.now())
+    )
+    Await.result(future, maxDuration)
+    val route = {
+      queueActorAndNameFromRequest(
+        Map("QueueUrl" -> "https://eu-central-1.queue.amazonaws.com/test-context/906175111765/lol")
+      ) { (_, name) => _.complete(name) }
+    }
+
+    Get("/test-context") ~> route ~> check {
+      responseAs[String] shouldEqual "lol"
+    }
+  }
+}
diff --git a/rest/rest-sqs/src/test/scala/org/elasticmq/rest/stats/StatisticsDirectivesTest.scala b/rest/rest-sqs/src/test/scala/org/elasticmq/rest/stats/StatisticsDirectivesTest.scala
index e967fccec..202e5f0f7 100644
--- a/rest/rest-sqs/src/test/scala/org/elasticmq/rest/stats/StatisticsDirectivesTest.scala
+++ b/rest/rest-sqs/src/test/scala/org/elasticmq/rest/stats/StatisticsDirectivesTest.scala
@@ -52,6 +52,8 @@ class StatisticsDirectivesTest
   lazy val queueManagerActor: ActorRef =
     actorSystem.actorOf(Props(new QueueManagerActor(nowProvider, StrictSQSLimits, None)))
 
+  lazy val contextPath = ""
+
   "statisticsRequestForAllQueues" should "return all queues statistics" in {
 
     val route = {