Skip to content

Commit 4431697

Browse files
authored
Add basic tests (#36)
Motivation: Currently, we have no tests for the reader, writer, and sender types. Modifications: Added some basic tests for those types. Result: Increased test coverage.
1 parent eea3f4e commit 4431697

File tree

4 files changed

+396
-0
lines changed

4 files changed

+396
-0
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
@testable import HTTPServer
2+
import HTTPTypes
3+
import NIOCore
4+
import NIOHTTP1
5+
import NIOHTTPTypes
6+
import NIOPosix
7+
import Testing
8+
9+
@Suite
10+
struct HTTPRequestConcludingAsyncReaderTests {
11+
@Test("Head request not allowed")
12+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
13+
func testWriteHeadRequestPartFatalError() async throws {
14+
// The request body reader should fatal error if it receives a head part
15+
await #expect(processExitsWith: .failure) {
16+
let (stream, source) = NIOAsyncChannelInboundStream<HTTPRequestPart>.makeTestingStream()
17+
18+
// Write just a request head
19+
source.yield(.head(.init(method: .get, scheme: "http", authority: "", path: "")))
20+
source.finish()
21+
22+
let requestReader = HTTPRequestConcludingAsyncReader(
23+
iterator: stream.makeAsyncIterator(),
24+
readerState: .init()
25+
)
26+
27+
_ = try await requestReader.consumeAndConclude { bodyReader in
28+
var bodyReader = bodyReader
29+
try await bodyReader.read { element in () }
30+
}
31+
}
32+
}
33+
34+
@Test(
35+
"Request with concluding element",
36+
arguments: [ByteBuffer(repeating: 1, count: 100), ByteBuffer()],
37+
[
38+
HTTPFields([.init(name: .cookie, value: "test_cookie")]),
39+
HTTPFields([.init(name: .cookie, value: "first_cookie"), .init(name: .cookie, value: "second_cookie")])
40+
]
41+
)
42+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
43+
func testRequestWithConcludingElement(body: ByteBuffer, trailers: HTTPFields) async throws {
44+
let (stream, source) = NIOAsyncChannelInboundStream<HTTPRequestPart>.makeTestingStream()
45+
46+
// First write the request
47+
source.yield(.body(body))
48+
source.yield(.end(trailers))
49+
source.finish()
50+
51+
// Then start reading the request
52+
let requestReader = HTTPRequestConcludingAsyncReader(iterator: stream.makeAsyncIterator(), readerState: .init())
53+
let (requestBody, finalElement) = try await requestReader.consumeAndConclude { bodyReader in
54+
var bodyReader = bodyReader
55+
56+
var buffer = ByteBuffer()
57+
// Read just once: we only sent one body chunk
58+
try await bodyReader.read { element in
59+
if let element {
60+
buffer.writeBytes(element.bytes)
61+
} else {
62+
Issue.record("Unexpectedly failed to read the client's request body")
63+
}
64+
}
65+
66+
// Attempting to read again should result in a `nil` element (we only sent one body chunk)
67+
try await bodyReader.read { element in
68+
if element != nil {
69+
Issue.record("Received a non-nil value after the request body was completely read")
70+
}
71+
}
72+
73+
return buffer
74+
}
75+
76+
#expect(requestBody == body)
77+
#expect(finalElement == trailers)
78+
}
79+
80+
@Test(
81+
"Streamed request with concluding element",
82+
arguments: [
83+
(0..<10).map { i in ByteBuffer() }, // 10 empty ByteBuffers
84+
(0..<100).map { i in ByteBuffer(bytes: [i]) } // 100 single-byte ByteBuffers
85+
],
86+
[
87+
HTTPFields([.init(name: .cookie, value: "test")]),
88+
HTTPFields([.init(name: .cookie, value: "first_cookie"), .init(name: .cookie, value: "second_cookie")]),
89+
]
90+
)
91+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
92+
func testStreamedRequestBody(bodyChunks: [ByteBuffer], trailers: HTTPFields) async throws {
93+
let (stream, source) = NIOAsyncChannelInboundStream<HTTPRequestPart>.makeTestingStream()
94+
95+
// Execute the writer and reader tasks concurrently
96+
await withThrowingTaskGroup { group in
97+
group.addTask {
98+
for chunk in bodyChunks {
99+
source.yield(.body(chunk))
100+
}
101+
source.yield(.end(trailers))
102+
source.finish()
103+
}
104+
105+
group.addTask {
106+
let requestReader = HTTPRequestConcludingAsyncReader(
107+
iterator: stream.makeAsyncIterator(),
108+
readerState: .init()
109+
)
110+
let finalElement = try await requestReader.consumeAndConclude { bodyReader in
111+
var bodyReader = bodyReader
112+
113+
for chunk in bodyChunks {
114+
try await bodyReader.read { element in
115+
if let element {
116+
var buffer = ByteBuffer()
117+
buffer.writeBytes(element.bytes)
118+
#expect(chunk == buffer)
119+
} else {
120+
Issue.record("Received a nil element before the request body was completely read")
121+
}
122+
}
123+
}
124+
125+
try await bodyReader.read { element in
126+
if element != nil {
127+
Issue.record("Received a non-nil element after the request body was completely read")
128+
}
129+
}
130+
}
131+
132+
#expect(finalElement == trailers)
133+
}
134+
}
135+
}
136+
137+
@Test("Throw while reading request")
138+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
139+
func testThrowingWhileReadingRequest() async throws {
140+
let (stream, source) = NIOAsyncChannelInboundStream<HTTPRequestPart>.makeTestingStream()
141+
142+
let bodyChunks = (0..<10).map { i in ByteBuffer(bytes: [i]) }
143+
for chunk in bodyChunks {
144+
source.yield(.body(chunk))
145+
}
146+
source.yield(.end([.cookie: "test"]))
147+
source.finish()
148+
149+
// Check that the read error is propagated
150+
try await #require(throws: TestError.errorWhileReading) {
151+
let requestReader = HTTPRequestConcludingAsyncReader(
152+
iterator: stream.makeAsyncIterator(),
153+
readerState: .init()
154+
)
155+
156+
_ = try await requestReader.consumeAndConclude { bodyReader in
157+
var bodyReader = bodyReader
158+
159+
try await bodyReader.read { element in
160+
throw TestError.errorWhileReading
161+
}
162+
}
163+
}
164+
}
165+
166+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
167+
@Test("More bytes available than consumption limit")
168+
func testCollectMoreBytesThanAvailable() async throws {
169+
let (stream, source) = NIOAsyncChannelInboundStream<HTTPRequestPart>.makeTestingStream()
170+
171+
// Write 10 bytes
172+
source.yield(.body(.init(repeating: 5, count: 10)))
173+
source.finish()
174+
175+
let requestReader = HTTPRequestConcludingAsyncReader(iterator: stream.makeAsyncIterator(), readerState: .init())
176+
177+
_ = try await requestReader.consumeAndConclude { requestBodyReader in
178+
var requestBodyReader = requestBodyReader
179+
180+
// Attempting to collect a maximum of 9 bytes should result in a LimitExceeded error.
181+
await #expect(throws: LimitExceeded.self) {
182+
try await requestBodyReader.collect(upTo: 9) { element in
183+
()
184+
}
185+
}
186+
}
187+
}
188+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
@testable import HTTPServer
2+
import HTTPTypes
3+
import NIOCore
4+
import NIOHTTPTypes
5+
import Testing
6+
7+
@Suite
8+
struct HTTPResponseConcludingAsyncWriterTests {
9+
let bodySampleOne: [UInt8] = [1, 2]
10+
let bodySampleTwo: [UInt8] = [3, 4]
11+
let trailerSampleOne: HTTPFields = [.serverTiming: "test"]
12+
let trailerSampleTwo: HTTPFields = [.serverTiming: "test", .cookie: "cookie"]
13+
14+
@Test("Write single body element")
15+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
16+
func testSingleWriteAndConclude() async throws {
17+
let (writer, sink) = NIOAsyncChannelOutboundWriter<HTTPResponsePart>.makeTestingWriter()
18+
let responseWriter = HTTPResponseConcludingAsyncWriter(writer: writer, writerState: .init())
19+
20+
try await responseWriter.writeAndConclude(element: self.bodySampleOne.span, finalElement: self.trailerSampleOne)
21+
22+
// Now read the response
23+
var responseIterator = sink.makeAsyncIterator()
24+
25+
let element = try #require(await responseIterator.next())
26+
#expect(element == .body(.init(bytes: self.bodySampleOne)))
27+
let trailer = try #require(await responseIterator.next())
28+
#expect(trailer == .end(self.trailerSampleOne))
29+
}
30+
31+
@Test("Write multiple body elements")
32+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
33+
func testProduceMultipleElementsAndSingleTrailer() async throws {
34+
let (writer, sink) = NIOAsyncChannelOutboundWriter<HTTPResponsePart>.makeTestingWriter()
35+
let responseWriter = HTTPResponseConcludingAsyncWriter(writer: writer, writerState: .init())
36+
37+
try await responseWriter.produceAndConclude { bodyWriter in
38+
var bodyWriter = bodyWriter
39+
40+
// Write multiple elements
41+
try await bodyWriter.write(self.bodySampleOne.span)
42+
try await bodyWriter.write(self.bodySampleTwo.span)
43+
44+
return self.trailerSampleOne
45+
}
46+
47+
var responseIterator = sink.makeAsyncIterator()
48+
49+
let firstElement = try #require(await responseIterator.next())
50+
let secondElement = try #require(await responseIterator.next())
51+
#expect(firstElement == .body(.init(bytes: self.bodySampleOne)))
52+
#expect(secondElement == .body(.init(bytes: self.bodySampleTwo)))
53+
54+
let trailer = try #require(await responseIterator.next())
55+
#expect(trailer == .end(self.trailerSampleOne))
56+
}
57+
58+
@Test("Throw while writing response")
59+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
60+
func testThrowWhileProducing() async throws {
61+
let (writer, sink) = NIOAsyncChannelOutboundWriter<HTTPResponsePart>.makeTestingWriter()
62+
63+
// Check that the write error is propagated
64+
try await #require(throws: TestError.errorWhileWriting) {
65+
let responseWriter = HTTPResponseConcludingAsyncWriter(writer: writer, writerState: .init())
66+
try await responseWriter.produceAndConclude { bodyWriter in
67+
var bodyWriter = bodyWriter
68+
69+
// Write an element
70+
try await bodyWriter.write(self.bodySampleOne.span)
71+
// Then throw
72+
throw TestError.errorWhileWriting
73+
}
74+
}
75+
76+
var responseIterator = sink.makeAsyncIterator()
77+
78+
let firstElement = try #require(await responseIterator.next())
79+
#expect(firstElement == .body(.init(bytes: self.bodySampleOne)))
80+
}
81+
82+
@Test("Write multiple elements and multiple trailers")
83+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
84+
func testProduceMultipleElementsAndMultipleTrailers() async throws {
85+
let (writer, sink) = NIOAsyncChannelOutboundWriter<HTTPResponsePart>.makeTestingWriter()
86+
let responseWriter = HTTPResponseConcludingAsyncWriter(writer: writer, writerState: .init())
87+
88+
try await responseWriter.produceAndConclude { bodyWriter in
89+
var bodyWriter = bodyWriter
90+
91+
// Write multiple elements
92+
try await bodyWriter.write(self.bodySampleOne.span)
93+
try await bodyWriter.write(self.bodySampleTwo.span)
94+
95+
return self.trailerSampleTwo
96+
}
97+
98+
var responseIterator = sink.makeAsyncIterator()
99+
100+
let firstElement = try #require(await responseIterator.next())
101+
let secondElement = try #require(await responseIterator.next())
102+
#expect(firstElement == .body(.init(bytes: self.bodySampleOne)))
103+
#expect(secondElement == .body(.init(bytes: self.bodySampleTwo)))
104+
105+
let trailer = try #require(await responseIterator.next())
106+
#expect(trailer == .end(self.trailerSampleTwo))
107+
}
108+
109+
@Test("No body, just trailers")
110+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
111+
func testNoBodyJustTrailers() async throws {
112+
let (writer, sink) = NIOAsyncChannelOutboundWriter<HTTPResponsePart>.makeTestingWriter()
113+
let responseWriter = HTTPResponseConcludingAsyncWriter(writer: writer, writerState: .init())
114+
115+
try await responseWriter.produceAndConclude { bodyWriter in
116+
return self.trailerSampleTwo
117+
}
118+
119+
var responseIterator = sink.makeAsyncIterator()
120+
let trailer = try #require(await responseIterator.next())
121+
#expect(trailer == .end(self.trailerSampleTwo))
122+
}
123+
}
124+
125+
extension HTTPField.Name {
126+
static var serverTiming: Self {
127+
Self("Server-Timing")!
128+
}
129+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
@testable import HTTPServer
2+
import NIOCore
3+
import NIOHTTPTypes
4+
import HTTPTypes
5+
import Testing
6+
7+
@Suite
8+
struct HTTPResponseSenderTests {
9+
@Test("Informational header without informational status code")
10+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
11+
func testInformationalResponseStatusCodePrecondition() async throws {
12+
// Sending an informational header with a non-1xx status code shouldn't be allowed
13+
try await #require(processExitsWith: .failure) {
14+
let (outboundWriter, _) = NIOAsyncChannelOutboundWriter<HTTPResponsePart>.makeTestingWriter()
15+
let sender = HTTPResponseSender { response in
16+
try await outboundWriter.write(.head(response))
17+
return HTTPResponseConcludingAsyncWriter(
18+
writer: outboundWriter,
19+
writerState: .init()
20+
)
21+
} sendInformational: { response in
22+
try await outboundWriter.write(.head(response))
23+
}
24+
25+
try await sender.sendInformational(.init(status: .ok, headerFields: [.contentType: "application/json"]))
26+
}
27+
}
28+
29+
@Test("Multiple informational responses before final response")
30+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
31+
func testSendMultipleInformationalResponses() async throws {
32+
let (outboundWriter, sink) = NIOAsyncChannelOutboundWriter<HTTPResponsePart>.makeTestingWriter()
33+
let sender = HTTPResponseSender { response in
34+
try await outboundWriter.write(.head(response))
35+
return HTTPResponseConcludingAsyncWriter(
36+
writer: outboundWriter,
37+
writerState: .init()
38+
)
39+
} sendInformational: { response in
40+
try await outboundWriter.write(.head(response))
41+
}
42+
43+
// Send two informational responses
44+
let firstInfoHead = HTTPResponse(status: .continue, headerFields: [.contentType: "application/json"])
45+
let secondInfoHead = HTTPResponse(status: .earlyHints, headerFields: [.contentType: "application/json"])
46+
try await sender.sendInformational(firstInfoHead)
47+
try await sender.sendInformational(secondInfoHead)
48+
49+
// Then send the final response
50+
let finalResponseHead = HTTPResponse(status: .ok, headerFields: [:])
51+
let finalResponseBody = [UInt8]([1, 2])
52+
let finalResponseTrailer: HTTPFields = [.cookie: "cookie"]
53+
54+
let responseWriter = try await sender.send(.init(status: .ok, headerFields: [:]))
55+
try await responseWriter.produceAndConclude { bodyTrailerWriter in
56+
var bodyTrailerWriter = bodyTrailerWriter
57+
try await bodyTrailerWriter.write(finalResponseBody.span)
58+
return finalResponseTrailer
59+
}
60+
61+
var responseIterator = sink.makeAsyncIterator()
62+
let firstHead = try #require(await responseIterator.next())
63+
let secondHead = try #require(await responseIterator.next())
64+
let finalHead = try #require(await responseIterator.next())
65+
let body = try #require(await responseIterator.next())
66+
let trailer = try #require(await responseIterator.next())
67+
68+
#expect(firstHead == .head(firstInfoHead))
69+
#expect(secondHead == .head(secondInfoHead))
70+
#expect(finalHead == .head(finalResponseHead))
71+
#expect(body == .body(ByteBuffer(bytes: finalResponseBody)))
72+
#expect(trailer == .end(finalResponseTrailer))
73+
}
74+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// An error type for use in tests
2+
enum TestError: Error {
3+
case errorWhileReading
4+
case errorWhileWriting
5+
}

0 commit comments

Comments
 (0)