{"action":"created","comment":{"url":"https://api.github.com/repos/wemrysi/quasar/pulls/comments/43309113","id":43309113,"diff_hunk":"@@ -0,0 +1,511 @@\n+package quasar\n+package api\n+package services\n+\n+import org.specs2.ScalaCheck\n+import org.specs2.mutable.Specification\n+import quasar.Data\n+import quasar.Predef._\n+import quasar.api.MessageFormat._\n+import JsonPrecision._\n+import JsonFormat._\n+import quasar.DataCodec\n+\n+import argonaut._, Argonaut._\n+import org.http4s._\n+import org.http4s.argonaut._\n+import org.http4s.headers._\n+import org.http4s.server._\n+import quasar.fs.{Path => _, _}\n+import pathy.Path\n+import pathy.Path._\n+import quasar.fs.PathyGen._\n+\n+import scalaz._, Scalaz._\n+import scalaz.concurrent.Task\n+\n+import org.scalacheck.Arbitrary\n+import Gen._\n+\n+\n+class DataServiceSpec extends Specification with ScalaCheck with FileSystemFixture with Http4s {\n+  import inmemory._, ManageFile.Node, quasar.DataGen._\n+\n+  def interpretM(mem: InMemState): ManageFile ~> Task =\n+    new (ManageFile ~> Task) {\n+      def apply[A](fs: ManageFile[A]) =\n+        Task.now(manageFile(fs).eval(mem))\n+    }\n+\n+  def interpretR(mem: InMemState): ReadFile ~> Task =\n+    new (ReadFile ~> Task) {\n+      def apply[A](fs: ReadFile[A]) =\n+        Task.now(readFile(fs).eval(mem))\n+    }\n+\n+  def interpretW(mem: InMemState): WriteFile ~> Task =\n+    new (WriteFile ~> Task) {\n+      def apply[A](fs: WriteFile[A]) =\n+        Task.now(writeFile(fs).eval(mem))\n+    }\n+\n+  def service(mem: InMemState): HttpService =\n+    data.service[FileSystem](interpretFileSystem[Task](interpretR(mem), interpretW(mem), interpretM(mem)))\n+\n+  implicit val arbJson: Arbitrary[Json] = Arbitrary(Arbitrary.arbitrary[String].map(jString(_)))\n+\n+  val jsonReadableLine = JsonContentType(Readable,LineDelimited)\n+  val csv = MediaType.`text/csv`\n+\n+  \"Data Service\" should {\n+    \"GET\" >> {\n+      \"respond with NotFound\" >> {\n+        \"if file does not exist\" >> {\n+          val unexistantFile = rootDir[Sandboxed] </> dir(\"foo\") </> file(\"bar.json\")\n+          val path:String = posixCodec.printPath(unexistantFile)\n+          val response = service(InMemState.empty)(Request(uri = Uri(path = path))).run\n+          response.status must_== Status.NotFound\n+          response.as[String].run must_== \"/foo/bar.json: doesn't exist\"\n+        }\n+      }\n+      \"respond with file data\" >> {\n+        val sampleFile = rootDir[Sandboxed] </> dir(\"foo\") </> file(\"bar\")\n+        val samplePath: String = posixCodec.printPath(sampleFile)\n+        def fileSystemWithSampleFile(data: Vector[Data]) = InMemState fromFiles Map(sampleFile -> data)\n+        \"json by default\" >> {\n+          def isExpectedResponse(\n+            data: Vector[Data], response: Response,\n+            format: JsonContentType = JsonContentType(Readable, LineDelimited)\n+          ) = {\n+            val expectedData = data.map(format.mode.codec.encode).sequenceU.toOption.get\n+            response.contentType must_== Some(format.mediaType)\n+            response.as[Json].run must_== expectedData\n+          }\n+          \"readable by default\" ! prop { data: Vector[Data] =>\n+            val response = service(fileSystemWithSampleFile(data))(Request(uri = Uri(path = samplePath))).run\n+            isExpectedResponse(data, response)\n+          }\n+          \"precise if specified\" >> {\n+            \"in the content-type header\" ! prop { data: Vector[Data] =>\n+              val contentType = JsonContentType(Precise,LineDelimited)\n+              val request = Request(\n+                uri = Uri(path = samplePath),\n+                headers = Headers(Accept(contentType.mediaType)))\n+              val response = service(fileSystemWithSampleFile(data))(request).run\n+              isExpectedResponse(data, response, contentType)\n+            }\n+            \"in the request-headers\" ! prop { data: Vector[Data] =>\n+              val request = Request(\n+                uri = Uri(path = samplePath).+?(\"request-headers\", \"\"\"{\"Accept\": \"application/ldjson; mode=precise\" }\"\"\"))\n+              val response = service(fileSystemWithSampleFile(data))(request).run\n+              isExpectedResponse(data, response, JsonContentType(Precise,LineDelimited))\n+            }\n+          }\n+          \"complicated Accept Header\" ! prop { data: Vector[Data] =>\n+            val request = Request(\n+              uri = Uri(path = samplePath),\n+              headers = Headers(Header(\"Accept\", \"application/ldjson;q=0.9;mode=readable,application/json;boundary=NL;mode=precise\")))\n+            val response = service(fileSystemWithSampleFile(data))(request).run\n+            isExpectedResponse(data, response, JsonContentType(Precise,LineDelimited))\n+          }\n+          \"in array if specified\" ! prop { data: Vector[Data] =>\n+            val request = Request(\n+              uri = Uri(path = samplePath),\n+              headers = Headers(Accept(MediaType.`application/json`)))\n+            val response = service(fileSystemWithSampleFile(data))(request).run\n+            val expectedData = data.map(Readable.codec.encode).sequenceU.toOption.get\n+            response.contentType must_== Some(JsonContentType(Readable,SingleArray).mediaType)\n+            response.as[Json].run must_== Json.array(expectedData: _*)\n+          }\n+          \"with gziped encoding when specified\" ! prop { data: Vector[Data] =>\n+            val request = Request(\n+              uri = Uri(path = samplePath),\n+              headers = Headers(`Accept-Encoding`(org.http4s.ContentCoding.gzip)))\n+            val response = service(fileSystemWithSampleFile(data))(request).run\n+            response.headers.get(headers.`Content-Encoding`) must_== Some(ContentCoding.gzip)\n+            response.status must_== Status.Ok\n+          }\n+          // TODO: Make the filename a property in order to remove this test\n+          \"even if file name contains a space\" ! prop { data: Vector[Data] =>\n+            val fileWithSpaceInName = rootDir[Sandboxed] </> dir(\"foo\") </> file(\"bar bix.json\")\n+            val path: String = posixCodec.printPath(fileWithSpaceInName)\n+            val filesystem = InMemState fromFiles Map(fileWithSpaceInName -> data)\n+            val response = service(filesystem)(Request(uri = Uri(path = path))).run\n+            isExpectedResponse(data, response)\n+          }\n+          \"support disposition\" ! prop { data: Vector[Data] =>\n+            val disposition = `Content-Disposition`(\"attachement\", Map(\"filename\" -> \"data.json\"))\n+            val request = Request(\n+              uri = Uri(path = samplePath),\n+              headers = Headers(Accept(jsonReadableLine.copy(disposition = Some(disposition)).mediaType)))\n+            val response = service(fileSystemWithSampleFile(data))(request).run\n+            response.headers.get(`Content-Disposition`) must_== Some(disposition)\n+          }\n+          \"support offset and limit\" >> {\n+            \"return expected result if user supplies valid values\" ! prop { (data: Vector[Data], offset: Int, limit: Int) =>\n+              (offset > 0 && limit >= 1) ==> {\n+                val request = Request(\n+                  uri = Uri(path = samplePath).+?(\"offset\", offset.toString).+?(\"limit\", limit.toString))\n+                val response = service(fileSystemWithSampleFile(data))(request).run\n+                isExpectedResponse(data.slice(offset, limit), response)\n+              }\n+            }\n+            \"return 400 if provided with\" >> {\n+              \"a negative limit\" ! prop { (offset: Natural, limit: Int) =>\n+                (limit < 1) ==> {\n+                  val request = Request(\n+                    uri = Uri(path = samplePath).+?(\"offset\", offset.value.toString).+?(\"limit\", limit.toString))\n+                  val response = service(InMemState.empty)(request).run\n+                  response.status must_== Status.BadRequest\n+                  response.as[String].run must_== s\"invalid limit: $limit (must be >= 1)\"\n+                }\n+              }\n+              \"what do we do if provided with multiple limits?\" ! prop { (offset: Natural, limits: List[Positive]) =>\n+                (limits.length >= 2) ==> {\n+                  val request = Request(\n+                    uri = Uri(path = samplePath).+?(\"offset\", offset.value.toString).+?(\"limit\", limits.map(_.value.toString)))\n+                  val response = service(InMemState.empty)(request).run\n+                  response.status must_== Status.BadRequest\n+                  response.as[String].run must_== s\"Two limits were provided, only supply one limit\"\n+                  todo // Confirm this is the expected behavior because http4s defaults to just grabbing the first one\n+                       // and going against that default behavior would be more work\n+                }\n+              }\n+              \"what do we do if provided with multiple offsets?\" ! prop { (limit: Positive, offsets: List[Natural]) =>\n+                (offsets.length >= 2) ==> {\n+                  val request = Request(\n+                    uri = Uri(path = samplePath).+?(\"offset\", offsets.map(_.value.toString)).+?(\"limit\", limit.value.toString))\n+                  val response = service(InMemState.empty)(request).run\n+                  response.status must_== Status.BadRequest\n+                  response.as[String].run must_== s\"Two offsets were provided, only supply one offset\"\n+                  todo // Confirm this is the expected behavior because http4s defaults to just grabbing the first one\n+                       // and going against that default behavior would be more work\n+                }\n+              }\n+              \"an unparsable limit\" >> {\n+                val request = Request(\n+                  uri = Uri(path = samplePath).+?(\"limit\", \"a\"))\n+                val response = service(InMemState.empty)(request).run\n+                response.status must_== Status.BadRequest\n+                response.as[String].run must_== s\"\"\"invalid limit: Query decoding Long failed (For input string: \"a\")\"\"\"\n+              }\n+            }\n+          }\n+          \"support very large data set\" >> {\n+            val data = (0 until 100*1000).map(n => Data.Obj(ListMap(\"n\" -> Data.Int(n)))).toVector\n+            \"plain text\" >> {\n+              val request = Request(\n+                uri = Uri(path = samplePath))\n+              val response = service(fileSystemWithSampleFile(data))(request).run\n+              isExpectedResponse(data, response)\n+            }\n+            \"gziped\" >> {\n+              val request = Request(\n+                uri = Uri(path = samplePath),\n+                headers = Headers(`Accept-Encoding`(org.http4s.ContentCoding.gzip)))\n+              val response = service(fileSystemWithSampleFile(data))(request).run\n+              isExpectedResponse(data, response)\n+            }\n+          }\n+        }\n+        \"csv if specified\" >> {\n+          val simpleData = List(\n+            Data.Obj(ListMap(\"a\" -> Data.Int(1))),\n+            Data.Obj(ListMap(\"b\" -> Data.Int(2))),\n+            Data.Obj(ListMap(\"c\" -> Data.Set(List(Data.Int(3))))))\n+          val simpleExpected = List(\"a,b,c[0]\", \"1,,\", \",2,\", \",,3\")\n+          def test(\n+                  data: List[Data],\n+                  expectedLines: List[String],\n+                  requestMediaType: MediaType = MediaType.`text/csv`,\n+                  expectedResponseMediaType: MediaType = MessageFormat.Csv.Default.mediaType) = {\n+            val request = Request(\n+              uri = Uri(path = samplePath),\n+              headers = Headers(headers.Accept(requestMediaType))\n+            )\n+            val response = service(fileSystemWithSampleFile(data.toVector))(request).run\n+            response.status must_== Status.Ok\n+            response.contentType must_== Some(expectedResponseMediaType)\n+            response.as[String].run must_== expectedLines.mkString(\"\\n\")\n+          }\n+\n+          \"simple\" >> test(\n+            data = simpleData,\n+            expectedLines = simpleExpected\n+          )\n+          \"with quoting\" >> test(\n+            data = List(\n+              Data.Obj(ListMap(\n+                \"a\" -> Data.Str(\"\\\"Hey\\\"\"),\n+                \"b\" -> Data.Str(\"a, b, c\")))),\n+            expectedLines = List(\"a,b\", \"\\\"\\\"\\\"Hey\\\"\\\"\\\",\\\"a, b, c\\\"\")\n+          )\n+          \"specifying format exactly\" >> {\n+            \"alternative delimiters\" >> {\n+              val extensions = Map(\n+                \"columnDelimiter\" -> \"\\\"\\t\\\"\",\n+                \"rowDelimiter\" -> \"\\\";\\\"\",\n+                \"quoteChar\" -> \"\\\"'\\\"\",\n+                \"escapeChar\" -> \"\\\"\\\\\\\\\\\"\"\n+              )\n+              val alternative = MediaType.`text/csv`.withExtensions(extensions)\n+              test(\n+                data = simpleData,\n+                expectedLines = List(\"a\\tb\\tc[0];1\\t\\t;\\t2\\t;\\t\\t3;\"),\n+                requestMediaType = alternative,\n+                expectedResponseMediaType = alternative\n+              )\n+            }\n+            \"the default parameters\" >> test(\n+              data = simpleData,\n+              expectedLines = simpleExpected,\n+              requestMediaType = MessageFormat.Csv.Default.mediaType,\n+              expectedResponseMediaType = MessageFormat.Csv.Default.mediaType\n+            )\n+          }\n+          \"using disposition to download as zipped directory\" >> {\n+            val disposition = `Content-Disposition`(\"attachement\", Map(\"filename\" -> \"foo.zip\"))\n+            val requestMediaType = MediaType.`text/csv`.withExtensions(Map(\"disposition\" -> disposition.value))\n+            val request = Request(\n+              uri = Uri(path = samplePath),\n+              headers = Headers(Accept(requestMediaType)))\n+            val response = service(fileSystemWithSampleFile(simpleData.toVector))(request).run\n+            response.status must_== Status.Ok\n+            response.contentType must_== Some(MediaType.`application/zip`)\n+            response.headers.get(`Content-Disposition`) must_== disposition\n+          }\n+        }\n+        \"what happens if user specifies a Path that is a directory but without the appropriate headers?\" >> todo\n+      }\n+    }\n+    \"POST and PUT\" >> {\n+      def testBoth[A](test: org.http4s.Method => Unit) = {\n+        \"POST\" should {\n+          test(org.http4s.Method.POST)\n+        }\n+        \"PUT\" should {\n+          test(org.http4s.Method.PUT)\n+        }\n+      }\n+      testBoth { method =>\n+        \"be 415 if media-type is missing\" ! prop { file: Path[Abs,File,Sandboxed] =>\n+          val path = posixCodec.printPath(file)\n+          val request = Request(\n+            uri = Uri(path = path),\n+            method = method).withBody(\"{\\\"a\\\": 1}\\n{\\\"b\\\": \\\"12:34:56\\\"}\").run\n+          val response = service(emptyMem)(request).run\n+          response.status must_== Status.UnsupportedMediaType\n+          response.as[String].run must_== \"No media-type is specified in Content-Type header\"\n+        }\n+        \"be 400 with\" >> {\n+          def be400(body: String, expectedBody: String, mediaType: MediaType = jsonReadableLine.mediaType) = {\n+            prop { file: Path[Abs, File, Sandboxed] =>\n+              val path = posixCodec.printPath(file)\n+              val request = Request(\n+                uri = Uri(path = path),\n+                headers = Headers(`Content-Type`(mediaType, Charset.`UTF-8`))).withBody(body).run\n+              val response = service(emptyMem)(request).run\n+              response.status must_== Status.BadRequest\n+              response.as[String].run must_== expectedBody\n+              // TODO: Assert nothing changed in the backend\n+            }\n+          }\n+          \"invalid body\" >> {\n+            \"no body\" ! be400(body = \"\", expectedBody = \"Request has no body\")\n+            \"invalid JSON\" ! be400(body = \"{\", expectedBody = \"Some uploaded value(s) could not be processed\")\n+            \"invalid CSV\" >> {\n+              \"empty (no headers)\" ! {\n+                be400(body = \"\", expectedBody = \"Some uploaded value(s) could not be processed\", csv)\n+              }\n+              \"if broken (after the tenth data line)\" ! {\n+                val brokenBody = \"\\\"a\\\",\\\"b\\\"\\n1,2\\n3,4\\n5,6\\n7,8\\n9,10\\n11,12\\n13,14\\n15,16\\n17,18\\n19,20\\n\\\",\\n\"\n+                be400(body = brokenBody, expectedBody = \"Some uploaded value(s) could not be processed\", csv)\n+              }\n+            }\n+          }\n+          // TODO: Consider spliting this into a case of Root (depth == 0) and missing dir (depth > 1)\n+          \"if path is invalid (parent directory does not exist)\" ! prop { (file: Path[Abs,File,Sandboxed], json: Json) =>\n+            Path.depth(file) != 1 ==> {\n+              be400(body = json.spaces4, s\"Invalid path: ${posixCodec.printPath(file)}\")\n+            }\n+          }\n+        }\n+        \"accept valid data\" >> {\n+          def accept(body: String, expected: List[Data], mediaType: MediaType) =\n+            prop { file: Path[Abs, File, Sandboxed] =>\n+              (Path.depth(file) == 1) ==> {\n+                val path = posixCodec.printPath(file)\n+                val request = Request(\n+                  uri = Uri(path = path),\n+                  method = method,\n+                  headers = Headers(`Content-Type`(mediaType, Charset.`UTF-8`))).withBody(body).run\n+                val response = service(emptyMem)(request).run\n+                response.status must_== Status.Ok\n+                response.as[String] must_== \"\"\n+                // TODO: How do I check the in-memory interpreter to find out if the files were create\n+                // also need to test for Append vs Save\n+              }\n+            }\n+          val expectedData = List(\n+            Data.Obj(ListMap(\"a\" -> Data.Int(1))),\n+            Data.Obj(ListMap(\"b\" -> Data.Time(org.threeten.bp.LocalTime.parse(\"12:34:56\")))))\n+          \"Json\" >> {\n+            def formatAsMultiLineArray(jsonBlob: String) = {\n+              // insert a comma at the end of each line (because we are in an array)\n+              val adaptedBlob = jsonBlob.split(\"\\n\").mkString(\",\\n\")\n+              s\"[$adaptedBlob]\"\n+            }\n+            def formatAsSingleLineArray(jsonBlob: String) = {\n+              // Remove the newline and replace with a comma only (because we are in an array)\n+              val adaptedBlob = jsonBlob.split(\"\\n\").mkString(\",\")\n+              s\"[$adaptedBlob]\"\n+            }\n+            def testPreciseAndReadable[A](test: (JsonPrecision, String) => Unit) = {\n+              \"Precise\" should {\n+                val jsonBlob =\n+                  \"\"\"{\"a\" : 1}\n+                    |{\"b\" : {\"$time\": \"12:34:56\"}}\"\"\".stripMargin\n+                test(Precise, jsonBlob)\n+              }//.pendingUntilFixed(\"SD-1066\") Damn you specs2 2.x!!\n+              \"Readable\" should {\n+                val jsonBlob =\n+                  \"\"\"{\"a\" : 1}\n+                    |{\"b\" : \"12:34:56\"}\"\"\".stripMargin\n+                test(Readable, jsonBlob)\n+              }\n+            }\n+            testPreciseAndReadable { (precision, jsonBlob) =>\n+              \"when formatted with one json object per line\" ! {\n+                accept(jsonBlob, expectedData, JsonContentType(precision, LineDelimited).mediaType)\n+              }\n+              \"when formatted as a single json array\" ! {\n+                accept(formatAsMultiLineArray(jsonBlob), List(Data.Arr(expectedData)), JsonContentType(precision, SingleArray).mediaType)\n+              }\n+              \"when having multiple lines containing arrays\" ! {\n+                val arbitraryValue = 3\n+                def replicate[A](a: A) = Applicative[Id].replicateM[A](arbitraryValue, a)\n+                val jsonString = replicate(formatAsSingleLineArray(jsonBlob)).mkString(\"\\n\")\n+                accept(jsonString, replicate(Data.Arr(expectedData)), JsonContentType(precision, LineDelimited).mediaType)\n+              }\n+              () // Eliminates warning, specs2 3.x will allow us to remove\n+            }\n+          }\n+          \"CSV\" >> {\n+            \"standard\" >> {\n+              accept(\"a,b\\n1,\\n,12:34:56\", expectedData, MediaType.`text/csv`)\n+            }\n+            \"weird\" >> {\n+              val weirdData = List(\n+                Data.Obj(ListMap(\"a\" -> Data.Int(1))),\n+                Data.Obj(ListMap(\"b\" -> Data.Str(\"[1|2|3]\"))))\n+              accept(\"a|b\\n1|\\n|'[1|2|3]'\\n\", weirdData, MediaType.`text/csv`)\n+            }\n+          }\n+        }\n+        \"produce two errors with partially invalid JSON\" ! prop { path: Path[Abs,File,Sandboxed] =>\n+          val twoErrorJson = \"\"\"{\"a\": 1}\n+                               |\"unmatched\n+                               |{\"b\": 2}\n+                               |}\n+                               |{\"c\": 3}\"\"\".stripMargin\n+          val pathString = posixCodec.printPath(path)\n+          val request = Request(\n+            uri = Uri(path = pathString),\n+            method = method,\n+            headers = Headers(`Content-Type`(jsonReadableLine.mediaType))).withBody(twoErrorJson).run\n+          val response = service(emptyMem)(request).run\n+          val jsonResponse = response.as[Json].run\n+          jsonResponse must_== Json(\"details\" := \"expected two error messages\") // TODO: Put actual error messages\n+          // TODO: Make sure the filesystem has not been changed?\n+        }\n+        () // Required for testBoth (not required with specs2 3.x)\n+      }\n+    }\n+    \"PUT\" >> {\n+      \"be 500 with simualated error on a particular value\" >> {\n+        todo\n+        // TODO: Understand what this test is meant to assert\n+      }\n+    }\n+    \"POST\" >> {\n+      \"be 500 with simulated error on a particular value\" >> {\n+        todo\n+        // TODO: Understand what this test is meant to assert\n+      }\n+    }\n+    \"MOVE\" >> {\n+      \"be 400 for missing Destination header\" ! prop { path: Path[Abs,File,Sandboxed] =>\n+        val pathString = posixCodec.printPath(path)\n+        val request = Request(uri = Uri(path = pathString), method = Method.MOVE)\n+        val response = service(emptyMem)(request).run\n+        response.status must_== Status.BadRequest\n+        response.as[String].run must_== \"The 'Destination' header must be specified\"\n+      }\n+      \"be 404 for missing source file\" ! prop { (source: Path[Abs,File,Sandboxed], destination: Path[Abs,File,Sandboxed]) =>\n+        val sourceString = posixCodec.printPath(source)\n+        val destinationString = posixCodec.printPath(destination)\n+        val request = Request(uri = Uri(path = sourceString), headers = Headers(Header(\"Destination\", destinationString)), method = Method.MOVE)\n+        val response = service(emptyMem)(request).run\n+        response.status must_== Status.NotFound\n+        response.as[String].run must_== s\"$sourceString: doesn't exist\"\n+      }\n+      \"be 404 for missing destination file\" ! prop { (source: Path[Abs,File,Sandboxed], destination: Path[Abs,File,Sandboxed]) =>\n+        todo // Figure out what we are doing with nested backends because not sure this test makes sense without them\n+      }\n+      \"be 201 with file\" ! prop {(source: Path[Abs,File,Sandboxed], destination: Path[Abs,File,Sandboxed], data: Vector[Data]) =>\n+        val sourceString = posixCodec.printPath(source)\n+        val destinationString = posixCodec.printPath(destination)\n+        val request = Request(uri = Uri(path = sourceString), headers = Headers(Header(\"Destination\", destinationString)), method = Method.MOVE)\n+        val filesystem = InMemState.fromFiles(Map(source -> data))\n+        val response = service(filesystem)(request).run\n+        response.status must_== Status.Created\n+        // TODO: Check that the file was moved properly (not in old test though...)\n+      }\n+      \"be 201 with dir\" ! prop {(sourceDir: Path[Abs,Dir,Sandboxed], sampleFiles: Map[String, Vector[Data]], destinationDir: Path[Abs,Dir,Sandboxed]) =>\n+        val sourceDirString = posixCodec.printPath(sourceDir)\n+        val fileMapping = sampleFiles.map{ case (fileName,data) => (sourceDir </> file(fileName), data)}\n+        val destinationString = posixCodec.printPath(destinationDir)\n+        val request = Request(uri = Uri(path = sourceDirString), headers = Headers(Header(\"Destination\", destinationString)), method = Method.MOVE)\n+        val filesystem = InMemState fromFiles fileMapping\n+        val response = service(filesystem)(request).run\n+        response.status must_== Status.Created\n+        // TODO: Check that the file was moved properly (not in old test though...)\n+      }\n+      \"be 501 for src and dst not in the same backend\" >> todo // What are we doing? make sure that we test all error conditions that the backend can return\n+    }\n+    \"DELETE\" >> {\n+      \"be 404 for missing backend\" >> todo // What are we doing?\n+      \"be 200 with existing file\" ! prop { (file: Path[Abs,File,Sandboxed], data: Vector[Data]) =>\n+        val path = posixCodec.printPath(file)\n+        val request = Request(uri = Uri(path = path), method = Method.DELETE)\n+        val filesystem = InMemState fromFiles (Map(file -> data))\n+        val response = service(filesystem)(request).run\n+        response.status must_== Status.Ok\n+        // Check if file was actually deleted\n+      }\n+      \"be 200 with existing dir\" ! prop { (dir: Path[Abs,Dir,Sandboxed], sampleFiles: Map[String, Vector[Data]]) =>\n+        val dirPath = posixCodec.printPath(dir)\n+        val fileMapping = sampleFiles.map{ case (fileName,data) => (dir </> file(fileName), data)}\n+        val request = Request(uri = Uri(path = dirPath), method = Method.DELETE)\n+        val filesystem = InMemState fromFiles fileMapping\n+        val response = service(filesystem)(request).run\n+        response.status must_== Status.Ok\n+        // Check if dir was actually deleted\n+      }\n+      \"be 200 with missing file (idempotency)\" ! prop { (file: Path[Abs,File,Sandboxed], data: Vector[Data]) =>","path":"web/src/test/scala/quasar/api/services/DataServiceSpec.scala","position":495,"original_position":495,"commit_id":"ad5a11e67135a28517f67ec92cb09f20efa26af2","original_commit_id":"ad5a11e67135a28517f67ec92cb09f20efa26af2","user":{"login":"wemrysi","id":32185,"avatar_url":"https://avatars.githubusercontent.com/u/32185?v=3","gravatar_id":"","url":"https://api.github.com/users/wemrysi","html_url":"https://github.com/wemrysi","followers_url":"https://api.github.com/users/wemrysi/followers","following_url":"https://api.github.com/users/wemrysi/following{/other_user}","gists_url":"https://api.github.com/users/wemrysi/gists{/gist_id}","starred_url":"https://api.github.com/users/wemrysi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wemrysi/subscriptions","organizations_url":"https://api.github.com/users/wemrysi/orgs","repos_url":"https://api.github.com/users/wemrysi/repos","events_url":"https://api.github.com/users/wemrysi/events{/privacy}","received_events_url":"https://api.github.com/users/wemrysi/received_events","type":"User","site_admin":false},"body":"The spec says:\r\n\r\n\"Methods can also have the property of 'idempotence' in that (aside from error or expiration issues) the **side-effects** of N > 0 identical requests is the same as for a single request.\"\r\n\r\nto me, this doesn't imply that identical requests must always result in the same response.","created_at":"2015-10-28T19:57:51Z","updated_at":"2015-10-28T19:57:51Z","html_url":"https://github.com/wemrysi/quasar/pull/2#discussion_r43309113","pull_request_url":"https://api.github.com/repos/wemrysi/quasar/pulls/2","_links":{"self":{"href":"https://api.github.com/repos/wemrysi/quasar/pulls/comments/43309113"},"html":{"href":"https://github.com/wemrysi/quasar/pull/2#discussion_r43309113"},"pull_request":{"href":"https://api.github.com/repos/wemrysi/quasar/pulls/2"}}},"pull_request":{"url":"https://api.github.com/repos/wemrysi/quasar/pulls/2","id":49043134,"html_url":"https://github.com/wemrysi/quasar/pull/2","diff_url":"https://github.com/wemrysi/quasar/pull/2.diff","patch_url":"https://github.com/wemrysi/quasar/pull/2.patch","issue_url":"https://api.github.com/repos/wemrysi/quasar/issues/2","number":2,"state":"open","locked":false,"title":"Wp/sd 983 web api","user":{"login":"jedesah","id":1780116,"avatar_url":"https://avatars.githubusercontent.com/u/1780116?v=3","gravatar_id":"","url":"https://api.github.com/users/jedesah","html_url":"https://github.com/jedesah","followers_url":"https://api.github.com/users/jedesah/followers","following_url":"https://api.github.com/users/jedesah/following{/other_user}","gists_url":"https://api.github.com/users/jedesah/gists{/gist_id}","starred_url":"https://api.github.com/users/jedesah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jedesah/subscriptions","organizations_url":"https://api.github.com/users/jedesah/orgs","repos_url":"https://api.github.com/users/jedesah/repos","events_url":"https://api.github.com/users/jedesah/events{/privacy}","received_events_url":"https://api.github.com/users/jedesah/received_events","type":"User","site_admin":false},"body":"","created_at":"2015-10-28T17:54:25Z","updated_at":"2015-10-28T19:57:51Z","closed_at":null,"merged_at":null,"merge_commit_sha":"dcc58cfb81ece4ab6c8eceb262ddcfc70589f614","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/wemrysi/quasar/pulls/2/commits","review_comments_url":"https://api.github.com/repos/wemrysi/quasar/pulls/2/comments","review_comment_url":"https://api.github.com/repos/wemrysi/quasar/pulls/comments{/number}","comments_url":"https://api.github.com/repos/wemrysi/quasar/issues/2/comments","statuses_url":"https://api.github.com/repos/wemrysi/quasar/statuses/ad5a11e67135a28517f67ec92cb09f20efa26af2","head":{"label":"jedesah:wp/SD-983_web-api","ref":"wp/SD-983_web-api","sha":"ad5a11e67135a28517f67ec92cb09f20efa26af2","user":{"login":"jedesah","id":1780116,"avatar_url":"https://avatars.githubusercontent.com/u/1780116?v=3","gravatar_id":"","url":"https://api.github.com/users/jedesah","html_url":"https://github.com/jedesah","followers_url":"https://api.github.com/users/jedesah/followers","following_url":"https://api.github.com/users/jedesah/following{/other_user}","gists_url":"https://api.github.com/users/jedesah/gists{/gist_id}","starred_url":"https://api.github.com/users/jedesah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jedesah/subscriptions","organizations_url":"https://api.github.com/users/jedesah/orgs","repos_url":"https://api.github.com/users/jedesah/repos","events_url":"https://api.github.com/users/jedesah/events{/privacy}","received_events_url":"https://api.github.com/users/jedesah/received_events","type":"User","site_admin":false},"repo":{"id":39122146,"name":"Quasar","full_name":"jedesah/Quasar","owner":{"login":"jedesah","id":1780116,"avatar_url":"https://avatars.githubusercontent.com/u/1780116?v=3","gravatar_id":"","url":"https://api.github.com/users/jedesah","html_url":"https://github.com/jedesah","followers_url":"https://api.github.com/users/jedesah/followers","following_url":"https://api.github.com/users/jedesah/following{/other_user}","gists_url":"https://api.github.com/users/jedesah/gists{/gist_id}","starred_url":"https://api.github.com/users/jedesah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jedesah/subscriptions","organizations_url":"https://api.github.com/users/jedesah/orgs","repos_url":"https://api.github.com/users/jedesah/repos","events_url":"https://api.github.com/users/jedesah/events{/privacy}","received_events_url":"https://api.github.com/users/jedesah/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/jedesah/Quasar","description":"The engine for SlamData","fork":true,"url":"https://api.github.com/repos/jedesah/Quasar","forks_url":"https://api.github.com/repos/jedesah/Quasar/forks","keys_url":"https://api.github.com/repos/jedesah/Quasar/keys{/key_id}","collaborators_url":"https://api.github.com/repos/jedesah/Quasar/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/jedesah/Quasar/teams","hooks_url":"https://api.github.com/repos/jedesah/Quasar/hooks","issue_events_url":"https://api.github.com/repos/jedesah/Quasar/issues/events{/number}","events_url":"https://api.github.com/repos/jedesah/Quasar/events","assignees_url":"https://api.github.com/repos/jedesah/Quasar/assignees{/user}","branches_url":"https://api.github.com/repos/jedesah/Quasar/branches{/branch}","tags_url":"https://api.github.com/repos/jedesah/Quasar/tags","blobs_url":"https://api.github.com/repos/jedesah/Quasar/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/jedesah/Quasar/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/jedesah/Quasar/git/refs{/sha}","trees_url":"https://api.github.com/repos/jedesah/Quasar/git/trees{/sha}","statuses_url":"https://api.github.com/repos/jedesah/Quasar/statuses/{sha}","languages_url":"https://api.github.com/repos/jedesah/Quasar/languages","stargazers_url":"https://api.github.com/repos/jedesah/Quasar/stargazers","contributors_url":"https://api.github.com/repos/jedesah/Quasar/contributors","subscribers_url":"https://api.github.com/repos/jedesah/Quasar/subscribers","subscription_url":"https://api.github.com/repos/jedesah/Quasar/subscription","commits_url":"https://api.github.com/repos/jedesah/Quasar/commits{/sha}","git_commits_url":"https://api.github.com/repos/jedesah/Quasar/git/commits{/sha}","comments_url":"https://api.github.com/repos/jedesah/Quasar/comments{/number}","issue_comment_url":"https://api.github.com/repos/jedesah/Quasar/issues/comments{/number}","contents_url":"https://api.github.com/repos/jedesah/Quasar/contents/{+path}","compare_url":"https://api.github.com/repos/jedesah/Quasar/compare/{base}...{head}","merges_url":"https://api.github.com/repos/jedesah/Quasar/merges","archive_url":"https://api.github.com/repos/jedesah/Quasar/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/jedesah/Quasar/downloads","issues_url":"https://api.github.com/repos/jedesah/Quasar/issues{/number}","pulls_url":"https://api.github.com/repos/jedesah/Quasar/pulls{/number}","milestones_url":"https://api.github.com/repos/jedesah/Quasar/milestones{/number}","notifications_url":"https://api.github.com/repos/jedesah/Quasar/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/jedesah/Quasar/labels{/name}","releases_url":"https://api.github.com/repos/jedesah/Quasar/releases{/id}","created_at":"2015-07-15T07:12:13Z","updated_at":"2015-09-14T20:57:00Z","pushed_at":"2015-10-28T17:52:36Z","git_url":"git://github.com/jedesah/Quasar.git","ssh_url":"git@github.com:jedesah/Quasar.git","clone_url":"https://github.com/jedesah/Quasar.git","svn_url":"https://github.com/jedesah/Quasar","homepage":null,"size":7585,"stargazers_count":0,"watchers_count":0,"language":"Scala","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"wemrysi:wip/SD-983","ref":"wip/SD-983","sha":"a9bf843317068281f6b0764ebe4546fed34a2de1","user":{"login":"wemrysi","id":32185,"avatar_url":"https://avatars.githubusercontent.com/u/32185?v=3","gravatar_id":"","url":"https://api.github.com/users/wemrysi","html_url":"https://github.com/wemrysi","followers_url":"https://api.github.com/users/wemrysi/followers","following_url":"https://api.github.com/users/wemrysi/following{/other_user}","gists_url":"https://api.github.com/users/wemrysi/gists{/gist_id}","starred_url":"https://api.github.com/users/wemrysi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wemrysi/subscriptions","organizations_url":"https://api.github.com/users/wemrysi/orgs","repos_url":"https://api.github.com/users/wemrysi/repos","events_url":"https://api.github.com/users/wemrysi/events{/privacy}","received_events_url":"https://api.github.com/users/wemrysi/received_events","type":"User","site_admin":false},"repo":{"id":40374964,"name":"quasar","full_name":"wemrysi/quasar","owner":{"login":"wemrysi","id":32185,"avatar_url":"https://avatars.githubusercontent.com/u/32185?v=3","gravatar_id":"","url":"https://api.github.com/users/wemrysi","html_url":"https://github.com/wemrysi","followers_url":"https://api.github.com/users/wemrysi/followers","following_url":"https://api.github.com/users/wemrysi/following{/other_user}","gists_url":"https://api.github.com/users/wemrysi/gists{/gist_id}","starred_url":"https://api.github.com/users/wemrysi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wemrysi/subscriptions","organizations_url":"https://api.github.com/users/wemrysi/orgs","repos_url":"https://api.github.com/users/wemrysi/repos","events_url":"https://api.github.com/users/wemrysi/events{/privacy}","received_events_url":"https://api.github.com/users/wemrysi/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/wemrysi/quasar","description":"The engine for SlamData","fork":true,"url":"https://api.github.com/repos/wemrysi/quasar","forks_url":"https://api.github.com/repos/wemrysi/quasar/forks","keys_url":"https://api.github.com/repos/wemrysi/quasar/keys{/key_id}","collaborators_url":"https://api.github.com/repos/wemrysi/quasar/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/wemrysi/quasar/teams","hooks_url":"https://api.github.com/repos/wemrysi/quasar/hooks","issue_events_url":"https://api.github.com/repos/wemrysi/quasar/issues/events{/number}","events_url":"https://api.github.com/repos/wemrysi/quasar/events","assignees_url":"https://api.github.com/repos/wemrysi/quasar/assignees{/user}","branches_url":"https://api.github.com/repos/wemrysi/quasar/branches{/branch}","tags_url":"https://api.github.com/repos/wemrysi/quasar/tags","blobs_url":"https://api.github.com/repos/wemrysi/quasar/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/wemrysi/quasar/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/wemrysi/quasar/git/refs{/sha}","trees_url":"https://api.github.com/repos/wemrysi/quasar/git/trees{/sha}","statuses_url":"https://api.github.com/repos/wemrysi/quasar/statuses/{sha}","languages_url":"https://api.github.com/repos/wemrysi/quasar/languages","stargazers_url":"https://api.github.com/repos/wemrysi/quasar/stargazers","contributors_url":"https://api.github.com/repos/wemrysi/quasar/contributors","subscribers_url":"https://api.github.com/repos/wemrysi/quasar/subscribers","subscription_url":"https://api.github.com/repos/wemrysi/quasar/subscription","commits_url":"https://api.github.com/repos/wemrysi/quasar/commits{/sha}","git_commits_url":"https://api.github.com/repos/wemrysi/quasar/git/commits{/sha}","comments_url":"https://api.github.com/repos/wemrysi/quasar/comments{/number}","issue_comment_url":"https://api.github.com/repos/wemrysi/quasar/issues/comments{/number}","contents_url":"https://api.github.com/repos/wemrysi/quasar/contents/{+path}","compare_url":"https://api.github.com/repos/wemrysi/quasar/compare/{base}...{head}","merges_url":"https://api.github.com/repos/wemrysi/quasar/merges","archive_url":"https://api.github.com/repos/wemrysi/quasar/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/wemrysi/quasar/downloads","issues_url":"https://api.github.com/repos/wemrysi/quasar/issues{/number}","pulls_url":"https://api.github.com/repos/wemrysi/quasar/pulls{/number}","milestones_url":"https://api.github.com/repos/wemrysi/quasar/milestones{/number}","notifications_url":"https://api.github.com/repos/wemrysi/quasar/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/wemrysi/quasar/labels{/name}","releases_url":"https://api.github.com/repos/wemrysi/quasar/releases{/id}","created_at":"2015-08-07T18:29:08Z","updated_at":"2015-09-14T20:43:00Z","pushed_at":"2015-10-28T17:54:25Z","git_url":"git://github.com/wemrysi/quasar.git","ssh_url":"git@github.com:wemrysi/quasar.git","clone_url":"https://github.com/wemrysi/quasar.git","svn_url":"https://github.com/wemrysi/quasar","homepage":null,"size":7554,"stargazers_count":0,"watchers_count":0,"language":"Scala","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/wemrysi/quasar/pulls/2"},"html":{"href":"https://github.com/wemrysi/quasar/pull/2"},"issue":{"href":"https://api.github.com/repos/wemrysi/quasar/issues/2"},"comments":{"href":"https://api.github.com/repos/wemrysi/quasar/issues/2/comments"},"review_comments":{"href":"https://api.github.com/repos/wemrysi/quasar/pulls/2/comments"},"review_comment":{"href":"https://api.github.com/repos/wemrysi/quasar/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/wemrysi/quasar/pulls/2/commits"},"statuses":{"href":"https://api.github.com/repos/wemrysi/quasar/statuses/ad5a11e67135a28517f67ec92cb09f20efa26af2"}}}}