Skip to content

Commit 00f2c83

Browse files
committed
Add basic tests
1 parent d56f742 commit 00f2c83

File tree

4 files changed

+371
-24
lines changed

4 files changed

+371
-24
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
138+
@Test("More bytes available than consumption limit")
139+
func testCollectMoreBytesThanAvailable() async throws {
140+
let (stream, source) = NIOAsyncChannelInboundStream<HTTPRequestPart>.makeTestingStream()
141+
142+
// Write 10 bytes
143+
source.yield(.body(.init(repeating: 5, count: 10)))
144+
source.finish()
145+
146+
let requestReader = HTTPRequestConcludingAsyncReader(iterator: stream.makeAsyncIterator(), readerState: .init())
147+
148+
_ = try await requestReader.consumeAndConclude { requestBodyReader in
149+
var requestBodyReader = requestBodyReader
150+
151+
// Attempting to collect a maximum of 9 bytes should result in a LimitExceeded error.
152+
await #expect(throws: LimitExceeded.self) {
153+
try await requestBodyReader.collect(upTo: 9) { element in
154+
()
155+
}
156+
}
157+
}
158+
}
159+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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("Write multiple elements and multiple trailers")
59+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
60+
func testProduceMultipleElementsAndMultipleTrailers() async throws {
61+
let (writer, sink) = NIOAsyncChannelOutboundWriter<HTTPResponsePart>.makeTestingWriter()
62+
let responseWriter = HTTPResponseConcludingAsyncWriter(writer: writer, writerState: .init())
63+
64+
try await responseWriter.produceAndConclude { bodyWriter in
65+
var bodyWriter = bodyWriter
66+
67+
// Write multiple elements
68+
try await bodyWriter.write(self.bodySampleOne.span)
69+
try await bodyWriter.write(self.bodySampleTwo.span)
70+
71+
return self.trailerSampleTwo
72+
}
73+
74+
var responseIterator = sink.makeAsyncIterator()
75+
76+
let firstElement = try #require(await responseIterator.next())
77+
let secondElement = try #require(await responseIterator.next())
78+
#expect(firstElement == .body(.init(bytes: self.bodySampleOne)))
79+
#expect(secondElement == .body(.init(bytes: self.bodySampleTwo)))
80+
81+
let trailer = try #require(await responseIterator.next())
82+
#expect(trailer == .end(self.trailerSampleTwo))
83+
}
84+
85+
@Test("No body, just trailers")
86+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
87+
func testNoBodyJustTrailers() async throws {
88+
let (writer, sink) = NIOAsyncChannelOutboundWriter<HTTPResponsePart>.makeTestingWriter()
89+
let responseWriter = HTTPResponseConcludingAsyncWriter(writer: writer, writerState: .init())
90+
91+
try await responseWriter.produceAndConclude { bodyWriter in
92+
return self.trailerSampleTwo
93+
}
94+
95+
var responseIterator = sink.makeAsyncIterator()
96+
let trailer = try #require(await responseIterator.next())
97+
#expect(trailer == .end(self.trailerSampleTwo))
98+
}
99+
}
100+
101+
extension HTTPField.Name {
102+
static var serverTiming: Self {
103+
Self("Server-Timing")!
104+
}
105+
}
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+
}

0 commit comments

Comments
 (0)