{{ message }}
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtest_framing.cpp
More file actions
211 lines (185 loc) · 9.69 KB
/
Copy pathtest_framing.cpp
File metadata and controls
211 lines (185 loc) · 9.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// The half of HTTP framing that can be examined without a server.
//
// `parse_chunk_size_line` and `parse_content_length` are exported so they can be
// tested here rather than through a network; `parse_status_line` joins them for
// the same reason. Every case below is a string a server can send.
#include <gtest/gtest.h>
import mcpplibs.tinyhttps;
import std;
namespace https = mcpplibs::tinyhttps;
// ── the status line ──────────────────────────────────────────────────────────
TEST(StatusLine, AcceptsTheOrdinaryShape) {
auto parsed = https::parse_status_line("HTTP/1.1 200 OK");
ASSERT_TRUE(parsed.has_value());
EXPECT_EQ(parsed->code, 200);
EXPECT_EQ(parsed->text, "OK");
}
TEST(StatusLine, AReasonPhraseIsOptionalAndMayContainSpaces) {
auto none = https::parse_status_line("HTTP/1.1 204");
ASSERT_TRUE(none.has_value());
EXPECT_EQ(none->code, 204);
EXPECT_TRUE(none->text.empty());
auto phrase = https::parse_status_line("HTTP/1.0 404 Not Found Here");
ASSERT_TRUE(phrase.has_value());
EXPECT_EQ(phrase->code, 404);
EXPECT_EQ(phrase->text, "Not Found Here");
}
// THE ONE FROM ISSUE #15.
//
// A socket returned to the pool with the previous response's body still on it
// gave the next request this to read. The older parser looked for the first
// space, then the second, then kept whatever digits it found between them:
//
// "BBBB 999 XHTTP/1.1 200 OK"
// ^ first space -> rest = "999 XHTTP/1.1 200 OK"
// ^ second space -> code = "999"
//
// and reported a perfectly ordinary-looking 999 to the caller.
TEST(StatusLine, RejectsALineThatDoesNotBeginWithTheVersion) {
EXPECT_FALSE(https::parse_status_line("BBBB 999 XHTTP/1.1 200 OK").has_value());
EXPECT_FALSE(https::parse_status_line("").has_value());
EXPECT_FALSE(https::parse_status_line("200 OK").has_value());
EXPECT_FALSE(https::parse_status_line("<html><body>").has_value());
// A run of digits with letters around it used to become 999 as well.
EXPECT_FALSE(https::parse_status_line("X9Y9Z9 999 OK").has_value());
}
TEST(StatusLine, RejectsAStatusCodeThatIsNotThreeDigits) {
EXPECT_FALSE(https::parse_status_line("HTTP/1.1 20 OK").has_value());
EXPECT_FALSE(https::parse_status_line("HTTP/1.1 2000 OK").has_value());
EXPECT_FALSE(https::parse_status_line("HTTP/1.1 20a OK").has_value());
EXPECT_FALSE(https::parse_status_line("HTTP/1.1 +20 OK").has_value());
EXPECT_FALSE(https::parse_status_line("HTTP/1.1").has_value());
// Outside the range the grammar defines. 999 is what issue #15 reported,
// and it is not a status code either.
EXPECT_FALSE(https::parse_status_line("HTTP/1.1 099 OK").has_value());
EXPECT_FALSE(https::parse_status_line("HTTP/1.1 999 OK").has_value());
}
TEST(StatusLine, AcceptsEveryClassTheGrammarDefines) {
for (int code : {100, 200, 204, 301, 302, 404, 418, 500, 599}) {
auto line = "HTTP/1.1 " + std::to_string(code) + " X";
auto parsed = https::parse_status_line(line);
ASSERT_TRUE(parsed.has_value()) << line;
EXPECT_EQ(parsed->code, code);
}
}
// ── chunk sizes ──────────────────────────────────────────────────────────────
TEST(ChunkedProtocol, RejectsEmptyInvalidAndOverflowSizeLines) {
EXPECT_FALSE(https::parse_chunk_size_line("").has_value());
EXPECT_FALSE(https::parse_chunk_size_line("xyz").has_value());
EXPECT_FALSE(https::parse_chunk_size_line("1g").has_value());
EXPECT_FALSE(https::parse_chunk_size_line("FFFFFFFFFFFFFFFF").has_value());
// The two the older `parse_hex` turned into a terminal chunk: an empty line
// (what a timed-out read produced) and a line of letters.
EXPECT_FALSE(https::parse_chunk_size_line("zz").has_value());
}
TEST(ChunkedProtocol, AcceptsValidSizeAndTerminalChunk) {
ASSERT_TRUE(https::parse_chunk_size_line("1a").has_value());
EXPECT_EQ(*https::parse_chunk_size_line("1a"), 26);
ASSERT_TRUE(https::parse_chunk_size_line("0").has_value());
EXPECT_EQ(*https::parse_chunk_size_line("0"), 0);
}
// ── content length ───────────────────────────────────────────────────────────
// `Content-Length` decides how many bytes a reader will trust, so a wrong
// answer is not cosmetic: too small leaves the next response's bytes in the
// stream and too large waits for bytes that are not coming.
//
// Both readers used to keep the digits and discard everything else. Measured,
// by compiling that parser on its own:
//
// "135" -> 135
// "0" -> 0
// "abc" -> 0 <- a refusal read as a real zero
// "12abc" -> 12 <- stops twelve bytes in
// "" -> 0
// "-1" -> 1 <- the sign is discarded
// "99999999999999999999" -> 7766279631452241919 <- wraps, in silence
//
// The last two are the ones no amount of care at the call site could recover
// from, because the value it receives is a plausible number.
TEST(ContentLength, AcceptsAWellFormedValue) {
ASSERT_TRUE(https::parse_content_length("135").has_value());
EXPECT_EQ(*https::parse_content_length("135"), 135);
// A field value may carry optional whitespace on either side.
ASSERT_TRUE(https::parse_content_length(" 135\t").has_value());
EXPECT_EQ(*https::parse_content_length(" 135\t"), 135);
}
// The one a salvaging parser cannot express. `abc` used to yield 0, which is
// indistinguishable from a server that genuinely declared an empty body — and
// the two call for opposite behaviour.
TEST(ContentLength, AZeroIsDistinguishableFromARefusal) {
ASSERT_TRUE(https::parse_content_length("0").has_value());
EXPECT_EQ(*https::parse_content_length("0"), 0);
EXPECT_FALSE(https::parse_content_length("abc").has_value());
}
TEST(ContentLength, RejectsEmptyTrailingGarbageAndOverflow) {
EXPECT_FALSE(https::parse_content_length("").has_value());
EXPECT_FALSE(https::parse_content_length(" ").has_value());
// `12abc` used to be 12: the reader would then stop twelve bytes in and
// leave the rest of the body to be read as the next response.
EXPECT_FALSE(https::parse_content_length("12abc").has_value());
EXPECT_FALSE(https::parse_content_length("-1").has_value());
EXPECT_FALSE(https::parse_content_length("+1").has_value());
// Past the width of the accumulator, which used to wrap in silence.
EXPECT_FALSE(https::parse_content_length("99999999999999999999").has_value());
}
// A value that fits in 64 bits but not in 32. `send_impl` held this in an `int`
// and truncated 2^32 to 0 in silence, which read as "the body is empty".
TEST(ContentLength, KeepsAValuePastThirtyTwoBits) {
auto parsed = https::parse_content_length("4294967296");
ASSERT_TRUE(parsed.has_value());
EXPECT_EQ(*parsed, 4294967296LL);
}
// ── the bounded error-body buffer ────────────────────────────────────────────
TEST(StreamErrorBody, KeepsEverythingWhileUnderLimit) {
std::string buffer;
EXPECT_TRUE(https::append_within_limit(buffer, "abc", 8));
EXPECT_TRUE(https::append_within_limit(buffer, "de", 8));
EXPECT_EQ(buffer, "abcde");
}
TEST(StreamErrorBody, TruncatesTheChunkThatCrossesTheLimit) {
std::string buffer = "abc";
EXPECT_FALSE(https::append_within_limit(buffer, "defgh", 5));
EXPECT_EQ(buffer, "abcde");
}
TEST(StreamErrorBody, RefusesFurtherDataOnceFull) {
std::string buffer = "abcde";
EXPECT_FALSE(https::append_within_limit(buffer, "f", 5));
EXPECT_EQ(buffer, "abcde");
// A zero limit must not append anything, not even an empty append.
std::string empty;
EXPECT_FALSE(https::append_within_limit(empty, "a", 0));
EXPECT_TRUE(empty.empty());
}
// ── the response contract ────────────────────────────────────────────────────
// `bodyComplete` defaults to true, so code that never looks at it behaves as it
// did. That default is the whole of the compatibility claim for this field.
TEST(ResponseContract, ABodyIsCompleteUntilSomethingSaysOtherwise) {
https::HttpResponse response;
EXPECT_TRUE(response.bodyComplete);
EXPECT_TRUE(response.bodyError.empty());
}
// `ok()` answers what the server said, and deliberately does not consult
// `bodyComplete`. Making it stricter would change what existing `if (res.ok())`
// means without changing what it says.
TEST(ResponseContract, OkReportsTheStatusCodeAndNotTheTransfer) {
https::HttpResponse response;
response.statusCode = 200;
response.bodyComplete = false;
response.bodyError = "Connection closed before the declared length";
EXPECT_TRUE(response.ok());
EXPECT_FALSE(response.bodyComplete);
}
TEST(DownloadResultContract, CarriesTransferAndResponseMetadata) {
https::DownloadToFileResult result;
result.bytesWritten = 42;
result.expectedBytes = 42;
result.finalUrl = "https://example.test/final";
result.etag = "\"abc\"";
result.lastModified = "Wed, 21 Oct 2015 07:28:00 GMT";
EXPECT_EQ(result.bytesWritten, 42);
ASSERT_TRUE(result.expectedBytes.has_value());
EXPECT_EQ(*result.expectedBytes, 42);
EXPECT_EQ(result.finalUrl, "https://example.test/final");
EXPECT_EQ(result.etag, "\"abc\"");
EXPECT_FALSE(result.lastModified.empty());
}
You can’t perform that action at this time.
