|
22 | 22 | #include <string> |
23 | 23 | #include <string_view> |
24 | 24 | #include <type_traits> |
| 25 | +#include <utility> |
25 | 26 |
|
26 | 27 | #include "./littletest.hpp" |
27 | 28 | #include "./httpserver.hpp" |
@@ -468,6 +469,138 @@ LT_BEGIN_AUTO_TEST(http_response_suite, get_header_view_reflects_replacement) |
468 | 469 | LT_CHECK_EQ(resp.get_header("K"), std::string_view("v2")); |
469 | 470 | LT_END_AUTO_TEST(get_header_view_reflects_replacement) |
470 | 471 |
|
| 472 | +// ----------------------------------------------------------------------- |
| 473 | +// TASK-012: fluent with_* setters return http_response& / http_response&& |
| 474 | +// (PRD-RSP-REQ-004). Tests below pin the new contract: |
| 475 | +// * the AC chain compiles end-to-end (factory_chain_compiles_and_works); |
| 476 | +// * lvalue chains return identity (lvalue_chain_returns_lvalue_ref); |
| 477 | +// * ref-qualifier dispatch is exact at the type level |
| 478 | +// (with_setters_return_types_are_ref_qualified); |
| 479 | +// * statement-form pre-TASK-012 callers still compile unchanged |
| 480 | +// (statement_form_with_setters_still_compile); |
| 481 | +// * with_status round-trips and is composition-safe |
| 482 | +// (with_status_changes_status_code, with_status_preserves_body_and_headers); |
| 483 | +// * mutation is observable through the returned reference |
| 484 | +// (mutation_observable_through_returned_ref); |
| 485 | +// * by-value string parameters are move-friendly (with_header_moves_string_args). |
| 486 | +// The SBO-inline / zero-copy invariant for the rvalue chain is verified |
| 487 | +// in test/unit/http_response_factories_test.cpp where the SBO friend |
| 488 | +// struct is already defined. |
| 489 | +// ----------------------------------------------------------------------- |
| 490 | + |
| 491 | +LT_BEGIN_AUTO_TEST(http_response_suite, factory_chain_compiles_and_works) |
| 492 | + auto r = http_response::string("hi") |
| 493 | + .with_header("X-Foo", "bar") |
| 494 | + .with_status(201); |
| 495 | + LT_CHECK_EQ(r.get_status(), 201); |
| 496 | + LT_CHECK_EQ(r.get_header("X-Foo"), std::string_view("bar")); |
| 497 | + LT_CHECK_EQ(r.get_header("Content-Type"), std::string_view("text/plain")); |
| 498 | + LT_CHECK_EQ(static_cast<int>(r.kind()), |
| 499 | + static_cast<int>(httpserver::body_kind::string)); |
| 500 | +LT_END_AUTO_TEST(factory_chain_compiles_and_works) |
| 501 | + |
| 502 | +LT_BEGIN_AUTO_TEST(http_response_suite, lvalue_chain_returns_lvalue_ref) |
| 503 | + http_response r = http_response::empty(); |
| 504 | + auto& ret = r.with_header("A", "1").with_footer("B", "2") |
| 505 | + .with_cookie("c", "3").with_status(202); |
| 506 | + LT_CHECK_EQ(&ret, &r); // Identity: returned ref must be *this. |
| 507 | + LT_CHECK_EQ(r.get_header("A"), std::string_view("1")); |
| 508 | + LT_CHECK_EQ(r.get_footer("B"), std::string_view("2")); |
| 509 | + LT_CHECK_EQ(r.get_cookie("c"), std::string_view("3")); |
| 510 | + LT_CHECK_EQ(r.get_status(), 202); |
| 511 | +LT_END_AUTO_TEST(lvalue_chain_returns_lvalue_ref) |
| 512 | + |
| 513 | +LT_BEGIN_AUTO_TEST(http_response_suite, with_setters_return_types_are_ref_qualified) |
| 514 | + using R = httpserver::http_response; |
| 515 | + // & overload returns R& |
| 516 | + static_assert(std::is_same_v< |
| 517 | + decltype(std::declval<R&>().with_header(std::string{}, std::string{})), |
| 518 | + R&>, "with_header() & must return http_response&"); |
| 519 | + static_assert(std::is_same_v< |
| 520 | + decltype(std::declval<R&>().with_footer(std::string{}, std::string{})), |
| 521 | + R&>, "with_footer() & must return http_response&"); |
| 522 | + static_assert(std::is_same_v< |
| 523 | + decltype(std::declval<R&>().with_cookie(std::string{}, std::string{})), |
| 524 | + R&>, "with_cookie() & must return http_response&"); |
| 525 | + static_assert(std::is_same_v< |
| 526 | + decltype(std::declval<R&>().with_status(0)), |
| 527 | + R&>, "with_status() & must return http_response&"); |
| 528 | + // && overload returns R&& |
| 529 | + static_assert(std::is_same_v< |
| 530 | + decltype(std::declval<R&&>().with_header(std::string{}, std::string{})), |
| 531 | + R&&>, "with_header() && must return http_response&&"); |
| 532 | + static_assert(std::is_same_v< |
| 533 | + decltype(std::declval<R&&>().with_footer(std::string{}, std::string{})), |
| 534 | + R&&>, "with_footer() && must return http_response&&"); |
| 535 | + static_assert(std::is_same_v< |
| 536 | + decltype(std::declval<R&&>().with_cookie(std::string{}, std::string{})), |
| 537 | + R&&>, "with_cookie() && must return http_response&&"); |
| 538 | + static_assert(std::is_same_v< |
| 539 | + decltype(std::declval<R&&>().with_status(0)), |
| 540 | + R&&>, "with_status() && must return http_response&&"); |
| 541 | + // Smoke runtime check so the suite still has at least one runtime |
| 542 | + // assertion (a static_assert-only test would still pass if removed). |
| 543 | + LT_CHECK_EQ(true, true); |
| 544 | +LT_END_AUTO_TEST(with_setters_return_types_are_ref_qualified) |
| 545 | + |
| 546 | +LT_BEGIN_AUTO_TEST(http_response_suite, statement_form_with_setters_still_compile) |
| 547 | + // Backward-compat: pre-TASK-012 callers wrote `r.with_X(k, v);` in |
| 548 | + // statement form, discarding the (then void) return. Switching to |
| 549 | + // a reference return must keep this form compiling unchanged. |
| 550 | + http_response resp = http_response::string("body"); |
| 551 | + resp.with_header("X-A", "1"); |
| 552 | + resp.with_footer("X-B", "2"); |
| 553 | + resp.with_cookie("c", "3"); |
| 554 | + resp.with_status(202); |
| 555 | + LT_CHECK_EQ(resp.get_header("X-A"), std::string_view("1")); |
| 556 | + LT_CHECK_EQ(resp.get_footer("X-B"), std::string_view("2")); |
| 557 | + LT_CHECK_EQ(resp.get_cookie("c"), std::string_view("3")); |
| 558 | + LT_CHECK_EQ(resp.get_status(), 202); |
| 559 | +LT_END_AUTO_TEST(statement_form_with_setters_still_compile) |
| 560 | + |
| 561 | +LT_BEGIN_AUTO_TEST(http_response_suite, with_status_changes_status_code) |
| 562 | + http_response r = http_response::string("body"); |
| 563 | + LT_CHECK_EQ(r.get_status(), 200); // factory default |
| 564 | + r.with_status(404); |
| 565 | + LT_CHECK_EQ(r.get_status(), 404); |
| 566 | + r.with_status(500); |
| 567 | + LT_CHECK_EQ(r.get_status(), 500); |
| 568 | +LT_END_AUTO_TEST(with_status_changes_status_code) |
| 569 | + |
| 570 | +LT_BEGIN_AUTO_TEST(http_response_suite, with_status_preserves_body_and_headers) |
| 571 | + auto r = http_response::string("payload", "application/json") |
| 572 | + .with_header("X-K", "v") |
| 573 | + .with_status(418); |
| 574 | + LT_CHECK_EQ(r.get_status(), 418); |
| 575 | + LT_CHECK_EQ(r.get_header("Content-Type"), |
| 576 | + std::string_view("application/json")); |
| 577 | + LT_CHECK_EQ(r.get_header("X-K"), std::string_view("v")); |
| 578 | + LT_CHECK_EQ(static_cast<int>(r.kind()), |
| 579 | + static_cast<int>(httpserver::body_kind::string)); |
| 580 | +LT_END_AUTO_TEST(with_status_preserves_body_and_headers) |
| 581 | + |
| 582 | +LT_BEGIN_AUTO_TEST(http_response_suite, mutation_observable_through_returned_ref) |
| 583 | + http_response r = http_response::empty(); |
| 584 | + auto& ret = r.with_header("X-Trace", "a"); |
| 585 | + LT_CHECK_EQ(ret.get_header("X-Trace"), std::string_view("a")); |
| 586 | + // And the rvalue chain leaves the result in the bound variable. |
| 587 | + auto r2 = http_response::empty().with_header("X-Trace", "b"); |
| 588 | + LT_CHECK_EQ(r2.get_header("X-Trace"), std::string_view("b")); |
| 589 | +LT_END_AUTO_TEST(mutation_observable_through_returned_ref) |
| 590 | + |
| 591 | +LT_BEGIN_AUTO_TEST(http_response_suite, with_header_moves_string_args) |
| 592 | + // By-value string parameters must accept rvalue inputs and forward |
| 593 | + // them into the underlying map. We don't assert on the moved-from |
| 594 | + // state of the source strings (the standard only guarantees "valid |
| 595 | + // but unspecified") — only that the value lands in the map intact. |
| 596 | + http_response r = http_response::empty(); |
| 597 | + std::string key = "X-Long-Header-Name-To-Avoid-SSO"; |
| 598 | + std::string value(64, 'v'); // > SSO threshold on libstdc++/libc++ |
| 599 | + r.with_header(std::move(key), std::move(value)); |
| 600 | + LT_CHECK_EQ(r.get_header("X-Long-Header-Name-To-Avoid-SSO"), |
| 601 | + std::string_view(std::string(64, 'v'))); |
| 602 | +LT_END_AUTO_TEST(with_header_moves_string_args) |
| 603 | + |
471 | 604 | LT_BEGIN_AUTO_TEST_ENV() |
472 | 605 | AUTORUN_TESTS() |
473 | 606 | LT_END_AUTO_TEST_ENV() |
0 commit comments