| 1 | #if os(macOS) |
| 2 | import Foundation |
| 3 | import XCTest |
| 4 | import RastrilloApple |
| 5 | import RastrilloTestSupport |
| 6 | |
| 7 | private actor Collected { |
| 8 | var lines: [String] = [] |
| 9 | func append(_ line: String) { lines.append(line) } |
| 10 | } |
| 11 | |
| 12 | /// A scripted stream must deliver exactly what a real one does for the same |
| 13 | /// bytes, or a test passes against framing no server produces. |
| 14 | final class StreamParityTests: XCTestCase { |
| 15 | private let bodies: [String: Data] = [ |
| 16 | "lf": Data("event: update\ndata: 1\n\n".utf8), |
| 17 | "crlf": Data("event: update\r\ndata: 2\r\n\r\n".utf8), |
| 18 | "cr": Data("event: update\rdata: 3\r\r".utf8), |
| 19 | "mixed": Data("a\r\nb\rc\n\r\n".utf8), |
| 20 | "unterminated": Data("data: 4\n\ndata: partial".utf8), |
| 21 | "bom": Data("\u{FEFF}data: 5\n\n".utf8), |
| 22 | ] |
| 23 | |
| 24 | private func server(_ bodies: [String: Data]) throws -> (Process, URL) { |
| 25 | let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) |
| 26 | try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) |
| 27 | for (name, body) in bodies { try body.write(to: directory.appendingPathComponent(name)) } |
| 28 | let process = Process() |
| 29 | process.executableURL = URL(fileURLWithPath: "/usr/bin/python3") |
| 30 | process.arguments = ["-u", "-c", #""" |
| 31 | import http.server, pathlib, sys |
| 32 | root = pathlib.Path(sys.argv[1]) |
| 33 | class Handler(http.server.BaseHTTPRequestHandler): |
| 34 | def log_message(self, *args): pass |
| 35 | def do_GET(self): |
| 36 | body = (root / self.path.strip('/')).read_bytes() |
| 37 | self.send_response(200) |
| 38 | self.send_header('Content-Type', 'text/event-stream') |
| 39 | self.send_header('Content-Length', str(len(body))) |
| 40 | self.end_headers() |
| 41 | self.wfile.write(body) |
| 42 | server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler) |
| 43 | print(server.server_port, flush=True) |
| 44 | server.serve_forever() |
| 45 | """#, directory.path] |
| 46 | let output = Pipe() |
| 47 | process.standardOutput = output |
| 48 | try process.run() |
| 49 | var port = Data() |
| 50 | while port.count < 12 { |
| 51 | let byte = output.fileHandleForReading.readData(ofLength: 1) |
| 52 | if byte.isEmpty || byte == Data([10]) { break } |
| 53 | port.append(byte) |
| 54 | } |
| 55 | guard let number = String(data: port, encoding: .utf8).flatMap(Int.init), |
| 56 | let origin = URL(string: "http://127.0.0.1:\(number)") else { |
| 57 | process.terminate() |
| 58 | throw NativeClientError.message("The parity fixture could not start.") |
| 59 | } |
| 60 | return (process, origin) |
| 61 | } |
| 62 | |
| 63 | private func lines(_ transport: NativeTransport, _ path: String) async throws -> [String] { |
| 64 | let collected = Collected() |
| 65 | try await transport.stream(path) { await collected.append($0) } |
| 66 | return await collected.lines |
| 67 | } |
| 68 | |
| 69 | func testScriptedStreamFramesLikeNativeHTTP() async throws { |
| 70 | let (process, origin) = try server(bodies) |
| 71 | defer { process.terminate() } |
| 72 | let real = NativeHTTP(origin: origin) |
| 73 | for (name, body) in bodies.sorted(by: { $0.key < $1.key }) { |
| 74 | let expected = try await lines(real, "/\(name)") |
| 75 | let scripted = try await lines(ScriptedTransport([.respond(status: 200, body: body)]), "/\(name)") |
| 76 | XCTAssertEqual(scripted, expected, "\(name): scripted framing differs from NativeHTTP") |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | func testScriptedStreamRefusesWhatNativeHTTPRefuses() async throws { |
| 81 | let invalid = Data([0x64, 0x61, 0x74, 0x61, 0x3A, 0xFF, 0x0A]) |
| 82 | let long = Data(repeating: 0x61, count: 256 * 1024 + 1) + Data("\n".utf8) |
| 83 | let (process, origin) = try server(["invalid": invalid, "long": long]) |
| 84 | defer { process.terminate() } |
| 85 | let real = NativeHTTP(origin: origin) |
| 86 | for (name, body) in [("invalid", invalid), ("long", long)] { |
| 87 | var realError: String? |
| 88 | do { _ = try await lines(real, "/\(name)") } catch { realError = error.localizedDescription } |
| 89 | XCTAssertNotNil(realError, "\(name): NativeHTTP accepted it") |
| 90 | do { |
| 91 | _ = try await lines(ScriptedTransport([.respond(status: 200, body: body)]), "/\(name)") |
| 92 | XCTFail("\(name): ScriptedTransport accepted what NativeHTTP refuses") |
| 93 | } catch { XCTAssertEqual(error.localizedDescription, realError, name) } |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | #endif |
| 98 | |