Add assertion helpers for NIOHTTP1TestServer (#1760)

Motivation:

Verifying the contents of inbound request parts for `NIOHTTP1TestServer`
can be quite tedious and whenever I use `NIOHTTP1TestServer` in
different projects I find myself writing similar extensions to help
verify the inbound request parts are as expected.

Modifications:

- Add verification methods to `NIOHTTP1TestServer` which checks for the
  expected part and optionally verifies it further in a callback

Result:

It's easier to test with NIOHTTP1TestServer
This commit is contained in:
George Barnett 2021-03-03 10:20:55 +00:00 committed by GitHub
parent bd41bd5cf5
commit d22d89804c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 211 additions and 13 deletions

View File

@ -135,21 +135,22 @@ private final class AggregateBodyHandler: ChannelInboundHandler {
/// body: requestBody))
///
/// // Assert the server received the expected request.
/// // Use custom methods if you only want some specific assertions on part
/// // of the request.
/// XCTAssertNoThrow(XCTAssertEqual(.head(.init(version: .http1_1,
/// method: .GET,
/// uri: "/some-route",
/// headers: .init([
/// ("Content-Type", "text/plain; charset=utf-8"),
/// ("Content-Length", "4")]))),
/// try testServer.readInbound()))
/// XCTAssertNoThrow(try testServer.receiveHeadAndVerify { head in
/// XCTAssertEqual(head, .init(version: .http1_1,
/// method: .GET,
/// uri: "/some-route",
/// headers: .init([
/// ("Content-Type", "text/plain; charset=utf-8"),
/// ("Content-Length", "4")])))
/// })
/// var requestBuffer = allocator.buffer(capacity: 128)
/// requestBuffer.writeString(requestBody)
/// XCTAssertNoThrow(XCTAssertEqual(.body(requestBuffer),
/// try testServer.readInbound()))
/// XCTAssertNoThrow(XCTAssertEqual(.end(nil),
/// try testServer.readInbound()))
/// XCTAssertNoThrow(try testServer.receiveBodyAndVerify { body in
/// XCTAssertEqual(body, requestBuffer)
/// })
/// XCTAssertNoThrow(try testServer.receiveEndAndVerify { trailers in
/// XCTAssertNil(trailers)
/// })
///
/// // Make the server send a response to the client.
/// let responseBody = "pong"
@ -314,3 +315,105 @@ extension NIOHTTP1TestServer {
self.inboundBuffer.append(.failure(error))
}
}
extension NIOHTTP1TestServer {
/// Waits for a message part to be received and checks that it was a `.head` before returning
/// the `HTTPRequestHead` it contained.
///
/// - Parameters:
/// - deadline: The deadline by which a part must have been received.
/// - Throws: If the part was not a `.head` or nothing was read before the deadline.
/// - Returns: The `HTTPRequestHead` from the `.head`.
public func receiveHead(deadline: NIODeadline = .now() + .seconds(10)) throws -> HTTPRequestHead {
let part = try self.readInbound(deadline: deadline)
switch part {
case .head(let head):
return head
default:
throw NIOHTTP1TestServerError(reason: "Expected .head but got '\(part)'")
}
}
/// Waits for a message part to be received and checks that it was a `.head` before passing
/// it to the `verify` block.
///
/// - Parameters:
/// - deadline: The deadline by which a part must have been received.
/// - verify: A closure which can be used to verify the contents of the `HTTPRequestHead`.
/// - Throws: If the part was not a `.head` or nothing was read before the deadline.
public func receiveHeadAndVerify(deadline: NIODeadline = .now() + .seconds(10),
_ verify: (HTTPRequestHead) throws -> () = { _ in }) throws {
try verify(self.receiveHead(deadline: deadline))
}
/// Waits for a message part to be received and checks that it was a `.body` before returning
/// the `ByteBuffer` it contained.
///
/// - Parameters:
/// - deadline: The deadline by which a part must have been received.
/// - Throws: If the part was not a `.body` or nothing was read before the deadline.
/// - Returns: The `ByteBuffer` from the `.body`.
public func receiveBody(deadline: NIODeadline = .now() + .seconds(10)) throws -> ByteBuffer {
let part = try self.readInbound(deadline: deadline)
switch part {
case .body(let buffer):
return buffer
default:
throw NIOHTTP1TestServerError(reason: "Expected .body but got '\(part)'")
}
}
/// Waits for a message part to be received and checks that it was a `.body` before passing
/// it to the `verify` block.
///
/// - Parameters:
/// - deadline: The deadline by which a part must have been received.
/// - verify: A closure which can be used to verify the contents of the `ByteBuffer`.
/// - Throws: If the part was not a `.body` or nothing was read before the deadline.
public func receiveBodyAndVerify(deadline: NIODeadline = .now() + .seconds(10),
_ verify: (ByteBuffer) throws -> () = { _ in }) throws {
try verify(self.receiveBody(deadline: deadline))
}
/// Waits for a message part to be received and checks that it was a `.end` before returning
/// the `HTTPHeaders?` it contained.
///
/// - Parameters:
/// - deadline: The deadline by which a part must have been received.
/// - Throws: If the part was not a `.end` or nothing was read before the deadline.
/// - Returns: The `HTTPHeaders?` from the `.end`.
public func receiveEnd(deadline: NIODeadline = .now() + .seconds(10)) throws -> HTTPHeaders? {
let part = try self.readInbound(deadline: deadline)
switch part {
case .end(let trailers):
return trailers
default:
throw NIOHTTP1TestServerError(reason: "Expected .end but got '\(part)'")
}
}
/// Waits for a message part to be received and checks that it was a `.end` before passing
/// it to the `verify` block.
///
/// - Parameters:
/// - deadline: The deadline by which a part must have been received.
/// - verify: A closure which can be used to verify the contents of the `HTTPHeaders?`.
/// - Throws: If the part was not a `.end` or nothing was read before the deadline.
public func receiveEndAndVerify(deadline: NIODeadline = .now() + .seconds(10),
_ verify: (HTTPHeaders?) throws -> () = { _ in }) throws {
try verify(self.receiveEnd())
}
}
public struct NIOHTTP1TestServerError: Error, Hashable, CustomStringConvertible {
public var reason: String
public init(reason: String) {
self.reason = reason
}
public var description: String {
return self.reason
}
}

View File

@ -32,6 +32,9 @@ extension NIOHTTP1TestServerTest {
("testConcurrentRequests", testConcurrentRequests),
("testTestWebServerCanBeReleased", testTestWebServerCanBeReleased),
("testStopClosesAcceptedChannel", testStopClosesAcceptedChannel),
("testReceiveAndVerify", testReceiveAndVerify),
("testReceive", testReceive),
("testReceiveAndVerifyWrongPart", testReceiveAndVerifyWrongPart),
]
}
}

View File

@ -237,6 +237,81 @@ class NIOHTTP1TestServerTest: XCTestCase {
XCTAssertNotNil(channel)
XCTAssertNoThrow(try channel.closeFuture.wait())
}
func testReceiveAndVerify() {
let testServer = NIOHTTP1TestServer(group: self.group)
let responsePromise = self.group.next().makePromise(of: String.self)
var channel: Channel!
XCTAssertNoThrow(channel = try self.connect(serverPort: testServer.serverPort,
responsePromise: responsePromise).wait())
self.sendRequest(channel: channel, uri: "/uri", message: "hello")
XCTAssertNoThrow(try testServer.receiveHeadAndVerify { head in
XCTAssertEqual(head.uri, "/uri")
})
XCTAssertNoThrow(try testServer.receiveBodyAndVerify { buffer in
XCTAssertEqual(buffer, ByteBuffer(string: "hello"))
})
XCTAssertNoThrow(try testServer.receiveEndAndVerify { trailers in
XCTAssertNil(trailers)
})
XCTAssertNoThrow(try testServer.stop())
XCTAssertNotNil(channel)
XCTAssertNoThrow(try channel.closeFuture.wait())
}
func testReceive() throws {
let testServer = NIOHTTP1TestServer(group: self.group)
let responsePromise = self.group.next().makePromise(of: String.self)
var channel: Channel!
XCTAssertNoThrow(channel = try self.connect(serverPort: testServer.serverPort,
responsePromise: responsePromise).wait())
self.sendRequest(channel: channel, uri: "/uri", message: "hello")
let head = try assertNoThrowWithValue(try testServer.receiveHead())
XCTAssertEqual(head.uri, "/uri")
let body = try assertNoThrowWithValue(try testServer.receiveBody())
XCTAssertEqual(body, ByteBuffer(string: "hello"))
let trailers = try assertNoThrowWithValue(try testServer.receiveEnd())
XCTAssertNil(trailers)
XCTAssertNoThrow(try testServer.stop())
XCTAssertNotNil(channel)
XCTAssertNoThrow(try channel.closeFuture.wait())
}
func testReceiveAndVerifyWrongPart() {
let testServer = NIOHTTP1TestServer(group: self.group)
let responsePromise = self.group.next().makePromise(of: String.self)
var channel: Channel!
XCTAssertNoThrow(channel = try self.connect(serverPort: testServer.serverPort,
responsePromise: responsePromise).wait())
self.sendRequest(channel: channel, uri: "/uri", message: "hello")
XCTAssertThrowsError(try testServer.receiveEndAndVerify()) { error in
XCTAssert(error is NIOHTTP1TestServerError)
}
XCTAssertThrowsError(try testServer.receiveHeadAndVerify()) { error in
XCTAssert(error is NIOHTTP1TestServerError)
}
XCTAssertThrowsError(try testServer.receiveBodyAndVerify()) { error in
XCTAssert(error is NIOHTTP1TestServerError)
}
XCTAssertNoThrow(try testServer.stop())
XCTAssertNotNil(channel)
XCTAssertNoThrow(try channel.closeFuture.wait())
}
}
private final class TestHTTPHandler: ChannelInboundHandler {
@ -352,3 +427,20 @@ func assert(_ condition: @autoclosure () -> Bool,
XCTFail(message, file: (file), line: line)
}
}
func assertNoThrowWithValue<T>(_ body: @autoclosure () throws -> T,
defaultValue: T? = nil,
message: String? = nil,
file: StaticString = #file,
line: UInt = #line) throws -> T {
do {
return try body()
} catch {
XCTFail("\(message.map { $0 + ": " } ?? "")unexpected error \(error) thrown", file: (file), line: line)
if let defaultValue = defaultValue {
return defaultValue
} else {
throw error
}
}
}