oatpp-authkit/test/test_json_serialization.cpp
Uwe Schuster f43f5f0633 #6: route ad-hoc JSON through ObjectMapper (Option A — DI everywhere, all-in-one)
- New dto/InternalDto.hpp with JsonErrorDto, WsEntityEventDto,
  WsPresenceUpdateDto, WsClientMsgDto.

- JsonErrorHandler: now takes a shared ObjectMapper (DI). Body built
  via writeToString on JsonErrorDto. Closes the audit's concrete bug
  where status.description was embedded raw — a Status with a `"`/`\\`
  in the description previously emitted invalid JSON.

- AuthInterceptor: takes an optional ObjectMapper ctor arg (defaults to
  a fresh mapper). makeForbidden's `msg` is now serialised via
  JsonErrorDto + ObjectMapper, so a `"` in a forbidden-reason no longer
  breaks the response envelope.

- Hub: process-wide sharedMapper() with optional setObjectMapper()
  override. buildPresenceMsg / notifyBooking / notifyPerson all go
  through ObjectMapper-emitted DTOs. User-supplied IDs / property IDs
  / usernames containing `"`/`\\`/control chars are now escaped.

- Listener: jsonStr/jsonInt regex parsers gone. handleMessage parses
  inbound frames via ObjectMapper::readFromString into WsClientMsgDto.
  Malformed JSON / nested objects / escaped quotes — previously silent
  corruption — now produce a clean drop of the frame.

- test/test_json_serialization.cpp: 4 cases pinning the round-trip
  behaviour (special chars in usernames, IDs, status.description, and
  malformed-input rejection).

Bump to 0.4.0 — ctor signatures changed (additive defaults, but the
behaviour of the JSON envelopes is now governed by ObjectMapper).

Closes #6

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:56:05 +02:00

97 lines
3.8 KiB
C++

// Tests for the #6 ObjectMapper migration — verifies that the JSON envelopes
// produced by JsonErrorHandler / Hub::buildEntityEvent / Hub::buildPresenceMsg
// escape special characters instead of emitting raw text. The previous
// hand-rolled concatenations broke when given a `"`/`\\`/control-char string.
// Avoid pulling Hub.hpp here — it includes oatpp-websocket, which is a
// separate optional dependency not necessarily on the test target's include
// path. The escaping behaviour we care about is purely a property of
// ObjectMapper round-tripping the InternalDto types, so we exercise the
// DTOs directly.
#include "oatpp-authkit/handler/JsonErrorHandler.hpp"
#include "oatpp-authkit/dto/InternalDto.hpp"
#include "oatpp/parser/json/mapping/ObjectMapper.hpp"
#include "oatpp/web/protocol/http/Http.hpp"
#include <cstdio>
#include <set>
#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)
void test_presence_dto_round_trips_special_chars() {
auto m = oatpp::parser::json::mapping::ObjectMapper::createShared();
auto dto = oatpp_authkit::dto::WsPresenceUpdateDto::createShared();
dto->type = oatpp::String("presence_update");
dto->booking_id = oatpp::String("id-with-\"-quote");
dto->users = {};
dto->users->push_back(oatpp::String("al\"ice"));
dto->users->push_back(oatpp::String("bo\\b"));
auto json = m->writeToString(dto);
auto rt = m->readFromString<oatpp::Object<oatpp_authkit::dto::WsPresenceUpdateDto>>(json);
REQUIRE(rt);
REQUIRE(std::string(*rt->booking_id) == "id-with-\"-quote");
REQUIRE(rt->users->size() == 2);
auto it = rt->users->begin();
REQUIRE(std::string(**it++) == "al\"ice");
REQUIRE(std::string(**it) == "bo\\b");
}
void test_entity_event_dto_round_trip() {
auto m = oatpp::parser::json::mapping::ObjectMapper::createShared();
auto dto = oatpp_authkit::dto::WsEntityEventDto::createShared();
dto->type = oatpp::String("booking_updated");
dto->id = oatpp::String("id-with-\"-and-\\");
auto json = m->writeToString(dto);
auto rt = m->readFromString<oatpp::Object<oatpp_authkit::dto::WsEntityEventDto>>(json);
REQUIRE(rt);
REQUIRE(std::string(*rt->id) == "id-with-\"-and-\\");
}
void test_client_msg_dto_rejects_malformed() {
auto m = oatpp::parser::json::mapping::ObjectMapper::createShared();
bool threw = false;
try {
m->readFromString<oatpp::Object<oatpp_authkit::dto::WsClientMsgDto>>(
oatpp::String("{not json"));
} catch (...) { threw = true; }
REQUIRE(threw);
}
void test_json_error_dto_round_trip() {
auto m = oatpp::parser::json::mapping::ObjectMapper::createShared();
auto dto = oatpp_authkit::dto::JsonErrorDto::createShared();
dto->status = oatpp::String("I'm a \"teapot\"");
dto->code = 418;
dto->message = oatpp::String("brew\nfailure");
auto json = m->writeToString(dto);
auto rt = m->readFromString<oatpp::Object<oatpp_authkit::dto::JsonErrorDto>>(json);
REQUIRE(rt);
REQUIRE(std::string(*rt->status) == "I'm a \"teapot\"");
REQUIRE(rt->code == 418);
REQUIRE(std::string(*rt->message) == "brew\nfailure");
}
} // namespace
int main() {
test_presence_dto_round_trips_special_chars();
test_entity_event_dto_round_trip();
test_client_msg_dto_rejects_malformed();
test_json_error_dto_round_trip();
std::printf("%s (%d failures)\n", g_failures ? "FAIL" : "OK", g_failures);
return g_failures ? 1 : 0;
}