AuthInterceptor previously returned application/json for every rejection,
which is wrong for browser navigation: the user followed a /set-password
link and saw a raw {"status":"Unauthorized"} blob.
Add wantsJson() negotiation (path /api/* OR X-Requested-With OR Accept
prefers application/json over text/html) and an IAuthPolicy hook
unauthenticatedRedirect(path) that lets consumers bounce browser
navigations to a landing/login page. JSON callers (fetch/axios) still
get JSON 401/403. Default policy returns nullopt → minimal HTML error
page, never raw JSON to a browser.
Same hook covers both 401 and 403 (decision Option A on the issue) so
consumers wire one redirect target for both unauth and forbidden cases.
Bootstrap a minimal test harness (decision Option T2): CMake option
OATPP_AUTHKIT_BUILD_TESTS gates enable_testing() + a tests subdir.
Adds test_negotiation covering wantsJson + urlEncode. No third-party
test framework — assertions use <cassert> + a tiny REQUIRE macro so the
suite stays dependency-free for future tests.
Closes #2
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
77 lines
2.9 KiB
C++
77 lines
2.9 KiB
C++
// Tests for AuthInterceptor::wantsJson + urlEncode (the negotiation primitives
|
|
// that decide whether a 401/403 returns JSON vs HTML/redirect).
|
|
//
|
|
// Kept dependency-free on purpose — the harness exists so future tests have
|
|
// somewhere to land, not to pull in doctest/Catch2.
|
|
|
|
#include "oatpp-authkit/auth/AuthInterceptor.hpp"
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
|
|
namespace {
|
|
|
|
int g_failures = 0;
|
|
|
|
#define REQUIRE(expr) do { \
|
|
if (!(expr)) { \
|
|
std::fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \
|
|
++g_failures; \
|
|
} \
|
|
} while (0)
|
|
|
|
using oatpp_authkit::AuthInterceptor;
|
|
|
|
void test_wantsJson_api_path() {
|
|
// /api/* always wants JSON, no matter what Accept says.
|
|
REQUIRE( AuthInterceptor::wantsJson("/api/users", "", "text/html"));
|
|
REQUIRE( AuthInterceptor::wantsJson("/api/", "", ""));
|
|
}
|
|
|
|
void test_wantsJson_xrequested_with() {
|
|
// Explicit AJAX wins regardless of path/Accept.
|
|
REQUIRE( AuthInterceptor::wantsJson("/admin", "XMLHttpRequest", "text/html"));
|
|
}
|
|
|
|
void test_wantsJson_accept_header() {
|
|
// application/json without text/html → JSON.
|
|
REQUIRE( AuthInterceptor::wantsJson("/admin", "", "application/json"));
|
|
// text/html present → browser navigation.
|
|
REQUIRE(!AuthInterceptor::wantsJson("/admin", "", "text/html,application/xhtml+xml"));
|
|
REQUIRE(!AuthInterceptor::wantsJson("/admin", "", "text/html,application/json"));
|
|
// No Accept → assume browser (HTML/redirect).
|
|
REQUIRE(!AuthInterceptor::wantsJson("/set-password", "", ""));
|
|
}
|
|
|
|
void test_wantsJson_set_password_browser() {
|
|
// The motivating regression: a browser following the password-reset link
|
|
// must NOT be served JSON. (Path is public so it shouldn't reach this in
|
|
// normal flow, but if auth ever rejects it the user sees HTML/redirect.)
|
|
REQUIRE(!AuthInterceptor::wantsJson("/set-password",
|
|
"",
|
|
"text/html,application/xhtml+xml,application/xml;q=0.9"));
|
|
}
|
|
|
|
void test_urlEncode() {
|
|
REQUIRE(AuthInterceptor::urlEncode("/admin") == "%2Fadmin");
|
|
REQUIRE(AuthInterceptor::urlEncode("/set-password?t=1")== "%2Fset-password%3Ft%3D1");
|
|
REQUIRE(AuthInterceptor::urlEncode("abc-_.~123") == "abc-_.~123");
|
|
REQUIRE(AuthInterceptor::urlEncode(" ") == "%20");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
test_wantsJson_api_path();
|
|
test_wantsJson_xrequested_with();
|
|
test_wantsJson_accept_header();
|
|
test_wantsJson_set_password_browser();
|
|
test_urlEncode();
|
|
if (g_failures) {
|
|
std::fprintf(stderr, "%d test(s) failed\n", g_failures);
|
|
return 1;
|
|
}
|
|
std::puts("ok");
|
|
return 0;
|
|
}
|