|
39 | 39 | #include <algorithm> |
40 | 40 | #include <functional> |
41 | 41 | #include <string> |
| 42 | +#include <string_view> |
42 | 43 |
|
43 | 44 | #include "./httpserver.hpp" |
44 | 45 | #include "httpserver/create_test_request.hpp" |
@@ -164,45 +165,97 @@ LT_BEGIN_AUTO_TEST(hooks_log_access_alias_slot_suite, |
164 | 165 | LT_CHECK(captured.find("GET") != std::string::npos); |
165 | 166 | LT_END_AUTO_TEST(alias_sanitizes_control_chars_in_method) |
166 | 167 |
|
167 | | -// Re-registration: calling log_access a second time on the builder |
168 | | -// replaces the previous callable. At v2.0 the only write point is |
169 | | -// webserver construction (write-once-at-construction contract); runtime |
170 | | -// re-registration via a setter is deferred to a future task. |
171 | | -// This test pins the replace semantics at construction time. |
| 168 | +// TASK-066: alias slots are construction-time-only. The previous test on |
| 169 | +// this site (`log_access_second_registration_replaces_first`) simulated |
| 170 | +// re-registration by constructing two webservers and asserting they did |
| 171 | +// not pollute each other -- but that was not a runtime "replace" |
| 172 | +// semantics pin, because no runtime replacement path exists or is |
| 173 | +// planned (DR-012, §4.10). The two tests below replace that simulated |
| 174 | +// scenario with the real contract: alias slots are written exactly once |
| 175 | +// at webserver construction and are immutable thereafter; the public |
| 176 | +// runtime extension surface for both phases is add_hook(), which lives |
| 177 | +// in the per-phase user vector and never touches the alias slot. |
172 | 178 | LT_BEGIN_AUTO_TEST(hooks_log_access_alias_slot_suite, |
173 | | - log_access_second_registration_replaces_first) |
174 | | - int first_calls = 0; |
175 | | - int second_calls = 0; |
176 | | - // Simulate re-registration by creating two builders and checking that |
177 | | - // only the callable set on the webserver used for construction is stored. |
178 | | - webserver ws1{create_webserver(8244) |
179 | | - .log_access([&first_calls](const std::string&) { ++first_calls; })}; |
180 | | - webserver ws2{create_webserver(8245) |
181 | | - .log_access([&second_calls](const std::string&) { ++second_calls; })}; |
182 | | - |
183 | | - // Each webserver holds its own callable; neither pollutes the other. |
184 | | - auto* impl1 = impl_of(ws1); |
185 | | - auto* impl2 = impl_of(ws2); |
186 | | - LT_CHECK(static_cast<bool>(impl1->log_access_alias_)); |
187 | | - LT_CHECK(static_cast<bool>(impl2->log_access_alias_)); |
188 | | - |
189 | | - // Invoke each alias independently. |
190 | | - http_request req1 = |
| 179 | + log_access_alias_is_immutable_after_construction) |
| 180 | + int direct_calls = 0; |
| 181 | + int user_hook_calls = 0; |
| 182 | + webserver ws{create_webserver(8244) |
| 183 | + .log_access([&direct_calls](const std::string&) { ++direct_calls; })}; |
| 184 | + auto* impl = impl_of(ws); |
| 185 | + |
| 186 | + // (a) Construction wires the slot exactly once. |
| 187 | + LT_CHECK(static_cast<bool>(impl->log_access_alias_)); |
| 188 | + LT_CHECK_EQ(impl->hooks_response_sent_.size(), |
| 189 | + static_cast<std::size_t>(0)); |
| 190 | + |
| 191 | + // (b) Adding a user response_sent hook grows the vector but leaves |
| 192 | + // the alias slot untouched -- the public way to add observation at |
| 193 | + // response_sent post-construction is add_hook(), which routes into |
| 194 | + // the user vector and never reseats the alias slot. |
| 195 | + auto h = ws.add_hook(hook_phase::response_sent, |
| 196 | + std::function<void(const response_sent_ctx&)>( |
| 197 | + [&user_hook_calls](const response_sent_ctx&) { |
| 198 | + ++user_hook_calls; |
| 199 | + })); |
| 200 | + LT_CHECK(static_cast<bool>(impl->log_access_alias_)); |
| 201 | + LT_CHECK_EQ(impl->hooks_response_sent_.size(), |
| 202 | + static_cast<std::size_t>(1)); |
| 203 | + |
| 204 | + // (c) Removing the user hook leaves the alias slot untouched -- the |
| 205 | + // slot is not under user control via the hook bus. |
| 206 | + h.remove(); |
| 207 | + LT_CHECK(static_cast<bool>(impl->log_access_alias_)); |
| 208 | + LT_CHECK_EQ(impl->hooks_response_sent_.size(), |
| 209 | + static_cast<std::size_t>(0)); |
| 210 | + |
| 211 | + // (d) Direct invocation still reaches the construction-time callable. |
| 212 | + http_request req = |
191 | 213 | create_test_request().path("/a").method("GET").build(); |
192 | | - http_request req2 = |
193 | | - create_test_request().path("/b").method("GET").build(); |
194 | | - response_sent_ctx ctx1{}; |
195 | | - ctx1.request = &req1; |
196 | | - response_sent_ctx ctx2{}; |
197 | | - ctx2.request = &req2; |
198 | | - |
199 | | - impl1->log_access_alias_(ctx1); |
200 | | - impl2->log_access_alias_(ctx2); |
201 | | - |
202 | | - // Only the callable stored on each respective webserver was invoked. |
203 | | - LT_CHECK_EQ(first_calls, 1); |
204 | | - LT_CHECK_EQ(second_calls, 1); |
205 | | -LT_END_AUTO_TEST(log_access_second_registration_replaces_first) |
| 214 | + response_sent_ctx ctx{}; |
| 215 | + ctx.request = &req; |
| 216 | + impl->log_access_alias_(ctx); |
| 217 | + LT_CHECK_EQ(direct_calls, 1); |
| 218 | + LT_CHECK_EQ(user_hook_calls, 0); |
| 219 | +LT_END_AUTO_TEST(log_access_alias_is_immutable_after_construction) |
| 220 | + |
| 221 | +LT_BEGIN_AUTO_TEST(hooks_log_access_alias_slot_suite, |
| 222 | + handler_exception_alias_is_immutable_after_construction) |
| 223 | + // Mirror of the log_access pin above for handler_exception_alias_. |
| 224 | + // Construction wires the slot; user add_hook() at handler_exception |
| 225 | + // grows hooks_handler_exception_ but does not displace or clear the |
| 226 | + // alias slot. Removing the user hook leaves the alias slot intact. |
| 227 | + webserver ws{create_webserver(8244) |
| 228 | + .internal_error_handler( |
| 229 | + [](const httpserver::http_request&, std::string_view) |
| 230 | + -> httpserver::http_response { |
| 231 | + return httpserver::http_response::string("oops"); |
| 232 | + })}; |
| 233 | + auto* impl = impl_of(ws); |
| 234 | + |
| 235 | + // (a) Construction wires the alias slot exactly once. |
| 236 | + LT_CHECK(static_cast<bool>(impl->handler_exception_alias_)); |
| 237 | + LT_CHECK_EQ(impl->hooks_handler_exception_.size(), |
| 238 | + static_cast<std::size_t>(0)); |
| 239 | + |
| 240 | + // (b) User add_hook(handler_exception, ...) grows the vector; alias |
| 241 | + // slot remains set independently. |
| 242 | + auto h = ws.add_hook(hook_phase::handler_exception, |
| 243 | + std::function<httpserver::hook_action( |
| 244 | + const httpserver::handler_exception_ctx&)>( |
| 245 | + [](const httpserver::handler_exception_ctx&) { |
| 246 | + return httpserver::hook_action::pass(); |
| 247 | + })); |
| 248 | + LT_CHECK(static_cast<bool>(impl->handler_exception_alias_)); |
| 249 | + LT_CHECK_EQ(impl->hooks_handler_exception_.size(), |
| 250 | + static_cast<std::size_t>(1)); |
| 251 | + |
| 252 | + // (c) Removing the user hook does not clear the alias slot -- the |
| 253 | + // slot is not under user control via the hook bus. |
| 254 | + h.remove(); |
| 255 | + LT_CHECK(static_cast<bool>(impl->handler_exception_alias_)); |
| 256 | + LT_CHECK_EQ(impl->hooks_handler_exception_.size(), |
| 257 | + static_cast<std::size_t>(0)); |
| 258 | +LT_END_AUTO_TEST(handler_exception_alias_is_immutable_after_construction) |
206 | 259 |
|
207 | 260 | LT_BEGIN_AUTO_TEST_ENV() |
208 | 261 | AUTORUN_TESTS() |
|
0 commit comments