Skip to content

Commit 7d7146b

Browse files
etrclaude
andcommitted
TASK-024: failing tests for register_path/register_prefix split (RED)
Adds test/unit/webserver_register_path_prefix_test.cpp pinning both the compile-time signature contract and the runtime matching semantics for the new public API: - register_path / register_prefix exist with unique_ptr<T> and shared_ptr<http_resource> overloads, returning void. - unregister_path / unregister_prefix exist, returning void. - Negative SFINAE pin: the bool-family register_resource overloads (both unique_ptr and shared_ptr) must be removed (acceptance criterion #1, "grep ... bool" returns no results). - Runtime: register_prefix matches a longer URL; register_path does not; parameterized exact paths bind args; unregister_path / unregister_prefix / the umbrella unregister_resource alias all 404 after removal; the [[deprecated]] register_resource forwarder still serves and behaves like register_path. Wired into test/Makefile.am as the webserver_register_path_prefix check_PROGRAMS entry. Currently RED: register_path / register_prefix do not exist, and the bool-family static_assert sentinels still see the old overloads. Phase 2 (GREEN) lands the implementation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a30a307 commit 7d7146b

2 files changed

Lines changed: 324 additions & 1 deletion

File tree

test/Makefile.am

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ LDADD += -lcurl
2626

2727
AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ -DHTTPSERVER_COMPILATION
2828
METASOURCES = AUTO
29-
check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource http_response create_webserver new_response_types daemon_info uri_log feature_unavailable header_hygiene_iovec header_hygiene iovec_entry http_method constants body http_response_sbo http_response_factories webserver_pimpl http_request_pimpl create_test_request http_request_arena http_request_const_getters http_request_tls_accessors webserver_register_smartptr
29+
check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource http_response create_webserver new_response_types daemon_info uri_log feature_unavailable header_hygiene_iovec header_hygiene iovec_entry http_method constants body http_response_sbo http_response_factories webserver_pimpl http_request_pimpl create_test_request http_request_arena http_request_const_getters http_request_tls_accessors webserver_register_smartptr webserver_register_path_prefix
3030

3131
MOSTLYCLEANFILES = *.gcda *.gcno *.gcov
3232

@@ -156,6 +156,18 @@ http_request_tls_accessors_LDADD =
156156
# direct link needed.
157157
webserver_register_smartptr_SOURCES = unit/webserver_register_smartptr_test.cpp
158158

159+
# webserver_register_path_prefix: TASK-024. Compile-time signature
160+
# assertions that register_path / register_prefix exist with both
161+
# unique_ptr and shared_ptr overloads, that unregister_path /
162+
# unregister_prefix exist, and that the bool-family register_resource
163+
# overload has been removed (negative SFINAE pin for acceptance criterion
164+
# #1). Runtime tests: register_prefix matches a longer URL; register_path
165+
# does not; parameterized exact paths bind their args; unregister_path /
166+
# unregister_prefix / the umbrella unregister_resource alias all 404 the
167+
# route after removal; the [[deprecated]] register_resource forwarder
168+
# still serves and behaves like register_path.
169+
webserver_register_path_prefix_SOURCES = unit/webserver_register_path_prefix_test.cpp
170+
159171
noinst_HEADERS = littletest.hpp
160172
AM_CXXFLAGS += -Wall -fPIC -Wno-overloaded-virtual
161173

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011-2026 Sebastiano Merlino
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
18+
USA
19+
*/
20+
21+
// TASK-024: register_path / register_prefix split.
22+
//
23+
// Goal: prefix-vs-exact matching is now an explicit named choice, not a
24+
// positional bool flag.
25+
//
26+
// - register_path(path, ptr) -> exact match (does NOT match a longer URL)
27+
// - register_prefix(path, ptr) -> prefix match (matches the path and all
28+
// children of it)
29+
// - register_resource(path, ptr) is kept as a [[deprecated]] alias for
30+
// register_path so TASK-023-era call sites still compile.
31+
// - The 3-arg `bool family` overloads of register_resource are gone.
32+
//
33+
// This TU pins both the compile-time signature contract (the new methods
34+
// exist with the right shape; the bool-family overload is removed) and
35+
// the runtime matching semantics (real curl round-trips against a
36+
// running webserver).
37+
38+
#include <curl/curl.h>
39+
40+
#include <memory>
41+
#include <string>
42+
#include <type_traits>
43+
#include <utility>
44+
45+
#include "./httpserver.hpp"
46+
#include "./littletest.hpp"
47+
48+
using httpserver::create_webserver;
49+
using httpserver::http_request;
50+
using httpserver::http_resource;
51+
using httpserver::http_response;
52+
using httpserver::webserver;
53+
54+
#define PORT 8180
55+
56+
namespace {
57+
58+
size_t writefunc(void* ptr, size_t size, size_t nmemb, std::string* s) {
59+
s->append(reinterpret_cast<char*>(ptr), size * nmemb);
60+
return size * nmemb;
61+
}
62+
63+
class ok_resource : public http_resource {
64+
public:
65+
std::shared_ptr<http_response> render_get(const http_request&) override {
66+
return std::make_shared<http_response>(http_response::string("OK"));
67+
}
68+
};
69+
70+
class echo_id_resource : public http_resource {
71+
public:
72+
std::shared_ptr<http_response> render_get(const http_request& req) override {
73+
std::string body = "id=";
74+
body.append(req.get_arg("id"));
75+
return std::make_shared<http_response>(http_response::string(body));
76+
}
77+
};
78+
79+
// Curl helper: GET url, return (response_code, body). Body is empty if curl
80+
// fails. The webserver must already be started.
81+
struct fetch_result {
82+
long response_code;
83+
std::string body;
84+
};
85+
86+
fetch_result fetch(const std::string& url) {
87+
fetch_result fr{0, {}};
88+
CURL* curl = curl_easy_init();
89+
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
90+
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
91+
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc);
92+
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &fr.body);
93+
curl_easy_perform(curl);
94+
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &fr.response_code);
95+
curl_easy_cleanup(curl);
96+
return fr;
97+
}
98+
99+
} // namespace
100+
101+
// ---- Compile-time signature contract -----------------------------------
102+
103+
// (1) register_path(string, unique_ptr<http_resource>) exists, returns void.
104+
static_assert(std::is_same_v<
105+
decltype(std::declval<webserver&>().register_path(
106+
std::declval<const std::string&>(),
107+
std::declval<std::unique_ptr<http_resource>>())),
108+
void>,
109+
"register_path(const string&, unique_ptr<http_resource>) "
110+
"must exist and return void");
111+
112+
// (2) register_path(string, shared_ptr<http_resource>) exists, returns void.
113+
static_assert(std::is_same_v<
114+
decltype(std::declval<webserver&>().register_path(
115+
std::declval<const std::string&>(),
116+
std::declval<std::shared_ptr<http_resource>>())),
117+
void>,
118+
"register_path(const string&, shared_ptr<http_resource>) "
119+
"must exist and return void");
120+
121+
// (3) register_prefix(string, unique_ptr<http_resource>) exists, returns void.
122+
static_assert(std::is_same_v<
123+
decltype(std::declval<webserver&>().register_prefix(
124+
std::declval<const std::string&>(),
125+
std::declval<std::unique_ptr<http_resource>>())),
126+
void>,
127+
"register_prefix(const string&, unique_ptr<http_resource>) "
128+
"must exist and return void");
129+
130+
// (4) register_prefix(string, shared_ptr<http_resource>) exists, returns void.
131+
static_assert(std::is_same_v<
132+
decltype(std::declval<webserver&>().register_prefix(
133+
std::declval<const std::string&>(),
134+
std::declval<std::shared_ptr<http_resource>>())),
135+
void>,
136+
"register_prefix(const string&, shared_ptr<http_resource>) "
137+
"must exist and return void");
138+
139+
// (5) unregister_path / unregister_prefix exist, return void.
140+
static_assert(std::is_same_v<
141+
decltype(std::declval<webserver&>().unregister_path(
142+
std::declval<const std::string&>())),
143+
void>,
144+
"unregister_path(const string&) must exist and return void");
145+
static_assert(std::is_same_v<
146+
decltype(std::declval<webserver&>().unregister_prefix(
147+
std::declval<const std::string&>())),
148+
void>,
149+
"unregister_prefix(const string&) must exist and return void");
150+
151+
// (6) Negative SFINAE: the 3-arg bool-family overload of register_resource
152+
// must be gone. (Acceptance criterion #1 of TASK-024 pinned at compile
153+
// time.) The probe expression is a call with a trailing `bool` arg; if
154+
// such an overload existed, the call would be well-formed and ::value
155+
// would flip to true.
156+
template <typename, typename = void>
157+
struct has_bool_family_register : std::false_type {};
158+
159+
template <typename WS>
160+
struct has_bool_family_register<WS, std::void_t<
161+
decltype(std::declval<WS&>().register_resource(
162+
std::declval<const std::string&>(),
163+
std::declval<std::shared_ptr<http_resource>>(),
164+
std::declval<bool>()))>> : std::true_type {};
165+
166+
static_assert(!has_bool_family_register<webserver>::value,
167+
"the bool-family register_resource overload must be removed");
168+
169+
template <typename, typename = void>
170+
struct has_bool_family_register_unique : std::false_type {};
171+
172+
template <typename WS>
173+
struct has_bool_family_register_unique<WS, std::void_t<
174+
decltype(std::declval<WS&>().register_resource(
175+
std::declval<const std::string&>(),
176+
std::declval<std::unique_ptr<http_resource>>(),
177+
std::declval<bool>()))>> : std::true_type {};
178+
179+
static_assert(!has_bool_family_register_unique<webserver>::value,
180+
"the bool-family register_resource unique_ptr overload must be removed");
181+
182+
// ---- Runtime behaviour tests -------------------------------------------
183+
184+
LT_BEGIN_SUITE(webserver_register_path_prefix_suite)
185+
void set_up() {}
186+
void tear_down() {}
187+
LT_END_SUITE(webserver_register_path_prefix_suite)
188+
189+
// register_prefix: a longer URL (child of the registered path) must match
190+
// the resource and serve its body.
191+
LT_BEGIN_AUTO_TEST(webserver_register_path_prefix_suite,
192+
register_prefix_matches_longer_path)
193+
webserver ws = create_webserver(PORT);
194+
ws.register_prefix("/static", std::make_shared<ok_resource>());
195+
ws.start(false);
196+
197+
fetch_result fr = fetch("localhost:8180/static/anything/here");
198+
LT_CHECK_EQ(fr.response_code, 200);
199+
LT_CHECK_EQ(fr.body, std::string("OK"));
200+
201+
ws.stop();
202+
LT_END_AUTO_TEST(register_prefix_matches_longer_path)
203+
204+
// register_path: a longer URL must NOT match (404).
205+
LT_BEGIN_AUTO_TEST(webserver_register_path_prefix_suite,
206+
register_path_does_not_match_longer_path)
207+
webserver ws = create_webserver(PORT + 1);
208+
ws.register_path("/static", std::make_shared<ok_resource>());
209+
ws.start(false);
210+
211+
fetch_result fr = fetch("localhost:8181/static/anything/here");
212+
LT_CHECK_EQ(fr.response_code, 404);
213+
214+
ws.stop();
215+
LT_END_AUTO_TEST(register_path_does_not_match_longer_path)
216+
217+
// register_path: parameterized exact path matches and binds the {id} arg.
218+
LT_BEGIN_AUTO_TEST(webserver_register_path_prefix_suite,
219+
register_path_parameterized_matches_exact)
220+
webserver ws = create_webserver(PORT + 2);
221+
ws.register_path("/users/{id}", std::make_shared<echo_id_resource>());
222+
ws.start(false);
223+
224+
fetch_result fr = fetch("localhost:8182/users/42");
225+
LT_CHECK_EQ(fr.response_code, 200);
226+
LT_CHECK_EQ(fr.body, std::string("id=42"));
227+
228+
ws.stop();
229+
LT_END_AUTO_TEST(register_path_parameterized_matches_exact)
230+
231+
// unregister_prefix removes only a prefix registration; subsequent GET 404s.
232+
LT_BEGIN_AUTO_TEST(webserver_register_path_prefix_suite,
233+
register_prefix_then_unregister_prefix_404s)
234+
webserver ws = create_webserver(PORT + 3);
235+
ws.register_prefix("/static", std::make_shared<ok_resource>());
236+
ws.start(false);
237+
238+
fetch_result before = fetch("localhost:8183/static/anything");
239+
LT_CHECK_EQ(before.response_code, 200);
240+
241+
ws.unregister_prefix("/static");
242+
243+
fetch_result after = fetch("localhost:8183/static/anything");
244+
LT_CHECK_EQ(after.response_code, 404);
245+
246+
ws.stop();
247+
LT_END_AUTO_TEST(register_prefix_then_unregister_prefix_404s)
248+
249+
// unregister_path removes only an exact registration; subsequent GET 404s.
250+
LT_BEGIN_AUTO_TEST(webserver_register_path_prefix_suite,
251+
register_path_then_unregister_path_404s)
252+
webserver ws = create_webserver(PORT + 4);
253+
ws.register_path("/foo", std::make_shared<ok_resource>());
254+
ws.start(false);
255+
256+
fetch_result before = fetch("localhost:8184/foo");
257+
LT_CHECK_EQ(before.response_code, 200);
258+
259+
ws.unregister_path("/foo");
260+
261+
fetch_result after = fetch("localhost:8184/foo");
262+
LT_CHECK_EQ(after.response_code, 404);
263+
264+
ws.stop();
265+
LT_END_AUTO_TEST(register_path_then_unregister_path_404s)
266+
267+
// The umbrella `unregister_resource(path)` must handle either kind; this
268+
// test uses two different URLs (one prefix, one path) to keep the
269+
// assertion well-defined.
270+
LT_BEGIN_AUTO_TEST(webserver_register_path_prefix_suite,
271+
unregister_resource_alias_handles_both_kinds)
272+
webserver ws = create_webserver(PORT + 5);
273+
ws.register_prefix("/p", std::make_shared<ok_resource>());
274+
ws.register_path("/x", std::make_shared<ok_resource>());
275+
ws.start(false);
276+
277+
LT_CHECK_EQ(fetch("localhost:8185/p/child").response_code, 200);
278+
LT_CHECK_EQ(fetch("localhost:8185/x").response_code, 200);
279+
280+
ws.unregister_resource("/p");
281+
ws.unregister_resource("/x");
282+
283+
LT_CHECK_EQ(fetch("localhost:8185/p/child").response_code, 404);
284+
LT_CHECK_EQ(fetch("localhost:8185/x").response_code, 404);
285+
286+
ws.stop();
287+
LT_END_AUTO_TEST(unregister_resource_alias_handles_both_kinds)
288+
289+
// The deprecated register_resource(path, ptr) forwarder must still compile
290+
// and behave like register_path (exact match, no longer-URL match).
291+
// Suppress the deprecation warning locally so the test binary still
292+
// builds with -Werror.
293+
LT_BEGIN_AUTO_TEST(webserver_register_path_prefix_suite,
294+
register_resource_deprecated_forwarder_behaves_like_register_path)
295+
webserver ws = create_webserver(PORT + 6);
296+
#pragma GCC diagnostic push
297+
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
298+
ws.register_resource("/d", std::make_shared<ok_resource>());
299+
#pragma GCC diagnostic pop
300+
ws.start(false);
301+
302+
LT_CHECK_EQ(fetch("localhost:8186/d").response_code, 200);
303+
// Exact-match behaviour: a longer URL must 404.
304+
LT_CHECK_EQ(fetch("localhost:8186/d/extra").response_code, 404);
305+
306+
ws.stop();
307+
LT_END_AUTO_TEST(register_resource_deprecated_forwarder_behaves_like_register_path)
308+
309+
LT_BEGIN_AUTO_TEST_ENV()
310+
AUTORUN_TESTS()
311+
LT_END_AUTO_TEST_ENV()

0 commit comments

Comments
 (0)