Repository: softwaremill/elasticmq (scala)

Issue:
Custom QueueUrlContext
Currently when creating a queue the URL is always of the form `http://<host>/queue/<name>`. Originally I thought I could configure the `queue` part via the `context-path` setting, but that does not appear to be the case. Would it be possible to allow this [hardcoded](https://github.com/adamw/elasticmq/blob/80bc3978f943752143b4cc009902e0a638fd8b45/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/SQSRestServerBuilder.scala#L228) value to be customised?

This is also a request to be able to hit the URL generated with the custom `QueueUrlContext` too.

Pull request description:
closes #117

Patch:
diff --git a/core/src/main/scala/org/elasticmq/NodeAddress.scala b/core/src/main/scala/org/elasticmq/NodeAddress.scala
index 9ac60a3a9..334e0dae5 100644
--- a/core/src/main/scala/org/elasticmq/NodeAddress.scala
+++ b/core/src/main/scala/org/elasticmq/NodeAddress.scala
@@ -7,6 +7,8 @@ case class NodeAddress(
     contextPath: String = ""
 ) {
   def hostAndPort: String = host + ":" + port
-  def fullAddress: String = protocol + "://" + hostAndPort + contextPath
+  def fullAddress: String = protocol + "://" + hostAndPort + suffix
   def isWildcard: Boolean = host == "*"
+  def contextPathStripped: String = contextPath.stripPrefix("/").stripSuffix("/")
+  def suffix = if (contextPath.isBlank) "" else "/" + contextPathStripped
 }
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 95848dece..c91885a15 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
@@ -167,7 +167,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 contextPath = serverAddress.contextPathStripped
 
       lazy val awsRegion: String = _awsRegion
       lazy val awsAccountId: String = _awsAccountId
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 0819e25a0..fd71ac831 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, ContextPathModule, QueueManagerActorModule, QueueURLModule}
+import org.elasticmq.rest.sqs.{ActorSystemModule, ContextPathModule, QueueManagerActorModule}
 import org.elasticmq.util.Logging
 
 trait ElasticMQDirectives
@@ -24,7 +24,7 @@ trait ElasticMQDirectives
   private val validFifoParameterValueCharsRe = """^[a-zA-Z0-9!"#\$%&'\(\)\*\+,-\./:;<=>?@\[\\\]\^_`\{|\}~]{1,128}$""".r
 
   def rootPath(body: Route): Route = {
-    path("") {
+    path(separateOnSlashes(contextPath) ~ Slash.?) {
       body
     }
   }
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 b7680549a..b74df5383 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,13 +2,11 @@ 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
 import org.elasticmq.actor.reply._
 import org.elasticmq.msg.{GetQueueData, LookupQueue}
-import org.elasticmq.rest.sqs.Constants._
 import org.elasticmq.rest.sqs._
 
 trait QueueDirectives {
@@ -53,10 +51,10 @@ trait QueueDirectives {
     val pathDirective =
       if (contextPath.nonEmpty)
         pathPrefix(contextPath / accountIdRegex / Segment).tmap(_._2) |
-          pathPrefix(contextPath / QueueUrlContext / Segment)
+          pathPrefix(contextPath / Segment)
       else
         pathPrefix(accountIdRegex / Segment).tmap(_._2) |
-          pathPrefix(QueueUrlContext / Segment)
+          pathPrefix(Segment)
 
     val queueNameDirective =
       checkOnlyOneSegmentInUri() |


Test patch:
diff --git a/rest/rest-sqs-testing-amazon-java-sdk/src/test/scala/org/elasticmq/rest/sqs/AmazonJavaSdkTestSuite.scala b/rest/rest-sqs-testing-amazon-java-sdk/src/test/scala/org/elasticmq/rest/sqs/AmazonJavaSdkTestSuite.scala
index fdd0a7a84..afa5fd644 100644
--- a/rest/rest-sqs-testing-amazon-java-sdk/src/test/scala/org/elasticmq/rest/sqs/AmazonJavaSdkTestSuite.scala
+++ b/rest/rest-sqs-testing-amazon-java-sdk/src/test/scala/org/elasticmq/rest/sqs/AmazonJavaSdkTestSuite.scala
@@ -102,22 +102,28 @@ class AmazonJavaSdkTestSuite extends SqsClientServerCommunication with Matchers
     client.listQueues().getQueueUrls.size() should be(0)
   }
 
-  test("should refer to queue via both URL formats") {
+  test("should refer to queue via three URL formats") {
     // Given
     val queueUrl = client.createQueue(new CreateQueueRequest("lol")).getQueueUrl
 
     // When
-    val attributes2 = client
+    val attributes1 = client
       .getQueueAttributes(new GetQueueAttributesRequest("http://localhost:9321/queue/lol").withAttributeNames("All"))
       .getAttributes
-    val attributes1 = client
+    val attributes2 = client
       .getQueueAttributes(
         new GetQueueAttributesRequest("http://localhost:9321/012345678900/lol").withAttributeNames("All")
       )
       .getAttributes
+    val attributes3 = client
+      .getQueueAttributes(
+        new GetQueueAttributesRequest("http://localhost:9321/lol").withAttributeNames("All")
+      )
+      .getAttributes
 
     // Then
     attributes1 should be(attributes2)
+    attributes1 should be(attributes3)
   }
 
   test("should get queue visibility timeout") {
@@ -255,7 +261,7 @@ class AmazonJavaSdkTestSuite extends SqsClientServerCommunication with Matchers
     Source.fromInputStream(res.getEntity.getContent).mkString should include("<Code>InvalidAction</Code>")
   }
 
-  test("should reply with 404 if request can't be recognized as any known route") {
+  test("should reply with 400 and NonExistentQueue if path starts with invalid name") {
     // given
     val httpHost = new HttpHost("localhost", 9321)
     val req = new HttpPost()
@@ -268,8 +274,9 @@ class AmazonJavaSdkTestSuite extends SqsClientServerCommunication with Matchers
 
     // then
     res.getStatusLine.getStatusCode shouldBe StatusCodes.BadRequest.intValue
-    Source.fromInputStream(res.getEntity.getContent).mkString should include("<Code>Invalid request")
-
+    Source.fromInputStream(res.getEntity.getContent).mkString should include(
+      "<Code>AWS.SimpleQueueService.NonExistentQueue"
+    )
   }
 
   test("should reply with a InvalidAction error when uknown action set") {
@@ -2048,13 +2055,6 @@ class AmazonJavaSdkTestSuite extends SqsClientServerCommunication with Matchers
     }
   }
 
-  test("should throw proper exception when referencing queue by name") {
-    val _ = client.createQueue(new CreateQueueRequest("testQueue1")).getQueueUrl
-
-    val ex = the[AmazonSQSException] thrownBy client.sendMessage(new SendMessageRequest("testQueue1", "Message 1"))
-    ex.getMessage should include("WrongURLFormatRejection(Provided only queueName instead of the full URL)")
-  }
-
   test("should move message to dlq after exceeding maxReceiveCount") {
 
     // given
diff --git a/rest/rest-sqs-testing-amazon-java-sdk/src/test/scala/org/elasticmq/rest/sqs/CustomServerContextTest.scala b/rest/rest-sqs-testing-amazon-java-sdk/src/test/scala/org/elasticmq/rest/sqs/CustomServerContextTest.scala
new file mode 100644
index 000000000..8974c9173
--- /dev/null
+++ b/rest/rest-sqs-testing-amazon-java-sdk/src/test/scala/org/elasticmq/rest/sqs/CustomServerContextTest.scala
@@ -0,0 +1,58 @@
+package org.elasticmq.rest.sqs
+
+import com.amazonaws.auth.{AWSStaticCredentialsProvider, BasicAWSCredentials}
+import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration
+import com.amazonaws.services.sqs.model.ReceiveMessageRequest
+import com.amazonaws.services.sqs.{AmazonSQS, AmazonSQSClientBuilder}
+import org.elasticmq.NodeAddress
+import org.elasticmq.util.Logging
+import org.scalatest.funsuite.AnyFunSuite
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.{BeforeAndAfter, OptionValues}
+
+import scala.collection.JavaConverters._
+import scala.util.Try
+
+class CustomServerContextTest extends AnyFunSuite with BeforeAndAfter with Logging with Matchers with OptionValues {
+
+  var client: AmazonSQS = _
+  var server: SQSRestServer = _
+
+  before {
+    server = SQSRestServerBuilder
+      .withPort(9321)
+      .withServerAddress(NodeAddress(port = 9321, contextPath = "xyz"))
+      .withAWSAccountId("123456789012")
+      .withAWSRegion("elasticmq")
+      .start()
+
+    client = AmazonSQSClientBuilder
+      .standard()
+      .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials("x", "x")))
+      .withEndpointConfiguration(new EndpointConfiguration("http://localhost:9321/xyz", "us-east-1"))
+      .build()
+  }
+
+  after {
+    client.shutdown()
+    Try(server.stopAndWait())
+  }
+
+  test("should create queue when context path is set") {
+    val queue = client.createQueue("testQueue")
+    queue.getQueueUrl should be("http://localhost:9321/xyz/123456789012/testQueue")
+  }
+
+  test("should send and receive messages when context path is set") {
+    val queue = client.createQueue("testQueue")
+
+    client.sendMessage("http://localhost:9321/xyz/123456789012/testQueue", "test msg1")
+    client.sendMessage("http://localhost:9321/xyz/queue/testQueue", "test msg2")
+    client.sendMessage("http://localhost:9321/xyz/abc/testQueue", "test msg3")
+    client.sendMessage("http://localhost:9321/xyz/testQueue", "test msg4")
+
+    val message = client.receiveMessage(new ReceiveMessageRequest(queue.getQueueUrl).withMaxNumberOfMessages(10))
+
+    message.getMessages.asScala.map(_.getBody) should contain allOf ("test msg1", "test msg2", "test msg3", "test msg4")
+  }
+}