summaryrefslogtreecommitdiffstats
path: root/Tests
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:18:32 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:18:32 -0300
commit72a4b451c9d06f7a97cac2f43f1b7f52fafcd3b7 (patch)
tree3e4d33a614b673bc8cd9de1e688245f340cd3e1e /Tests
downloadswift-0.1.0.tar.gz
Initial importv0.1.0
Diffstat (limited to 'Tests')
-rw-r--r--Tests/CepisTests/CepisTests.swift459
-rw-r--r--Tests/CepisTests/TestHTTPServer.swift266
2 files changed, 725 insertions, 0 deletions
diff --git a/Tests/CepisTests/CepisTests.swift b/Tests/CepisTests/CepisTests.swift
new file mode 100644
index 0000000..46699bc
--- /dev/null
+++ b/Tests/CepisTests/CepisTests.swift
@@ -0,0 +1,459 @@
+import Foundation
+import XCTest
+
+@testable import Cepis
+
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+let seAddressJSON = """
+ {"cep":"01001000","logradouro":"Praça da Sé","complemento":"lado ímpar",
+ "unidade":"","bairro":"Sé","localidade":"São Paulo","uf":"SP",
+ "estado":"São Paulo","regiao":"Sudeste","ibge":"3550308","gia":"1004",
+ "ddd":"11","siafi":"7107"}
+ """
+
+let csvHeader =
+ "cep,logradouro,complemento,unidade,bairro,localidade,uf,estado,regiao,ibge,gia,ddd,siafi"
+
+func csvBody(_ rows: [String]) -> String {
+ ([csvHeader] + rows).joined(separator: "\n") + "\n"
+}
+
+class ServerBackedTestCase: XCTestCase {
+ var server: TestHTTPServer!
+ var client: Client!
+
+ var routes: Router { server.router }
+
+ override func setUpWithError() throws {
+ try super.setUpWithError()
+ server = try TestHTTPServer()
+ client = Client(
+ baseURL: server.baseURL,
+ timeout: 5,
+ backoffBase: 0.001,
+ backoffMax: 0.01
+ )
+ }
+
+ override func tearDown() {
+ server?.stop()
+ server = nil
+ client = nil
+ super.tearDown()
+ }
+}
+
+final class NormalisationTests: XCTestCase {
+ func testAcceptsMaskedAndUnmasked() throws {
+ for value in ["01001000", "01001-000", " 01001-000 ", "01001.000"] {
+ XCTAssertEqual(try normalizeCEP(value), "01001000")
+ }
+ }
+
+ func testRejectsBadFormat() {
+ for value in ["", "123", "0100100a", "010010000", "abcdefgh"] {
+ XCTAssertThrowsError(try normalizeCEP(value)) { error in
+ guard case CepisError.invalidCEP = error else {
+ return XCTFail("expected invalidCEP for \"\(value)\", got \(error)")
+ }
+ }
+ }
+ }
+
+ func testInvalidCEPIsNotRetriable() {
+ XCTAssertFalse(CepisError.invalidCEP("x").isRetriable)
+ XCTAssertFalse(CepisError.cepNotFound("x").isRetriable)
+ XCTAssertTrue(CepisError.networkError(message: "x").isRetriable)
+ }
+}
+
+final class LookupTests: ServerBackedTestCase {
+ func testSuccessfulLookup() async throws {
+ routes.respond("/01001000", body: seAddressJSON)
+ let address = try await client.lookup("01001-000")
+ XCTAssertEqual(address.localidade, "São Paulo")
+ XCTAssertEqual(address.logradouro, "Praça da Sé")
+ XCTAssertEqual(address.uf, "SP")
+ XCTAssertEqual(address.formattedCEP, "01001-000")
+ XCTAssertNil(address.geo)
+ }
+
+ func testReadsTheCoordinateWhenPresent() async throws {
+ routes.respond(
+ "/01310100",
+ body: """
+ {"cep":"01310100","logradouro":"Avenida Paulista","localidade":"São Paulo",
+ "uf":"SP","geo":{"lat":-23.5,"lon":-46.6,"raio_m":120,
+ "precisao":"logradouro"}}
+ """)
+ let address = try await client.lookup("01310100")
+ let geo = try XCTUnwrap(address.geo)
+ XCTAssertEqual(geo.lat, -23.5, accuracy: 0.0001)
+ XCTAssertEqual(geo.lon, -46.6, accuracy: 0.0001)
+ XCTAssertEqual(geo.raioM, 120)
+ XCTAssertTrue(geo.isPrecise)
+ }
+
+ func testMunicipalityCoordinateIsNotPrecise() async throws {
+ routes.respond(
+ "/01001000",
+ body: """
+ {"cep":"01001000","localidade":"São Paulo","geo":{"lat":-23.5,"lon":-46.6,
+ "raio_m":22000,"precisao":"localidade"}}
+ """)
+ let address = try await client.lookup("01001000")
+ let geo = try XCTUnwrap(address.geo)
+ XCTAssertFalse(geo.isPrecise)
+ XCTAssertEqual(geo.raioM, 22000)
+ }
+
+ func testMalformedGeoDoesNotLoseTheAddress() async throws {
+ routes.respond(
+ "/01001000",
+ body: #"{"cep":"01001000","localidade":"São Paulo","geo":{"lat":"not a number"}}"#)
+ let address = try await client.lookup("01001000")
+ XCTAssertNil(address.geo)
+ XCTAssertEqual(address.localidade, "São Paulo")
+ }
+
+ func testNullGeoDoesNotLoseTheAddress() async throws {
+ routes.respond(
+ "/01001000", body: #"{"cep":"01001000","localidade":"São Paulo","geo":null}"#)
+ let address = try await client.lookup("01001000")
+ XCTAssertNil(address.geo)
+ XCTAssertEqual(address.localidade, "São Paulo")
+ }
+
+ func testNewAPIFieldDoesNotBreak() async throws {
+ routes.respond(
+ "/01001000",
+ body: #"{"cep":"01001000","localidade":"São Paulo","campo_do_futuro":"value"}"#)
+ let address = try await client.lookup("01001000")
+ XCTAssertEqual(address.localidade, "São Paulo")
+ }
+
+ func testNotFound() async throws {
+ routes.respond(
+ "/99999999", status: 404,
+ body: #"{"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE"}"#)
+ do {
+ _ = try await client.lookup("99999999")
+ XCTFail("expected cepNotFound")
+ } catch CepisError.cepNotFound(let message) {
+ XCTAssertTrue(message.contains("99999999"))
+ }
+ }
+
+ func testNotFoundIsNotRetried() async throws {
+ routes.respond("/99999999", status: 404, body: #"{"erro":"cep_nao_encontrado"}"#)
+ do {
+ _ = try await client.lookup("99999999")
+ XCTFail("expected cepNotFound")
+ } catch CepisError.cepNotFound {
+ }
+ XCTAssertEqual(routes.count("/99999999"), 1)
+ }
+
+ func testInvalidCEPNeverLeavesTheMachine() async throws {
+ do {
+ _ = try await client.lookup("123")
+ XCTFail("expected invalidCEP")
+ } catch CepisError.invalidCEP {
+ }
+ XCTAssertEqual(routes.calls.count, 0)
+ }
+
+ func testAPI400BecomesInvalidCEP() async throws {
+ routes.respond(
+ "/00000000", status: 400,
+ body: #"{"erro":"cep_invalido","mensagem":"rejected by the API"}"#)
+ do {
+ _ = try await client.lookup("00000000")
+ XCTFail("expected invalidCEP")
+ } catch CepisError.invalidCEP(let message) {
+ XCTAssertEqual(message, "rejected by the API")
+ }
+ }
+
+ func testResponseThatIsNotJSON() async throws {
+ routes.respond("/01001000", body: "<html>captive portal</html>")
+ do {
+ _ = try await client.lookup("01001000")
+ XCTFail("expected invalidResponse")
+ } catch CepisError.invalidResponse {
+ }
+ }
+}
+
+final class RetryTests: ServerBackedTestCase {
+ func testRetriesOn500ThenSucceeds() async throws {
+ routes.respond(
+ "/01001000",
+ sequence: [
+ StubResponse(status: 500, body: #"{"erro":"interno"}"#),
+ StubResponse(status: 200, body: seAddressJSON),
+ ])
+ let address = try await client.lookup("01001000")
+ XCTAssertEqual(address.localidade, "São Paulo")
+ XCTAssertEqual(routes.count("/01001000"), 2)
+ }
+
+ func testExhaustsAttemptsThenThrows() async throws {
+ routes.respond("/01001000", status: 503, body: #"{"erro":"interno"}"#)
+ do {
+ _ = try await client.lookup("01001000")
+ XCTFail("expected serverError")
+ } catch CepisError.serverError(_, let status) {
+ XCTAssertEqual(status, 503)
+ }
+ XCTAssertEqual(routes.count("/01001000"), 3)
+ }
+
+ func testMaxAttemptsOneDisablesRetry() async throws {
+ let single = Client(baseURL: server.baseURL, timeout: 5, maxAttempts: 1)
+ routes.respond("/01001000", status: 500, body: "{}")
+ do {
+ _ = try await single.lookup("01001000")
+ XCTFail("expected serverError")
+ } catch CepisError.serverError {
+ }
+ XCTAssertEqual(routes.count("/01001000"), 1)
+ }
+
+ func test429BecomesRateLimitedWithRetryAfter() async throws {
+ routes.respond("/01001000", status: 429, body: "{}", headers: ["Retry-After": "7"])
+ do {
+ _ = try await client.lookup("01001000")
+ XCTFail("expected rateLimited")
+ } catch CepisError.rateLimited(_, let retryAfter) {
+ XCTAssertEqual(retryAfter, 7)
+ }
+ }
+
+ func testRetryAfterHonoursTheCap() async throws {
+ routes.respond("/01001000", status: 429, body: "{}", headers: ["Retry-After": "3600"])
+ let started = Date()
+ do {
+ _ = try await client.lookup("01001000")
+ XCTFail("expected rateLimited")
+ } catch CepisError.rateLimited {
+ }
+ XCTAssertLessThan(Date().timeIntervalSince(started), 5)
+ }
+
+ func testRetryAfterAcceptsAnHTTPDate() throws {
+ let formatter = DateFormatter()
+ formatter.locale = Locale(identifier: "en_US_POSIX")
+ formatter.timeZone = TimeZone(identifier: "GMT")
+ formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz"
+ let header = formatter.string(from: Date().addingTimeInterval(30))
+
+ let seconds = try XCTUnwrap(Client.retryAfterSeconds(header))
+ XCTAssertGreaterThan(seconds, 20)
+ XCTAssertLessThan(seconds, 40)
+ }
+
+ func testRetryAfterInThePastIsClampedToZero() {
+ XCTAssertEqual(Client.retryAfterSeconds("-5"), 0)
+ XCTAssertNil(Client.retryAfterSeconds(nil))
+ XCTAssertNil(Client.retryAfterSeconds("not a date"))
+ }
+
+ func testNetworkErrorWhenNothingListens() async throws {
+ let unreachable = Client(
+ baseURL: URL(string: "http://127.0.0.1:1")!,
+ timeout: 1,
+ maxAttempts: 2,
+ backoffBase: 0.001,
+ backoffMax: 0.01
+ )
+ do {
+ _ = try await unreachable.lookup("01001000")
+ XCTFail("expected networkError")
+ } catch CepisError.networkError {
+ }
+ }
+
+ func testTotalTimeoutBoundsTheWholeCall() async throws {
+ let bounded = Client(
+ baseURL: server.baseURL,
+ timeout: 5,
+ maxAttempts: 50,
+ backoffBase: 0.05,
+ backoffMax: 0.05,
+ totalTimeout: 0.3
+ )
+ routes.respond("/01001000", status: 500, body: "{}")
+ let started = Date()
+ do {
+ _ = try await bounded.lookup("01001000")
+ XCTFail("expected serverError")
+ } catch CepisError.serverError {
+ }
+ XCTAssertLessThan(Date().timeIntervalSince(started), 5)
+ XCTAssertLessThan(routes.count("/01001000"), 50)
+ }
+}
+
+final class BatchTests: ServerBackedTestCase {
+ func testPreservesOrderAndMarksMissing() async throws {
+ let body = csvBody([
+ "01001000,Praça da Sé,lado ímpar,,Sé,São Paulo,SP,São Paulo,"
+ + "Sudeste,3550308,1004,11,7107",
+ "99999999,,,,,,,,,,,,",
+ "20040020,Avenida Rio Branco,,,Centro,Rio de Janeiro,RJ,"
+ + "Rio de Janeiro,Sudeste,3304557,,21,6001",
+ ])
+ routes.setHandler { _ in StubResponse(status: 200, body: body) }
+
+ let result = try await client.lookupMany(["01001000", "99999999", "20040020"])
+ XCTAssertEqual(result.count, 3)
+ XCTAssertEqual(result[0]?.localidade, "São Paulo")
+ XCTAssertNil(result[1])
+ XCTAssertEqual(result[2]?.uf, "RJ")
+ XCTAssertNil(result[0]?.geo)
+ }
+
+ func testFieldWithCommaIsParsed() async throws {
+ let body = csvBody([
+ #"01001000,"Rua Um, Dois",,,Centro,São Paulo,SP,São Paulo,Sudeste,1,,11,1"#
+ ])
+ routes.setHandler { _ in StubResponse(status: 200, body: body) }
+
+ let result = try await client.lookupMany(["01001000"])
+ XCTAssertEqual(result[0]?.logradouro, "Rua Um, Dois")
+ }
+
+ func testEscapedQuotesInAField() async throws {
+ let body = csvBody([
+ #"01001000,"Rua ""Dois"" Irmãos",,,Centro,São Paulo,SP,São Paulo,Sudeste,1,,11,1"#
+ ])
+ routes.setHandler { _ in StubResponse(status: 200, body: body) }
+
+ let result = try await client.lookupMany(["01001000"])
+ XCTAssertEqual(result[0]?.logradouro, #"Rua "Dois" Irmãos"#)
+ }
+
+ func testEmptyListMakesNoRequest() async throws {
+ let result = try await client.lookupMany([])
+ XCTAssertTrue(result.isEmpty)
+ XCTAssertEqual(routes.calls.count, 0)
+ }
+
+ func testChunksAboveTheAPILimit() async throws {
+ let ceps = (0..<501).map { String(format: "%08d", $0) }
+ let first = csvBody(ceps[0..<500].map { "\($0),,,,,X,SP,São Paulo,Sudeste,1,,11,1" })
+ let second = csvBody(ceps[500...].map { "\($0),,,,,X,SP,São Paulo,Sudeste,1,,11,1" })
+
+ let served = NSLock()
+ var count = 0
+ routes.setHandler { _ in
+ served.lock()
+ defer { served.unlock() }
+ count += 1
+ return StubResponse(status: 200, body: count == 1 ? first : second)
+ }
+
+ let result = try await client.lookupMany(ceps)
+ XCTAssertEqual(routes.count("/csv"), 2)
+ XCTAssertEqual(result.count, 501)
+ XCTAssertEqual(result[500]?.cep, "00000500")
+ }
+
+ func testTruncatedResponseDoesNotShift() async throws {
+ let body = csvBody(["01001000,,,,,São Paulo,SP,São Paulo,Sudeste,1,,11,1"])
+ routes.setHandler { _ in StubResponse(status: 200, body: body) }
+
+ let result = try await client.lookupMany(["01001000", "20040020"])
+ XCTAssertEqual(result.count, 2)
+ XCTAssertEqual(result[0]?.localidade, "São Paulo")
+ XCTAssertNil(result[1])
+ }
+
+ func testMalformedCEPBecomesNilRatherThanThrowing() async throws {
+ let body = csvBody([
+ "01001000,,,,,São Paulo,SP,São Paulo,Sudeste,1,,11,1",
+ "abc,,,,,,,,,,,,",
+ ])
+ routes.setHandler { _ in StubResponse(status: 200, body: body) }
+
+ let result = try await client.lookupMany(["01001000", "abc"])
+ XCTAssertEqual(result.count, 2)
+ XCTAssertNil(result[1])
+ }
+}
+
+final class HealthTests: ServerBackedTestCase {
+ func testReadsHealth() async throws {
+ routes.respond(
+ "/health",
+ body: """
+ {"status":"ok","dne":"26071","ceps":1596168,"geo":894489,
+ "base_gerada_em":"2026-07-24T03:48:28Z"}
+ """)
+ let health = try await client.health()
+ XCTAssertTrue(health.ok)
+ XCTAssertEqual(health.dne, "26071")
+ XCTAssertEqual(health.ceps, 1_596_168)
+ XCTAssertEqual(health.geo, 894_489)
+ XCTAssertEqual(health.baseGeradaEm, "2026-07-24T03:48:28Z")
+ }
+
+ func testDegradedDoesNotThrow() async throws {
+ routes.respond(
+ "/health", status: 503,
+ body: #"{"status":"degradado","dne":"","ceps":0,"geo":0}"#)
+ let health = try await client.health()
+ XCTAssertFalse(health.ok)
+ XCTAssertEqual(health.status, "degradado")
+ XCTAssertEqual(routes.count("/health"), 1)
+ }
+}
+
+final class ConfigurationTests: XCTestCase {
+ func testTrailingSlashIsNotDuplicated() {
+ let client = Client(baseURL: URL(string: "http://example.test/")!)
+ XCTAssertEqual(client.baseURL.absoluteString, "http://example.test")
+ }
+
+ func testUserAgentIdentifiesTheLibrary() {
+ XCTAssertTrue(Client().userAgent.hasPrefix("cepis-swift/"))
+ XCTAssertTrue(Client().userAgent.contains(Client.version))
+ }
+
+ func testDefaultBaseURL() {
+ XCTAssertEqual(Client.defaultBaseURL.absoluteString, "https://cep.is")
+ XCTAssertEqual(Client.maxBatch, 500)
+ }
+
+ func testFormattedCEPLeavesOddLengthsAlone() {
+ XCTAssertEqual(Address(cep: "01001000").formattedCEP, "01001-000")
+ XCTAssertEqual(Address(cep: "123").formattedCEP, "123")
+ }
+}
+
+final class CSVTests: XCTestCase {
+ func testParsesQuotedFieldsAndCRLF() {
+ let rows = CSV.parse("a,b\r\n\"x,1\",\"y\"\"z\"\r\n")
+ XCTAssertEqual(rows.count, 2)
+ XCTAssertEqual(rows[0], ["a", "b"])
+ XCTAssertEqual(rows[1], ["x,1", "y\"z"])
+ }
+
+ func testParsesLastLineWithoutNewline() {
+ let rows = CSV.parse("a,b\n1,2")
+ XCTAssertEqual(rows.count, 2)
+ XCTAssertEqual(rows[1], ["1", "2"])
+ }
+
+ func testTrailingNewlineDoesNotProduceAnEmptyRow() {
+ XCTAssertEqual(CSV.parse("a,b\n1,2\n").count, 2)
+ }
+
+ func testEmptyInput() {
+ XCTAssertTrue(CSV.parse("").isEmpty)
+ }
+}
diff --git a/Tests/CepisTests/TestHTTPServer.swift b/Tests/CepisTests/TestHTTPServer.swift
new file mode 100644
index 0000000..66722d4
--- /dev/null
+++ b/Tests/CepisTests/TestHTTPServer.swift
@@ -0,0 +1,266 @@
+import Foundation
+
+#if canImport(Glibc)
+import Glibc
+private let socketStreamType = Int32(SOCK_STREAM.rawValue)
+private let sendFlags = Int32(MSG_NOSIGNAL)
+#elseif canImport(Darwin)
+import Darwin
+private let socketStreamType = SOCK_STREAM
+private let sendFlags: Int32 = 0
+#endif
+
+struct StubResponse {
+ var status: Int = 200
+ var body: String = ""
+ var headers: [String: String] = [:]
+}
+
+final class Router: @unchecked Sendable {
+ private let lock = NSLock()
+ private var routes: [String: [StubResponse]] = [:]
+ private var handler: ((String) -> StubResponse)?
+ private var recorded: [String] = []
+
+ func respond(_ path: String, status: Int = 200, body: String, headers: [String: String] = [:]) {
+ lock.lock()
+ defer { lock.unlock() }
+ routes[path] = [StubResponse(status: status, body: body, headers: headers)]
+ }
+
+ func respond(_ path: String, sequence: [StubResponse]) {
+ lock.lock()
+ defer { lock.unlock() }
+ routes[path] = sequence
+ }
+
+ func setHandler(_ handler: @escaping (String) -> StubResponse) {
+ lock.lock()
+ defer { lock.unlock() }
+ self.handler = handler
+ }
+
+ var calls: [String] {
+ lock.lock()
+ defer { lock.unlock() }
+ return recorded
+ }
+
+ func count(_ prefix: String) -> Int {
+ calls.filter { $0.hasPrefix(prefix) }.count
+ }
+
+ func next(_ path: String) -> StubResponse {
+ lock.lock()
+ defer { lock.unlock() }
+ recorded.append(path)
+
+ if let handler {
+ return handler(path)
+ }
+ guard var queued = routes[path], !queued.isEmpty else {
+ return StubResponse(status: 404, body: #"{"erro":"cep_nao_encontrado"}"#)
+ }
+ if queued.count > 1 {
+ let head = queued.removeFirst()
+ routes[path] = queued
+ return head
+ }
+ return queued[0]
+ }
+}
+
+/// A throwaway HTTP/1.1 server on a random loopback port, built on BSD
+/// sockets because Foundation ships no server on Linux.
+///
+/// Every response closes its connection, so there is no keep-alive state to
+/// track and every request is a fresh accept.
+final class TestHTTPServer: @unchecked Sendable {
+ let router = Router()
+ let port: UInt16
+
+ private let listener: Int32
+ private let stopped = NSLock()
+ private var isStopped = false
+
+ init() throws {
+ signal(SIGPIPE, SIG_IGN)
+
+ let opened = try TestHTTPServer.openListener()
+ listener = opened.descriptor
+ port = opened.port
+
+ let thread = Thread { [weak self] in self?.acceptLoop() }
+ thread.stackSize = 1 << 20
+ thread.start()
+ }
+
+ private static func openListener() throws -> (descriptor: Int32, port: UInt16) {
+ let descriptor = socket(AF_INET, socketStreamType, 0)
+ guard descriptor >= 0 else {
+ throw TestServerError.failed("socket() failed with errno \(errno)")
+ }
+
+ var reuse: Int32 = 1
+ setsockopt(
+ descriptor, SOL_SOCKET, SO_REUSEADDR, &reuse, socklen_t(MemoryLayout<Int32>.size))
+
+ var address = sockaddr_in()
+ address.sin_family = sa_family_t(AF_INET)
+ address.sin_port = in_port_t(UInt16(0).bigEndian)
+ address.sin_addr = in_addr(s_addr: inet_addr("127.0.0.1"))
+
+ let bound = withUnsafePointer(to: &address) { pointer -> Int32 in
+ pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { generic in
+ bind(descriptor, generic, socklen_t(MemoryLayout<sockaddr_in>.size))
+ }
+ }
+ guard bound == 0 else {
+ close(descriptor)
+ throw TestServerError.failed("bind() failed with errno \(errno)")
+ }
+ guard listen(descriptor, 32) == 0 else {
+ close(descriptor)
+ throw TestServerError.failed("listen() failed with errno \(errno)")
+ }
+
+ var actual = sockaddr_in()
+ var length = socklen_t(MemoryLayout<sockaddr_in>.size)
+ let named = withUnsafeMutablePointer(to: &actual) { pointer -> Int32 in
+ pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { generic in
+ getsockname(descriptor, generic, &length)
+ }
+ }
+ guard named == 0 else {
+ close(descriptor)
+ throw TestServerError.failed("getsockname() failed with errno \(errno)")
+ }
+ return (descriptor, UInt16(bigEndian: actual.sin_port))
+ }
+
+ var baseURL: URL {
+ URL(string: "http://127.0.0.1:\(port)")!
+ }
+
+ func stop() {
+ stopped.lock()
+ if isStopped {
+ stopped.unlock()
+ return
+ }
+ isStopped = true
+ stopped.unlock()
+
+ shutdown(listener, Int32(SHUT_RDWR))
+ close(listener)
+ }
+
+ private var shuttingDown: Bool {
+ stopped.lock()
+ defer { stopped.unlock() }
+ return isStopped
+ }
+
+ private func acceptLoop() {
+ while !shuttingDown {
+ let client = accept(listener, nil, nil)
+ if client < 0 {
+ if errno == EINTR { continue }
+ return
+ }
+ let thread = Thread { [weak self] in self?.handle(client) }
+ thread.stackSize = 1 << 20
+ thread.start()
+ }
+ }
+
+ private func handle(_ client: Int32) {
+ defer { close(client) }
+
+ #if canImport(Darwin)
+ var noSigPipe: Int32 = 1
+ setsockopt(
+ client, SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, socklen_t(MemoryLayout<Int32>.size))
+ #endif
+
+ guard let path = readRequestPath(client) else { return }
+ let stub = router.next(path)
+ write(client, response(for: stub))
+ }
+
+ private func readRequestPath(_ client: Int32) -> String? {
+ var received: [UInt8] = []
+ var chunk = [UInt8](repeating: 0, count: 4096)
+
+ while headerEnd(of: received) == nil {
+ let read = recv(client, &chunk, chunk.count, 0)
+ if read <= 0 { break }
+ received.append(contentsOf: chunk[0..<read])
+ if received.count > 1 << 20 { break }
+ }
+ guard headerEnd(of: received) != nil else { return nil }
+
+ var lineEnd = received.count
+ for index in 0..<max(0, received.count - 1) where received[index] == 0x0D {
+ lineEnd = index
+ break
+ }
+ let line = String(decoding: received[0..<lineEnd], as: UTF8.self)
+ let parts = line.split(separator: " ")
+ guard parts.count >= 2 else { return nil }
+ return String(parts[1])
+ }
+
+ private func headerEnd(of bytes: [UInt8]) -> Int? {
+ guard bytes.count >= 4 else { return nil }
+ for index in 0...(bytes.count - 4) {
+ if bytes[index] == 0x0D, bytes[index + 1] == 0x0A, bytes[index + 2] == 0x0D,
+ bytes[index + 3] == 0x0A
+ {
+ return index
+ }
+ }
+ return nil
+ }
+
+ private func response(for stub: StubResponse) -> [UInt8] {
+ let body = Array(stub.body.utf8)
+ var head = "HTTP/1.1 \(stub.status) \(reason(stub.status))\r\n"
+ for (name, value) in stub.headers {
+ head += "\(name): \(value)\r\n"
+ }
+ head += "Content-Length: \(body.count)\r\n"
+ head += "Connection: close\r\n\r\n"
+ return Array(head.utf8) + body
+ }
+
+ private func reason(_ status: Int) -> String {
+ switch status {
+ case 200: return "OK"
+ case 400: return "Bad Request"
+ case 404: return "Not Found"
+ case 429: return "Too Many Requests"
+ case 500: return "Internal Server Error"
+ case 502: return "Bad Gateway"
+ case 503: return "Service Unavailable"
+ case 504: return "Gateway Timeout"
+ default: return "Status"
+ }
+ }
+
+ private func write(_ client: Int32, _ bytes: [UInt8]) {
+ var offset = 0
+ while offset < bytes.count {
+ let written = bytes.withUnsafeBufferPointer { buffer -> Int in
+ guard let base = buffer.baseAddress else { return -1 }
+ return send(client, base + offset, bytes.count - offset, sendFlags)
+ }
+ if written <= 0 { return }
+ offset += written
+ }
+ }
+}
+
+enum TestServerError: Error {
+ case failed(String)
+}