-
-
Notifications
You must be signed in to change notification settings - Fork 191
Expand file tree
/
Copy pathv2_dispatch_contract_test.cpp
More file actions
324 lines (281 loc) · 12.8 KB
/
Copy pathv2_dispatch_contract_test.cpp
File metadata and controls
324 lines (281 loc) · 12.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
/*
This file is part of libhttpserver
Copyright (C) 2011-2026 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
*/
// v2 dispatch contract gate.
//
// This TU pins the *end-to-end* observable invariants the dispatch path
// must satisfy through the public webserver surface, BEFORE we cut over
// finalize_answer() from resolve_resource_for_request (v1) to
// resolve_resource_for_request_v2 (lookup_v2-backed). Each test fires a
// real HTTP request and asserts on response body / status / hook context.
//
// The four pinned invariants:
// 1. Parameterized routes: `/users/{id}` matched against `/users/42`
// populates `req.get_arg("id") == "42"`.
// 2. Prefix routes: `/static` matched against `/static/foo/bar` hits
// the registered resource and `ctx.matched->is_prefix == true`.
// 3. Exact routes: `/exact` returns `ctx.matched->is_prefix == false`.
// 4. Method mismatch: POST to a GET-only route still returns 405 and
// the route_resolved hook ctx still carries a non-null resource
// pointer (the resolve step ran; only the method check failed).
//
// All four currently pass against the v1 dispatch path. They MUST keep
// passing after the v2 cutover. Anchoring them HERE — pre-cutover — is
// the "safety net first" pattern that lets each subsequent step land
// without regression risk.
#include <curl/curl.h>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <functional>
#include <memory>
#include <string>
#include <thread>
#include "./httpserver.hpp"
#include "./littletest.hpp"
// wait_for_server_ready: poll the given port with an HTTP GET / until the
// server responds (any status), or until the deadline elapses. This avoids
// a fixed sleep that is either too short on slow/sanitizer CI builds or
// wastes 50 ms of wall time on every test invocation.
static void wait_for_server_ready(int port,
std::chrono::milliseconds deadline
= std::chrono::milliseconds(3000)) {
using clock = std::chrono::steady_clock;
auto end = clock::now() + deadline;
std::string url = "http://127.0.0.1:" + std::to_string(port) + "/";
while (clock::now() < end) {
CURL* c = curl_easy_init();
if (!c) break;
curl_easy_setopt(c, CURLOPT_URL, url.c_str());
curl_easy_setopt(c, CURLOPT_NOBODY, 1L);
curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT_MS, 50L);
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 50L);
CURLcode rc = curl_easy_perform(c);
curl_easy_cleanup(c);
if (rc == CURLE_OK) return;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
}
using httpserver::create_webserver;
using httpserver::hook_phase;
using httpserver::http_request;
using httpserver::http_response;
using httpserver::route_resolved_ctx;
using httpserver::webserver;
// 8260 is unused elsewhere in the test/ tree; the prior 8231 collided with
// test/integ/hooks_handler_exception_user_handler_throws_continues_chain.cpp
// and caused intermittent EADDRINUSE under `make check -j`.
#define PORT 8260
namespace {
size_t writefunc(void* ptr, size_t size, size_t nmemb, std::string* s) {
s->append(reinterpret_cast<char*>(ptr), size * nmemb);
return size * nmemb;
}
// Echo the `id` URL parameter back in the response body so the test
// can read it via the body stream. Used by the parameterized-route test.
class echo_id_resource : public httpserver::http_resource {
public:
http_response render_get(const http_request& req) override {
return http_response::string(std::string(req.get_arg("id")));
}
};
class hello_resource : public httpserver::http_resource {
public:
http_response render_get(const http_request&) override {
return http_response::string("OK");
}
};
// Performs a GET and returns body + status code.
struct response_capture {
long status = 0; // NOLINT(runtime/int)
std::string body;
};
response_capture do_get(const std::string& path) {
response_capture out;
CURL* curl = curl_easy_init();
std::string url = "http://127.0.0.1:" + std::to_string(PORT) + path;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &out.body);
curl_easy_perform(curl);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &out.status);
curl_easy_cleanup(curl);
return out;
}
// Performs a POST with no body and returns the status code.
long do_post_status(const std::string& path) { // NOLINT(runtime/int)
long status = 0; // NOLINT(runtime/int)
CURL* curl = curl_easy_init();
std::string url = "http://127.0.0.1:" + std::to_string(PORT) + path;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "");
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0L);
std::string sink;
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &sink);
curl_easy_perform(curl);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
curl_easy_cleanup(curl);
return status;
}
// Probe state for the route_resolved hook ctx. Captures the most-recent
// invocation's matched flags and resource pointer so we can assert them
// in the test bodies.
struct hook_probe {
std::atomic<std::size_t> calls{0};
std::atomic<bool> last_matched_engaged{false};
std::atomic<bool> last_is_prefix{false};
std::atomic<bool> last_resource_non_null{false};
};
} // namespace
LT_BEGIN_SUITE(v2_dispatch_contract_suite)
void set_up() {}
void tear_down() {}
LT_END_SUITE(v2_dispatch_contract_suite)
// Invariant 1: parameterized route — `/users/{id}` against `/users/42`
// must populate `req.get_arg("id") == "42"` (the v2 equivalent of
// apply_extracted_params).
LT_BEGIN_AUTO_TEST(v2_dispatch_contract_suite, parameterized_route_extracts_capture)
webserver ws{create_webserver(PORT)};
auto resource = std::make_shared<echo_id_resource>();
ws.register_path("/users/{id}", resource);
ws.start(false);
wait_for_server_ready(PORT);
response_capture r = do_get("/users/42");
ws.stop();
LT_CHECK_EQ(r.status, 200L);
LT_CHECK_EQ(r.body, std::string("42"));
LT_END_AUTO_TEST(parameterized_route_extracts_capture)
// Invariant 2: prefix route — `/static` matched against `/static/foo/bar`
// must hit and route_resolved ctx must carry is_prefix=true.
LT_BEGIN_AUTO_TEST(v2_dispatch_contract_suite, prefix_route_marks_is_prefix_true)
hook_probe probe;
webserver ws{create_webserver(PORT)};
auto h = ws.add_hook(hook_phase::route_resolved,
std::function<void(const route_resolved_ctx&)>(
[&probe](const route_resolved_ctx& ctx) {
probe.calls.fetch_add(1, std::memory_order_relaxed);
if (ctx.matched.has_value()) {
probe.last_matched_engaged.store(true,
std::memory_order_relaxed);
probe.last_is_prefix.store(ctx.matched->is_prefix,
std::memory_order_relaxed);
probe.last_resource_non_null.store(
ctx.resource != nullptr,
std::memory_order_relaxed);
}
}));
(void)h;
auto resource = std::make_shared<hello_resource>();
ws.register_prefix("/static", resource);
ws.start(false);
wait_for_server_ready(PORT);
response_capture r = do_get("/static/foo/bar");
ws.stop();
LT_CHECK_EQ(r.status, 200L);
LT_CHECK(probe.last_matched_engaged.load());
LT_CHECK_EQ(probe.last_is_prefix.load(), true);
LT_CHECK(probe.last_resource_non_null.load());
LT_END_AUTO_TEST(prefix_route_marks_is_prefix_true)
// Invariant 3: exact route — `/exact` hit must carry is_prefix=false.
LT_BEGIN_AUTO_TEST(v2_dispatch_contract_suite, exact_route_marks_is_prefix_false)
hook_probe probe;
webserver ws{create_webserver(PORT)};
auto h = ws.add_hook(hook_phase::route_resolved,
std::function<void(const route_resolved_ctx&)>(
[&probe](const route_resolved_ctx& ctx) {
probe.calls.fetch_add(1, std::memory_order_relaxed);
if (ctx.matched.has_value()) {
probe.last_matched_engaged.store(true,
std::memory_order_relaxed);
probe.last_is_prefix.store(ctx.matched->is_prefix,
std::memory_order_relaxed);
probe.last_resource_non_null.store(
ctx.resource != nullptr,
std::memory_order_relaxed);
}
}));
(void)h;
auto resource = std::make_shared<hello_resource>();
ws.register_path("/exact", resource);
ws.start(false);
wait_for_server_ready(PORT);
response_capture r = do_get("/exact");
ws.stop();
LT_CHECK_EQ(r.status, 200L);
LT_CHECK(probe.last_matched_engaged.load());
LT_CHECK_EQ(probe.last_is_prefix.load(), false);
LT_CHECK(probe.last_resource_non_null.load());
LT_END_AUTO_TEST(exact_route_marks_is_prefix_false)
// Invariant 4: method mismatch returns 405; the route_resolved hook ctx
// MUST still carry a non-null resource pointer because route resolution
// succeeded — the method check that produces 405 runs AFTER the lookup
// and uses the resolved resource's get_allowed_methods().
LT_BEGIN_AUTO_TEST(v2_dispatch_contract_suite, method_mismatch_still_resolves_route)
hook_probe probe;
webserver ws{create_webserver(PORT)};
auto h = ws.add_hook(hook_phase::route_resolved,
std::function<void(const route_resolved_ctx&)>(
[&probe](const route_resolved_ctx& ctx) {
probe.calls.fetch_add(1, std::memory_order_relaxed);
if (ctx.matched.has_value()) {
probe.last_matched_engaged.store(true,
std::memory_order_relaxed);
probe.last_is_prefix.store(ctx.matched->is_prefix,
std::memory_order_relaxed);
probe.last_resource_non_null.store(
ctx.resource != nullptr,
std::memory_order_relaxed);
}
}));
(void)h;
auto resource = std::make_shared<hello_resource>();
// Constrain the resource to GET-only so a POST against /get_only
// exercises the dispatch path's 405 branch — the lookup MUST resolve
// the resource (so the hook ctx carries it) and the method check
// that runs AFTER the lookup returns 405.
resource->disallow_all();
resource->set_allowing(httpserver::http_method::get, true);
ws.register_path("/get_only", resource);
ws.start(false);
wait_for_server_ready(PORT);
long post_status = do_post_status("/get_only"); // NOLINT(runtime/int)
ws.stop();
LT_CHECK_EQ(post_status, 405L);
LT_CHECK(probe.last_matched_engaged.load());
LT_CHECK(probe.last_resource_non_null.load());
// /get_only is an exact (non-prefix) route; is_prefix must be false
// in the 405 code path as well as in the 200 code path.
LT_CHECK_EQ(probe.last_is_prefix.load(), false);
LT_END_AUTO_TEST(method_mismatch_still_resolves_route)
// Invariant 5 (miss-path safety net): an unregistered path returns 404.
// If the dispatch path ever broke the miss-path (e.g. by always returning
// a default route), invariants 1-4 would silently still pass because
// they only test registered paths. This is the other side of the coin.
LT_BEGIN_AUTO_TEST(v2_dispatch_contract_suite, unregistered_path_returns_404)
webserver ws{create_webserver(PORT)};
auto resource = std::make_shared<hello_resource>();
ws.register_path("/registered", resource);
ws.start(false);
wait_for_server_ready(PORT);
response_capture r = do_get("/not_registered_at_all");
ws.stop();
LT_CHECK_EQ(r.status, 404L);
LT_END_AUTO_TEST(unregistered_path_returns_404)
LT_BEGIN_AUTO_TEST_ENV()
AUTORUN_TESTS()
LT_END_AUTO_TEST_ENV()