mirror of
https://github.com/lightbend/config.git
synced 2025-01-15 23:01:05 +08:00
Add HttpTest for testing URL parsing and error handling
This commit is contained in:
parent
7f19e46d1c
commit
52ba150a9a
@ -14,6 +14,7 @@ import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
147
config/src/test/scala/com/typesafe/config/impl/HttpTest.scala
Normal file
147
config/src/test/scala/com/typesafe/config/impl/HttpTest.scala
Normal file
@ -0,0 +1,147 @@
|
||||
package com.typesafe.config.impl
|
||||
|
||||
import org.junit.Assert._
|
||||
import org.junit._
|
||||
import org.junit.BeforeClass
|
||||
import java.net.URL
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigParseOptions
|
||||
import com.typesafe.config.ConfigSyntax
|
||||
import com.typesafe.config.ConfigException
|
||||
|
||||
class HttpTest extends TestUtils {
|
||||
import HttpTest._
|
||||
|
||||
private def foreachSyntax(body: Option[ConfigSyntax] => Unit): Unit = {
|
||||
for (syntax <- Seq(Some(ConfigSyntax.JSON), Some(ConfigSyntax.CONF), Some(ConfigSyntax.PROPERTIES), None))
|
||||
body(syntax)
|
||||
}
|
||||
|
||||
private def foreachSyntaxOptions(body: ConfigParseOptions => Unit): Unit = foreachSyntax { syntaxOption =>
|
||||
val options = syntaxOption map { syntax =>
|
||||
ConfigParseOptions.defaults.setSyntax(syntax)
|
||||
} getOrElse {
|
||||
ConfigParseOptions.defaults()
|
||||
}
|
||||
|
||||
body(options)
|
||||
}
|
||||
|
||||
def url(path: String): URL = new URL(s"$baseUrl/$path")
|
||||
|
||||
@Test
|
||||
def parseEmpty(): Unit = {
|
||||
foreachSyntaxOptions { options =>
|
||||
val conf = ConfigFactory.parseURL(url("empty"), options)
|
||||
assertTrue("empty conf was parsed", conf.root.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
def parseFooIs42(): Unit = {
|
||||
foreachSyntaxOptions { options =>
|
||||
val conf = ConfigFactory.parseURL(url("foo"), options)
|
||||
assertEquals(42, conf.getInt("foo"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
def notFoundThrowsIO(): Unit = {
|
||||
val e = intercept[ConfigException.IO] {
|
||||
ConfigFactory.parseURL(url("notfound"), ConfigParseOptions.defaults().setAllowMissing(false))
|
||||
}
|
||||
assertTrue(s"expected different exception for notfound, got $e", e.getMessage.contains("/notfound"))
|
||||
}
|
||||
|
||||
@Test
|
||||
def internalErrorThrowsBroken(): Unit = {
|
||||
val e = intercept[ConfigException.BugOrBroken] {
|
||||
ConfigFactory.parseURL(url("error"), ConfigParseOptions.defaults().setAllowMissing(false))
|
||||
}
|
||||
assertTrue(s"expected different exception for error url, got $e", e.getMessage.contains("/error"))
|
||||
}
|
||||
|
||||
@Test
|
||||
def notFoundDoesNotThrowIfAllowingMissing(): Unit = {
|
||||
val conf = ConfigFactory.parseURL(url("notfound"), ConfigParseOptions.defaults().setAllowMissing(true))
|
||||
assertEquals(0, conf.root.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
def internalErrorThrowsEvenIfAllowingMissing(): Unit = {
|
||||
val e = intercept[ConfigException.BugOrBroken] {
|
||||
ConfigFactory.parseURL(url("error"), ConfigParseOptions.defaults().setAllowMissing(true))
|
||||
}
|
||||
assertTrue(s"expected different exception for error url when allowing missing, got $e", e.getMessage.contains("/error"))
|
||||
}
|
||||
|
||||
@Test
|
||||
def relativeInclude(): Unit = {
|
||||
val conf = ConfigFactory.parseURL(url("includes-a-friend"))
|
||||
assertEquals(42, conf.getInt("foo"))
|
||||
assertEquals(43, conf.getInt("bar"))
|
||||
}
|
||||
}
|
||||
|
||||
object HttpTest {
|
||||
import ToyHttp.{ Request, Response }
|
||||
|
||||
final val jsonContentType = "application/json"
|
||||
final val propertiesContentType = "text/x-java-properties"
|
||||
final val hoconContentType = "application/hocon"
|
||||
|
||||
private var server: Option[ToyHttp] = None
|
||||
|
||||
def port = server.map(_.port).getOrElse(throw new Exception("http server isn't running"))
|
||||
def baseUrl = s"http://127.0.0.1:$port"
|
||||
|
||||
private def handleThreeTypes(request: Request, json: String, props: String, hocon: String): Response = {
|
||||
request.headers.get("accept") match {
|
||||
case Some(`jsonContentType`) | None => Response(200, jsonContentType, json)
|
||||
case Some(`propertiesContentType`) => Response(200, propertiesContentType, props)
|
||||
case Some(`hoconContentType`) => Response(200, hoconContentType, hocon)
|
||||
case Some(other) => Response(500, "text/plain", s"bad content type '$other'")
|
||||
}
|
||||
}
|
||||
|
||||
private def handleRequest(request: Request): Response = {
|
||||
request.path match {
|
||||
case "/empty" =>
|
||||
handleThreeTypes(request, "{}", "", "")
|
||||
|
||||
case "/foo" | "/foo.conf" =>
|
||||
handleThreeTypes(request, "{ \"foo\" : 42 }", "foo:42", "foo=42")
|
||||
|
||||
case "/notfound" =>
|
||||
Response(404, "text/plain", "This is never found")
|
||||
|
||||
case "/error" =>
|
||||
Response(500, "text/plain", "This is always an error")
|
||||
|
||||
case "/includes-a-friend" =>
|
||||
// currently, a suffix-less include like this will cause
|
||||
// us to search for foo.conf, foo.json, foo.properties, but
|
||||
// not load plain foo.
|
||||
Response(200, hoconContentType, """
|
||||
include "foo"
|
||||
include "foo/bar"
|
||||
""")
|
||||
|
||||
case "/foo/bar.conf" =>
|
||||
Response(200, hoconContentType, "{ bar = 43 }")
|
||||
|
||||
case path =>
|
||||
Response(404, "text/plain", s"Never heard of '$path'")
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
def startServer(): Unit = {
|
||||
server = Some(ToyHttp(handleRequest))
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
def stopServer(): Unit = {
|
||||
server.foreach(_.stop())
|
||||
}
|
||||
}
|
141
config/src/test/scala/com/typesafe/config/impl/ToyHttp.scala
Normal file
141
config/src/test/scala/com/typesafe/config/impl/ToyHttp.scala
Normal file
@ -0,0 +1,141 @@
|
||||
package com.typesafe.config.impl
|
||||
|
||||
import java.net.ServerSocket
|
||||
import java.net.InetSocketAddress
|
||||
import scala.annotation.tailrec
|
||||
import scala.util.control.NonFatal
|
||||
import java.net.Socket
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.EOFException
|
||||
import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.io.PrintWriter
|
||||
import java.io.OutputStreamWriter
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Date
|
||||
|
||||
// terrible http server that's good enough for our test suite
|
||||
final class ToyHttp(handler: ToyHttp.Request => ToyHttp.Response) {
|
||||
|
||||
import ToyHttp.{ Request, Response }
|
||||
|
||||
private final val serverSocket = new ServerSocket()
|
||||
serverSocket.bind(new InetSocketAddress("127.0.0.1", 0))
|
||||
final val port = serverSocket.getLocalPort
|
||||
private final val thread = new Thread(new Runnable() {
|
||||
override def run() =
|
||||
mainLoop();
|
||||
})
|
||||
|
||||
thread.setDaemon(true)
|
||||
thread.setName("ToyHttp")
|
||||
thread.start()
|
||||
|
||||
def stop(): Unit = {
|
||||
try serverSocket.close() catch { case e: IOException => }
|
||||
try thread.interrupt() catch { case NonFatal(e) => }
|
||||
thread.join()
|
||||
}
|
||||
|
||||
@tailrec
|
||||
private def mainLoop(): Unit = {
|
||||
val done = try {
|
||||
val socket = serverSocket.accept()
|
||||
|
||||
try handleRequest(socket)
|
||||
catch {
|
||||
case _: EOFException =>
|
||||
case e: IOException =>
|
||||
System.err.println(s"error handling http request: ${e.getClass.getName}: ${e.getMessage} ${e.getStackTrace.mkString("\n")}")
|
||||
}
|
||||
false
|
||||
} catch {
|
||||
case e: java.net.SocketException =>
|
||||
true
|
||||
}
|
||||
if (!done)
|
||||
mainLoop()
|
||||
}
|
||||
|
||||
private def handleRequest(socket: Socket): Unit = {
|
||||
val in = socket.getInputStream
|
||||
val out = socket.getOutputStream
|
||||
try {
|
||||
// HTTP requests look like this:
|
||||
// GET /path/here HTTP/1.0
|
||||
// SomeHeader: bar
|
||||
// OtherHeader: foo
|
||||
// \r\n
|
||||
val reader = new BufferedReader(new java.io.InputStreamReader(in))
|
||||
val path = parsePath(reader)
|
||||
val header = parseHeader(reader)
|
||||
//System.err.println(s"request path '$path' headers $header")
|
||||
val response = handler(Request(path, header))
|
||||
//System.err.println(s"response $response")
|
||||
sendResponse(out, response)
|
||||
} finally {
|
||||
in.close()
|
||||
out.close()
|
||||
}
|
||||
}
|
||||
|
||||
private def parseHeader(reader: BufferedReader): Map[String, String] = {
|
||||
|
||||
def readHeaders(sofar: Map[String, String]): Map[String, String] = {
|
||||
val line = reader.readLine()
|
||||
val colon = line.indexOf(':')
|
||||
if (colon > 0) {
|
||||
val name = line.substring(0, colon).toLowerCase()
|
||||
val value = line.substring(colon + 1).replaceAll("^[ \t]+", "")
|
||||
readHeaders(sofar + (name -> value))
|
||||
} else {
|
||||
sofar
|
||||
}
|
||||
}
|
||||
|
||||
readHeaders(Map.empty)
|
||||
}
|
||||
|
||||
private def parsePath(reader: BufferedReader): String = {
|
||||
val methodPathProto = reader.readLine().split(" +")
|
||||
val method = methodPathProto(0)
|
||||
val path = methodPathProto(1)
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
private def codeText(code: Int) = code match {
|
||||
case 200 => "OK"
|
||||
case 404 => "Not Found"
|
||||
case 500 => "Internal Server Error"
|
||||
case _ => throw new RuntimeException(s"add text for $code")
|
||||
}
|
||||
|
||||
private def sendResponse(out: OutputStream, response: Response): Unit = {
|
||||
//val stuff = new java.io.ByteArrayOutputStream
|
||||
//val writer = new PrintWriter(new OutputStreamWriter(stuff, StandardCharsets.UTF_8))
|
||||
val writer = new PrintWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))
|
||||
val dateFormat = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
|
||||
writer.append(s"HTTP/1.1 ${response.code} ${codeText(response.code)}\r\n")
|
||||
writer.append(s"Date: ${dateFormat.format(new Date)}\r\n")
|
||||
writer.append(s"Content-Type: ${response.contentType}; charset=utf-8\r\n")
|
||||
val bytes = response.body.getBytes("UTF-8")
|
||||
writer.append(s"Content-Length: $bytes\r\n")
|
||||
writer.append("\r\n")
|
||||
writer.append(response.body)
|
||||
writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
object ToyHttp {
|
||||
def apply(handler: Request => Response): ToyHttp =
|
||||
new ToyHttp(handler)
|
||||
|
||||
final case class Request(path: String, headers: Map[String, String])
|
||||
final case class Response(code: Int, contentType: String, body: String)
|
||||
}
|
Loading…
Reference in New Issue
Block a user