Repository navigation
Expand file tree
/
Copy pathhttp_response_factories_test.cpp
More file actions
505 lines (459 loc) · 21.8 KB
/
Copy pathhttp_response_factories_test.cpp
File metadata and controls
505 lines (459 loc) · 21.8 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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
/*
This file is part of libhttpserver
Copyright (C) 2011-2019 Sebastiano Merlino
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
USA
*/
// Unit tests for the static factory functions on http_response.
//
// Each factory placement-news the corresponding detail::response_body subclass
// into the SBO buffer (or, in the future, onto the heap) and tags the
// response with the appropriate body_kind. Tests cover:
// * the public observable contract: kind(), get_status(),
// get_header() — the surface a v2 caller sees;
// * the SBO inline placement, asserted through the existing
// http_response_sbo_test_access friend so no new private members
// are exposed;
// * the lifetime guarantees for pipe fd ownership and the
// unauthorized() status + WWW-Authenticate header contract.
//
// The TU is built with -DHTTPSERVER_COMPILATION (set by the test
// AM_CPPFLAGS) so it can include httpserver/detail/response_body.hpp directly,
// matching http_response_sbo_test.cpp's pattern.
//
// Header hygiene note: this TU does NOT include <sys/uio.h>.
// http_response::iovec(...) must compile from user code
// without that header in scope; the umbrella header_hygiene tests
// guard the umbrella surface, and this file simply does not pull it
// in to give callers a working reference.
#include <microhttpd.h>
#include <unistd.h> // pipe, close (POSIX)
#include <fcntl.h>
#include <array>
#include <cerrno>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include "./httpserver.hpp" // public umbrella
#include "httpserver/detail/response_body.hpp" // private detail::response_body (test-only)
#include "./littletest.hpp"
using httpserver::body_kind;
using httpserver::http_response;
// http_response_sbo_test_access is the same friend struct used by
// http_response_sbo_test.cpp. Since we only need read access to the
// SBO inline flag and body kind here, we accept the cross-TU duplicate
// symbol rule by declaring (NOT defining) the struct here as a
// friend-only forward declaration is impossible. Instead, we re-define
// the struct in this TU's anonymous namespace via the friend hook
// already declared in http_response.hpp. Defining a non-anonymous
// struct in two TUs would be an ODR violation; using the friendship
// from http_response.hpp lets us define it once per TU under the
// httpserver namespace, with internal linkage via an anonymous helper.
namespace httpserver {
// The friend struct in http_response.hpp is named
// http_response_sbo_test_access. Defining it here in the httpserver
// namespace gives this TU access to the private SBO state. This is the
// same pattern used by http_response_sbo_test.cpp; both TUs are
// build-tree-only test sources, never linked together, so there is no
// ODR conflict at link time.
struct http_response_sbo_test_access {
static bool body_inline(http_response& r) noexcept {
return r.body_inline_;
}
static httpserver::detail::response_body* body_ptr(http_response& r) noexcept {
return r.body_;
}
static body_kind kind(http_response& r) noexcept { return r.kind_; }
};
} // namespace httpserver
namespace {
using SBO = httpserver::http_response_sbo_test_access;
} // namespace
LT_BEGIN_SUITE(http_response_factories_suite)
void set_up() {}
void tear_down() {}
LT_END_SUITE(http_response_factories_suite)
// -----------------------------------------------------------------------
// empty() — simplest factory; verifies kind() accessor + SBO placement.
// -----------------------------------------------------------------------
LT_BEGIN_AUTO_TEST(http_response_factories_suite, empty_factory)
http_response r = http_response::empty();
LT_CHECK_EQ(static_cast<int>(r.kind()),
static_cast<int>(body_kind::empty));
LT_CHECK_EQ(SBO::body_inline(r), true);
LT_ASSERT_NEQ(SBO::body_ptr(r),
static_cast<httpserver::detail::response_body*>(nullptr));
// Default status code: 204 No Content (matches v1 empty_response).
LT_CHECK_EQ(r.get_status(), 204);
LT_END_AUTO_TEST(empty_factory)
// -----------------------------------------------------------------------
// string() — kind() must report body_kind::string.
// -----------------------------------------------------------------------
LT_BEGIN_AUTO_TEST(http_response_factories_suite, string_factory_kind)
auto r = http_response::string("hi");
LT_CHECK_EQ(static_cast<int>(r.kind()),
static_cast<int>(body_kind::string));
LT_CHECK_EQ(SBO::body_inline(r), true);
LT_END_AUTO_TEST(string_factory_kind)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
string_factory_default_content_type)
auto r = http_response::string("hi");
LT_CHECK_EQ(r.get_header("Content-Type"), std::string("text/plain"));
LT_CHECK_EQ(r.get_status(), 200);
LT_END_AUTO_TEST(string_factory_default_content_type)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
string_factory_overridden_content_type)
auto r = http_response::string("{}", "application/json");
LT_CHECK_EQ(r.get_header("Content-Type"),
std::string("application/json"));
LT_END_AUTO_TEST(string_factory_overridden_content_type)
// -----------------------------------------------------------------------
// file() — opens at construction, missing path doesn't throw.
// -----------------------------------------------------------------------
LT_BEGIN_AUTO_TEST(http_response_factories_suite, file_factory_existing)
// test_content lives in test/ — same fixture response_body_test uses.
auto r = http_response::file("test_content");
LT_CHECK_EQ(static_cast<int>(r.kind()),
static_cast<int>(body_kind::file));
LT_CHECK_EQ(SBO::body_inline(r), true);
LT_CHECK_EQ(r.get_status(), 200);
LT_END_AUTO_TEST(file_factory_existing)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
file_factory_missing_path_does_not_throw)
// Mirrors v1 file_response semantics: bad path is observable at
// dispatch time (the materialized MHD_Response is null), not at
// construction time.
auto r = http_response::file("/no/such/path/should/exist");
LT_CHECK_EQ(static_cast<int>(r.kind()),
static_cast<int>(body_kind::file));
LT_END_AUTO_TEST(file_factory_missing_path_does_not_throw)
// -----------------------------------------------------------------------
// iovec() — must compile without <sys/uio.h> in scope.
// -----------------------------------------------------------------------
LT_BEGIN_AUTO_TEST(http_response_factories_suite, iovec_factory_kind)
static const char a[] = "abc";
static const char b[] = "defg";
std::array<httpserver::iovec_entry, 2> entries{{
{a, 3},
{b, 4},
}};
auto r = http_response::iovec(entries);
LT_CHECK_EQ(static_cast<int>(r.kind()),
static_cast<int>(body_kind::iovec));
LT_CHECK_EQ(r.get_status(), 200);
LT_END_AUTO_TEST(iovec_factory_kind)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
iovec_factory_deep_copies_span)
// Build a span over a temporary array; let the array go out of
// scope before we observe r. The factory's deep-copy must keep the
// body's iovec_entry vector valid.
//
// Span deep-copy / caller-buffer lifetime contract:
// http_response::iovec() copies the iovec_entry structs (base+len
// pairs) into an internal std::vector. The *entries* are owned by
// the response, but the *buffers they point to* are NOT copied —
// callers must keep their payload buffers alive until the response
// is dispatched (i.e. until the MHD send callback completes).
// http_response itself is move-only, so copy-prohibition does not
// apply; the invariant to test is that the internal vector survives
// after the caller's span goes out of scope.
auto r = []() {
std::array<httpserver::iovec_entry, 1> entries{{ {"x", 1} }};
return http_response::iovec(entries);
}();
LT_CHECK_EQ(static_cast<int>(r.kind()),
static_cast<int>(body_kind::iovec));
LT_END_AUTO_TEST(iovec_factory_deep_copies_span)
LT_BEGIN_AUTO_TEST(http_response_factories_suite, iovec_factory_empty_span)
// An iovec with zero entries must not crash, must have kind iovec,
// and the default status (200) must be preserved.
std::array<httpserver::iovec_entry, 0> entries{};
auto r = http_response::iovec(std::span<const httpserver::iovec_entry>(entries));
LT_CHECK_EQ(static_cast<int>(r.kind()),
static_cast<int>(body_kind::iovec));
LT_CHECK_EQ(r.get_status(), 200);
// iovec_response_body fits in the 64-byte SBO buffer — verify inline placement.
LT_CHECK_EQ(SBO::body_inline(r), true);
LT_END_AUTO_TEST(iovec_factory_empty_span)
LT_BEGIN_AUTO_TEST(http_response_factories_suite, iovec_factory_single_entry)
// A single-entry iovec must produce kind iovec and the default status.
static const char buf[] = "hello";
std::array<httpserver::iovec_entry, 1> entries{{ {buf, 5} }};
auto r = http_response::iovec(entries);
LT_CHECK_EQ(static_cast<int>(r.kind()),
static_cast<int>(body_kind::iovec));
LT_CHECK_EQ(r.get_status(), 200);
// iovec_response_body fits in the 64-byte SBO buffer — verify inline placement.
LT_CHECK_EQ(SBO::body_inline(r), true);
LT_END_AUTO_TEST(iovec_factory_single_entry)
// Pin: pipe() must accept exactly one argument (the fd). Any future
// reintroduction of a size_hint / chunk_size / Content-Length parameter
// is a deliberate API change and must update this assertion.
// Rationale: an accepted-but-ignored parameter teaches
// callers a lie; honoring it would require synthesising Content-Length
// without ground truth from the pipe fd.
//
// http_response::pipe is declared unconditionally (no platform guard), so
// this compile-time signature check has no platform dependency and runs
// on every CI lane, including Windows.
using pipe_fn_t = http_response (*)(int);
static_assert(std::is_same_v<decltype(&http_response::pipe), pipe_fn_t>,
"http_response::pipe must take exactly one parameter "
"(int fd); see TASK-063");
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
pipe_factory_signature_is_single_arg)
LT_CHECK_EQ(true, true); // littletest needs at least one runtime check
LT_END_AUTO_TEST(pipe_factory_signature_is_single_arg)
// -----------------------------------------------------------------------
// pipe() — owns the fd, destructor closes it when not materialized.
// reason: Windows uses _pipe()/CreatePipe() rather than POSIX ::pipe();
// MSYS2/mingw does not ship POSIX ::pipe(). See response_body_test.cpp for the
// same gate rationale. Gap tracked in test/PORTABILITY.md.
// -----------------------------------------------------------------------
#ifndef _WIN32
LT_BEGIN_AUTO_TEST(http_response_factories_suite, pipe_factory_kind)
int fds[2];
int rc = ::pipe(fds);
LT_ASSERT_EQ(rc, 0);
{
auto r = http_response::pipe(fds[0]);
LT_CHECK_EQ(static_cast<int>(r.kind()),
static_cast<int>(body_kind::pipe));
LT_CHECK_EQ(r.get_status(), 200);
}
// Destructor must have closed fds[0]; second close fails with EBADF.
int second = ::close(fds[0]);
LT_CHECK_EQ(second, -1);
LT_CHECK_EQ(errno, EBADF);
::close(fds[1]);
LT_END_AUTO_TEST(pipe_factory_kind)
#endif // !_WIN32
// -----------------------------------------------------------------------
// deferred() — type-erased producer; sentinel test mirrors response_body_test.
// -----------------------------------------------------------------------
LT_BEGIN_AUTO_TEST(http_response_factories_suite, deferred_factory_kind)
auto r = http_response::deferred(
[](std::uint64_t, char*, std::size_t) -> ssize_t {
return MHD_CONTENT_READER_END_OF_STREAM;
});
LT_CHECK_EQ(static_cast<int>(r.kind()),
static_cast<int>(body_kind::deferred));
LT_CHECK_EQ(SBO::body_inline(r), true);
LT_CHECK_EQ(r.get_status(), 200);
LT_END_AUTO_TEST(deferred_factory_kind)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
deferred_factory_releases_capture_on_destruction)
auto sentinel = std::make_shared<int>(42);
std::weak_ptr<int> w = sentinel;
{
auto r = http_response::deferred(
[s = std::move(sentinel)](std::uint64_t, char*,
std::size_t) -> ssize_t {
(void)s;
return MHD_CONTENT_READER_END_OF_STREAM;
});
LT_CHECK_EQ(w.expired(), false);
}
LT_CHECK_EQ(w.expired(), true);
LT_END_AUTO_TEST(deferred_factory_releases_capture_on_destruction)
// -----------------------------------------------------------------------
// unauthorized() — 401 status + WWW-Authenticate header.
// -----------------------------------------------------------------------
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
unauthorized_basic_status_and_header)
auto r = http_response::unauthorized("Basic", "myrealm");
LT_CHECK_EQ(r.get_status(),
httpserver::http::http_utils::http_unauthorized);
LT_CHECK_EQ(r.get_status(), 401);
// The rendered header value must match byte-for-byte.
LT_CHECK_EQ(r.get_header(httpserver::http::http_utils::http_header_www_authenticate),
std::string(R"(Basic realm="myrealm")"));
LT_END_AUTO_TEST(unauthorized_basic_status_and_header)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
unauthorized_digest_scheme_renders_in_header)
auto r = http_response::unauthorized("Digest", "myrealm");
// Status must be 401 for the Digest scheme, same as Basic.
LT_CHECK_EQ(r.get_status(), 401);
LT_CHECK_EQ(r.get_header(httpserver::http::http_utils::http_header_www_authenticate),
std::string(R"(Digest realm="myrealm")"));
LT_END_AUTO_TEST(unauthorized_digest_scheme_renders_in_header)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
unauthorized_kind_is_string_even_when_body_empty)
// The body slot literally holds a string_response_body (with empty content)
// so kind() must report body_kind::string. Forking on the empty
// case to report body_kind::empty would break the invariant.
auto r = http_response::unauthorized("Basic", "myrealm");
LT_CHECK_EQ(static_cast<int>(r.kind()),
static_cast<int>(body_kind::string));
LT_END_AUTO_TEST(unauthorized_kind_is_string_even_when_body_empty)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
unauthorized_with_explicit_body)
auto r = http_response::unauthorized("Basic", "myrealm",
"please log in");
LT_CHECK_EQ(static_cast<int>(r.kind()),
static_cast<int>(body_kind::string));
LT_CHECK_EQ(r.get_status(), 401);
LT_END_AUTO_TEST(unauthorized_with_explicit_body)
// -----------------------------------------------------------------------
// unauthorized() — header injection validation.
// CRLF sequences in scheme or realm must be
// rejected (std::invalid_argument) to prevent header injection (CWE-113).
// Double-quotes embedded in realm must be escaped per RFC 7235 §2.1
// (backslash-escape) so the quoted-string is syntactically valid.
// -----------------------------------------------------------------------
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
unauthorized_crlf_in_scheme_throws)
// CRLF in scheme must throw — caller error, not a runtime failure.
bool caught = false;
try {
auto r = http_response::unauthorized("Basic\r\nX-Injected: hdr",
"myrealm");
(void)r;
} catch (const std::invalid_argument&) {
caught = true;
}
LT_CHECK_EQ(caught, true);
LT_END_AUTO_TEST(unauthorized_crlf_in_scheme_throws)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
unauthorized_lf_in_scheme_throws)
bool caught = false;
try {
auto r = http_response::unauthorized("Basic\nEvil: hdr", "myrealm");
(void)r;
} catch (const std::invalid_argument&) {
caught = true;
}
LT_CHECK_EQ(caught, true);
LT_END_AUTO_TEST(unauthorized_lf_in_scheme_throws)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
unauthorized_cr_in_scheme_throws)
bool caught = false;
try {
auto r = http_response::unauthorized("Basic\r", "myrealm");
(void)r;
} catch (const std::invalid_argument&) {
caught = true;
}
LT_CHECK_EQ(caught, true);
LT_END_AUTO_TEST(unauthorized_cr_in_scheme_throws)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
unauthorized_nul_in_scheme_throws)
// NUL in scheme is equally dangerous — reject it.
bool caught = false;
try {
std::string s("Basic");
s.push_back('\0');
s += "evil";
auto r = http_response::unauthorized(std::string_view(s.data(),
s.size()),
"myrealm");
(void)r;
} catch (const std::invalid_argument&) {
caught = true;
}
LT_CHECK_EQ(caught, true);
LT_END_AUTO_TEST(unauthorized_nul_in_scheme_throws)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
unauthorized_crlf_in_realm_throws)
bool caught = false;
try {
auto r = http_response::unauthorized(
"Basic", "evil\r\nX-Injected: hdr");
(void)r;
} catch (const std::invalid_argument&) {
caught = true;
}
LT_CHECK_EQ(caught, true);
LT_END_AUTO_TEST(unauthorized_crlf_in_realm_throws)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
unauthorized_lf_in_realm_throws)
bool caught = false;
try {
auto r = http_response::unauthorized("Basic", "evil\nMore: hdr");
(void)r;
} catch (const std::invalid_argument&) {
caught = true;
}
LT_CHECK_EQ(caught, true);
LT_END_AUTO_TEST(unauthorized_lf_in_realm_throws)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
unauthorized_nul_in_realm_throws)
bool caught = false;
try {
std::string realm("my");
realm.push_back('\0');
realm += "realm";
auto r = http_response::unauthorized(
"Basic", std::string_view(realm.data(), realm.size()));
(void)r;
} catch (const std::invalid_argument&) {
caught = true;
}
LT_CHECK_EQ(caught, true);
LT_END_AUTO_TEST(unauthorized_nul_in_realm_throws)
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
unauthorized_double_quote_in_realm_is_escaped)
// RFC 7235 §2.1: double-quotes inside a quoted-string must be
// backslash-escaped. realm=foo"bar must produce
// WWW-Authenticate: Basic realm="foo\"bar"
auto r = http_response::unauthorized("Basic", R"(foo"bar)");
LT_CHECK_EQ(
r.get_header(httpserver::http::http_utils::http_header_www_authenticate),
std::string(R"(Basic realm="foo\"bar")"));
LT_END_AUTO_TEST(unauthorized_double_quote_in_realm_is_escaped)
// -----------------------------------------------------------------------
// Move smoke: factory results survive being returned from a function.
// Protects against a future regression of the noexcept move ctor.
// -----------------------------------------------------------------------
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
factory_move_preserves_kind_and_headers)
auto make = []() {
return http_response::string("payload", "text/html");
};
http_response r = make();
LT_CHECK_EQ(static_cast<int>(r.kind()),
static_cast<int>(body_kind::string));
LT_CHECK_EQ(r.get_header("Content-Type"), std::string("text/html"));
LT_CHECK_EQ(r.get_status(), 200);
// And one move-assign.
http_response other = http_response::empty();
other = std::move(r);
LT_CHECK_EQ(static_cast<int>(other.kind()),
static_cast<int>(body_kind::string));
LT_CHECK_EQ(other.get_status(), 200);
LT_END_AUTO_TEST(factory_move_preserves_kind_and_headers)
// -----------------------------------------------------------------------
// Zero-copy invariant: chained with_* calls on a factory's
// rvalue must not perturb the SBO placement. http_response::string(...)
// places a string_response_body inline in the SBO buffer; the && overloads of
// with_header / with_status return http_response&& (i.e. propagate the
// xvalue without an intermediate copy/move-construct), so the body
// pointer must remain inline through the chain.
// -----------------------------------------------------------------------
LT_BEGIN_AUTO_TEST(http_response_factories_suite,
factory_chain_keeps_body_inline_in_sbo)
auto r = http_response::string("hi")
.with_header("X-Foo", "bar")
.with_status(201);
LT_CHECK_EQ(SBO::body_inline(r), true);
LT_CHECK_EQ(static_cast<int>(SBO::kind(r)),
static_cast<int>(body_kind::string));
LT_END_AUTO_TEST(factory_chain_keeps_body_inline_in_sbo)
LT_BEGIN_AUTO_TEST_ENV()
AUTORUN_TESTS()
LT_END_AUTO_TEST_ENV()