Skip to content

Commit daf038c

Browse files
committed
Enhance Test HTTP Server
- Add support for URL parameters. - Add status code URLs for /xxx to allow specifiing specific codes including setting the Location header for 3xx codes.
1 parent 03fcf08 commit daf038c

File tree

1 file changed

+192
-47
lines changed

1 file changed

+192
-47
lines changed

Tests/Foundation/HTTPServer.swift

Lines changed: 192 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -483,27 +483,53 @@ class _HTTPServer {
483483

484484
struct _HTTPRequest {
485485
enum Method : String {
486+
case HEAD
486487
case GET
487488
case POST
488489
case PUT
490+
case DELETE
489491
}
490-
let method: Method
491-
let uri: String
492-
let body: String
493-
var messageBody: String?
494-
var messageData: Data?
495-
let headers: [String]
496492

497493
enum Error: Swift.Error {
494+
case invalidURI
495+
case invalidMethod
498496
case headerEndNotFound
499497
}
500-
498+
499+
let method: Method
500+
let uri: String
501+
private(set) var headers: [String] = []
502+
private(set) var parameters: [String: String] = [:]
503+
var messageBody: String?
504+
var messageData: Data?
505+
506+
501507
public init(header: String) throws {
502-
headers = header.components(separatedBy: _HTTPUtils.CRLF)
503-
let action = headers[0]
504-
method = Method(rawValue: action.components(separatedBy: " ")[0])!
505-
uri = action.components(separatedBy: " ")[1]
506-
body = ""
508+
self.headers = header.components(separatedBy: _HTTPUtils.CRLF)
509+
guard headers.count > 0 else {
510+
throw Error.invalidURI
511+
}
512+
let uriParts = headers[0].components(separatedBy: " ")
513+
guard uriParts.count > 2, let methodName = Method(rawValue: uriParts[0]) else {
514+
throw Error.invalidMethod
515+
}
516+
method = methodName
517+
let params = uriParts[1].split(separator: "?", maxSplits: 1, omittingEmptySubsequences: true)
518+
if params.count > 1 {
519+
for arg in params[1].split(separator: "&", omittingEmptySubsequences: true) {
520+
let keyValue = arg.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false)
521+
guard !keyValue.isEmpty else { continue }
522+
guard let key = keyValue[0].removingPercentEncoding else {
523+
throw Error.invalidURI
524+
}
525+
guard let value = (keyValue.count > 1) ? keyValue[1].removingPercentEncoding : "" else {
526+
throw Error.invalidURI
527+
}
528+
self.parameters[key] = value
529+
}
530+
}
531+
532+
self.uri = String(params[0])
507533
}
508534

509535
public func getCommaSeparatedHeaders() -> String {
@@ -524,32 +550,83 @@ struct _HTTPRequest {
524550
}
525551
return nil
526552
}
553+
554+
public func headersAsJSON() throws -> Data {
555+
var headerDict: [String: String] = [:]
556+
for header in headers {
557+
if header.hasPrefix(method.rawValue) {
558+
headerDict["uri"] = header
559+
continue
560+
}
561+
let parts = header.components(separatedBy: ":")
562+
if parts.count > 1 {
563+
headerDict[parts[0]] = parts[1].trimmingCharacters(in: CharacterSet(charactersIn: " "))
564+
}
565+
}
566+
return try JSONSerialization.data(withJSONObject: headerDict, options: .sortedKeys)
567+
}
527568
}
528569

529570
struct _HTTPResponse {
530571
enum Response: Int {
531572
case OK = 200
532-
case REDIRECT = 302
533-
case NOTFOUND = 404
573+
case FOUND = 302
574+
case BAD_REQUEST = 400
575+
case NOT_FOUND = 404
534576
case METHOD_NOT_ALLOWED = 405
577+
case SERVER_ERROR = 500
535578
}
536-
private let responseCode: Response
537-
private let headers: String
579+
580+
581+
private let responseCode: Int
582+
private var headers: [String]
538583
public let bodyData: Data
539584

540-
public init(response: Response, headers: String = _HTTPUtils.EMPTY, bodyData: Data) {
541-
self.responseCode = response
585+
public init(responseCode: Int, headers: [String] = [], bodyData: Data) {
586+
self.responseCode = responseCode
542587
self.headers = headers
543588
self.bodyData = bodyData
589+
590+
for header in headers {
591+
if header.lowercased().hasPrefix("content-length") {
592+
return
593+
}
594+
}
595+
self.headers.append("Content-Length: \(bodyData.count)")
596+
}
597+
598+
public init(response: Response, headers: [String] = [], bodyData: Data = Data()) {
599+
self.init(responseCode: response.rawValue, headers: headers, bodyData: bodyData)
600+
}
601+
602+
public init(response: Response, headers: String = _HTTPUtils.EMPTY, bodyData: Data) {
603+
let headers = headers.split(separator: "\r\n").map { String($0) }
604+
self.init(responseCode: response.rawValue, headers: headers, bodyData: bodyData)
544605
}
545606

546-
public init(response: Response, headers: String = _HTTPUtils.EMPTY, body: String) {
547-
self.init(response: response, headers: headers, bodyData: body.data(using: .utf8)!)
607+
public init(response: Response, headers: String = _HTTPUtils.EMPTY, body: String) throws {
608+
guard let data = body.data(using: .utf8) else {
609+
throw InternalServerError.badBody
610+
}
611+
self.init(response: response, headers: headers, bodyData: data)
612+
}
613+
614+
public init(responseCode: Int, headers: [String] = [], body: String) throws {
615+
guard let data = body.data(using: .utf8) else {
616+
throw InternalServerError.badBody
617+
}
618+
self.init(responseCode: responseCode, headers: headers, bodyData: data)
548619
}
549620

550621
public var header: String {
551-
let statusLine = _HTTPUtils.VERSION + _HTTPUtils.SPACE + "\(responseCode.rawValue)" + _HTTPUtils.SPACE + "\(responseCode)"
552-
return statusLine + (headers != _HTTPUtils.EMPTY ? _HTTPUtils.CRLF + headers : _HTTPUtils.EMPTY) + _HTTPUtils.CRLF2
622+
let responseCodeName = HTTPURLResponse.localizedString(forStatusCode: responseCode)
623+
let statusLine = _HTTPUtils.VERSION + _HTTPUtils.SPACE + "\(responseCode)" + _HTTPUtils.SPACE + "\(responseCodeName)"
624+
let header = headers.joined(separator: "\r\n")
625+
return statusLine + (header != _HTTPUtils.EMPTY ? _HTTPUtils.CRLF + header : _HTTPUtils.EMPTY) + _HTTPUtils.CRLF2
626+
}
627+
628+
mutating func addHeader(_ header: String) {
629+
headers.append(header)
553630
}
554631
}
555632

@@ -593,57 +670,93 @@ public class TestURLSessionServer {
593670
responseData.append(content)
594671
try httpServer.tcpSocket.writeRawData(responseData)
595672
} else {
596-
try httpServer.respond(with: _HTTPResponse(response: .NOTFOUND, body: "Not Found"))
673+
try httpServer.respond(with: _HTTPResponse(response: .NOT_FOUND, body: "Not Found"))
597674
}
598675
} else if req.uri.hasPrefix("/auth") {
599676
try httpServer.respondWithAuthResponse(request: req)
600677
} else if req.uri.hasPrefix("/unauthorized") {
601678
try httpServer.respondWithUnauthorizedHeader()
602679
} else {
603-
if req.method == .GET || req.method == .POST || req.method == .PUT {
604-
try httpServer.respond(with: getResponse(request: req))
605-
}
606-
else {
607-
try httpServer.respond(with: _HTTPResponse(response: .METHOD_NOT_ALLOWED, body: "Method not allowed"))
608-
}
680+
try httpServer.respond(with: getResponse(request: req))
609681
}
610682
}
611683

612-
func getResponse(request: _HTTPRequest) -> _HTTPResponse {
684+
func getResponse(request: _HTTPRequest) throws -> _HTTPResponse {
685+
686+
func headersAsJSONResponse() throws -> _HTTPResponse {
687+
return try _HTTPResponse(response: .OK, headers: ["Content-Type: application/json"], bodyData: request.headersAsJSON())
688+
}
689+
613690
let uri = request.uri
691+
if uri == "/jsonBody" {
692+
return try headersAsJSONResponse()
693+
}
694+
695+
if uri == "/head" {
696+
guard request.method == .HEAD else { return try _HTTPResponse(response: .METHOD_NOT_ALLOWED, body: "Method not allowed") }
697+
return try headersAsJSONResponse()
698+
}
699+
700+
if uri == "/get" {
701+
guard request.method == .GET else { return try _HTTPResponse(response: .METHOD_NOT_ALLOWED, body: "Method not allowed") }
702+
return try headersAsJSONResponse()
703+
}
704+
705+
if uri == "/put" {
706+
guard request.method == .PUT else { return try _HTTPResponse(response: .METHOD_NOT_ALLOWED, body: "Method not allowed") }
707+
return try headersAsJSONResponse()
708+
}
709+
710+
if uri == "/post" {
711+
guard request.method == .POST else { return try _HTTPResponse(response: .METHOD_NOT_ALLOWED, body: "Method not allowed") }
712+
return try headersAsJSONResponse()
713+
}
714+
715+
if uri == "/delete" {
716+
guard request.method == .DELETE else { return try _HTTPResponse(response: .METHOD_NOT_ALLOWED, body: "Method not allowed") }
717+
return try headersAsJSONResponse()
718+
}
719+
614720
if uri == "/upload" {
615-
let text = "Upload completed!"
616-
return _HTTPResponse(response: .OK, headers: "Content-Length: \(text.data(using: .utf8)!.count)", body: text)
721+
if let contentLength = request.getHeader(for: "content-length") {
722+
let text = "Upload completed!, Content-Length: \(contentLength)"
723+
return try _HTTPResponse(response: .OK, body: text)
724+
}
725+
if let te = request.getHeader(for: "transfer-encoding"), te == "chunked" {
726+
return try _HTTPResponse(response: .OK, body: "Received Chunked request")
727+
} else {
728+
return try _HTTPResponse(response: .BAD_REQUEST, body: "Missing Content-Length")
729+
}
617730
}
618731

619732
if uri == "/country.txt" {
620733
let text = capitals[String(uri.dropFirst())]!
621-
return _HTTPResponse(response: .OK, headers: "Content-Length: \(text.data(using: .utf8)!.count)", body: text)
734+
return try _HTTPResponse(response: .OK, body: text)
622735
}
623736

624737
if uri == "/requestHeaders" {
625738
let text = request.getCommaSeparatedHeaders()
626-
return _HTTPResponse(response: .OK, headers: "Content-Length: \(text.data(using: .utf8)!.count)", body: text)
739+
return try _HTTPResponse(response: .OK, body: text)
627740
}
628741

629742
if uri == "/emptyPost" {
630-
if request.body.count == 0 && request.getHeader(for: "Content-Type") == nil {
631-
return _HTTPResponse(response: .OK, body: "")
743+
if request.getHeader(for: "Content-Type") == nil {
744+
return try _HTTPResponse(response: .OK, body: "")
632745
}
633-
return _HTTPResponse(response: .NOTFOUND, body: "")
746+
return try _HTTPResponse(response: .NOT_FOUND, body: "")
634747
}
635748

636749
if uri == "/requestCookies" {
637-
return _HTTPResponse(response: .OK, headers: "Set-Cookie: fr=anjd&232; Max-Age=7776000; path=/\r\nSet-Cookie: nm=sddf&232; Max-Age=7776000; path=/; domain=.swift.org; secure; httponly\r\n", body: "")
750+
return try _HTTPResponse(response: .OK, headers: "Set-Cookie: fr=anjd&232; Max-Age=7776000; path=/\r\nSet-Cookie: nm=sddf&232; Max-Age=7776000; path=/; domain=.swift.org; secure; httponly\r\n", body: "")
638751
}
639752

640753
if uri == "/echoHeaders" {
641754
let text = request.getCommaSeparatedHeaders()
642-
return _HTTPResponse(response: .OK, headers: "Content-Length: \(text.data(using: .utf8)!.count)", body: text)
755+
return try _HTTPResponse(response: .OK, headers: "Content-Length: \(text.data(using: .utf8)!.count)", body: text)
643756
}
644757

645758
if uri == "/redirectToEchoHeaders" {
646-
return _HTTPResponse(response: .REDIRECT, headers: "location: /echoHeaders\r\nSet-Cookie: redirect=true; Max-Age=7776000; path=/", body: "")
759+
return try _HTTPResponse(response: .FOUND, headers: "location: /echoHeaders\r\nSet-Cookie: redirect=true; Max-Age=7776000; path=/", body: "")
647760
}
648761

649762
if uri == "/UnitedStates" {
@@ -654,7 +767,7 @@ public class TestURLSessionServer {
654767
let port = host.components(separatedBy: ":")[1]
655768
let newPort = Int(port)! + 1
656769
let newHost = ip + ":" + String(newPort)
657-
let httpResponse = _HTTPResponse(response: .REDIRECT, headers: "Location: http://\(newHost + "/" + value)", body: text)
770+
let httpResponse = try _HTTPResponse(response: .FOUND, headers: "Location: http://\(newHost + "/" + value)", body: text)
658771
return httpResponse
659772
}
660773

@@ -680,26 +793,26 @@ public class TestURLSessionServer {
680793
<!ELEMENT real (#PCDATA)> <!-- Contents should represent a floating point number matching ("+" | "-")? d+ ("."d*)? ("E" ("+" | "-") d+)? where d is a digit 0-9. -->
681794
<!ELEMENT integer (#PCDATA)> <!-- Contents should represent a (possibly signed) integer number in base 10 -->
682795
"""
683-
return _HTTPResponse(response: .OK, body: dtd)
796+
return try _HTTPResponse(response: .OK, body: dtd)
684797
}
685798

686799
if uri == "/UnitedKingdom" {
687800
let value = capitals[String(uri.dropFirst())]!
688801
let text = request.getCommaSeparatedHeaders()
689802
//Response header with only path to the location to redirect.
690-
let httpResponse = _HTTPResponse(response: .REDIRECT, headers: "Location: \(value)", body: text)
803+
let httpResponse = try _HTTPResponse(response: .FOUND, headers: "Location: \(value)", body: text)
691804
return httpResponse
692805
}
693806

694807
if uri == "/echo" {
695-
return _HTTPResponse(response: .OK, body: request.messageBody ?? request.body)
808+
return try _HTTPResponse(response: .OK, body: request.messageBody ?? "")
696809
}
697810

698811
if uri == "/redirect-with-default-port" {
699812
let text = request.getCommaSeparatedHeaders()
700813
let host = request.headers[1].components(separatedBy: " ")[1]
701814
let ip = host.components(separatedBy: ":")[0]
702-
let httpResponse = _HTTPResponse(response: .REDIRECT, headers: "Location: http://\(ip)/redirected-with-default-port", body: text)
815+
let httpResponse = try _HTTPResponse(response: .FOUND, headers: "Location: http://\(ip)/redirected-with-default-port", body: text)
703816
return httpResponse
704817

705818
}
@@ -716,11 +829,42 @@ public class TestURLSessionServer {
716829
bodyData: helloWorld)
717830
}
718831

832+
// Look for /xxx where xxx is a 3digit HTTP code
833+
if uri.hasPrefix("/") && uri.count == 4, let code = Int(String(uri.dropFirst())), code > 0 && code < 1000 {
834+
return try statusCodeResponse(forRequest: request, statusCode: code)
835+
}
836+
719837
guard let capital = capitals[String(uri.dropFirst())] else {
720-
return _HTTPResponse(response: .NOTFOUND, body: "Not Found")
838+
return _HTTPResponse(response: .NOT_FOUND)
839+
}
840+
return try _HTTPResponse(response: .OK, body: capital)
841+
}
842+
843+
private func statusCodeResponse(forRequest request: _HTTPRequest, statusCode: Int) throws -> _HTTPResponse {
844+
guard let bodyData = try? request.headersAsJSON() else {
845+
return try _HTTPResponse(response: .SERVER_ERROR, body: "Cant convert headers to JSON object")
846+
}
847+
848+
var response: _HTTPResponse
849+
switch statusCode {
850+
case 300...303, 305...308:
851+
let location = request.parameters["location"] ?? "/" + request.method.rawValue.lowercased()
852+
let body = "Redirecting to \(request.method) \(location)"
853+
let headers = ["Content-Type: test/plain", "Location: \(location)"]
854+
response = try _HTTPResponse(responseCode: statusCode, headers: headers, body: body)
855+
856+
case 401:
857+
let headers = ["Content-Type: application/json", "Content-Length: \(bodyData.count)"]
858+
response = _HTTPResponse(responseCode: statusCode, headers: headers, bodyData: bodyData)
859+
response.addHeader("WWW-Authenticate: Basic realm=\"Fake Relam\"")
860+
861+
default:
862+
let headers = ["Content-Type: application/json", "Content-Length: \(bodyData.count)"]
863+
response = _HTTPResponse(responseCode: statusCode, headers: headers, bodyData: bodyData)
864+
break
721865
}
722-
return _HTTPResponse(response: .OK, body: capital)
723866

867+
return response
724868
}
725869
}
726870

@@ -744,6 +888,7 @@ extension ServerError : CustomStringConvertible {
744888
enum InternalServerError : Error {
745889
case socketAlreadyClosed
746890
case requestTooShort
891+
case badBody
747892
}
748893

749894
public class ServerSemaphore {

0 commit comments

Comments
 (0)