oatpp-authkit/test/test_temporal_field_traits.cpp
Uwe Schuster 1baff07b71 #10: TemporalFieldTraits<T> — decouple decorator from canonical column names
Replace hard-coded dto->entity_id/valid_from/valid_until accesses in
TemporalRepository with trait calls (F::entityId/validFrom/validUntil).
DTOs register canonical→actual member name mapping via
OATPP_AUTHKIT_REGISTER_TEMPORAL. Forgetting to register is a hard
compile error. ITemporalEntity marker is gone; the trait specialisation
carries the contract. Bumps version 0.4.0 → 0.5.0.

New test verifies the full save/close/history/softDelete flow against a
DTO whose columns are id/effective_from/effective_until rather than the
canonical names — exercises the renaming the trait enables.

Closes #10

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:23:40 +02:00

126 lines
4.4 KiB
C++

// Tests for the oatpp-authkit#10 TemporalFieldTraits<T> extension.
//
// Exercises the temporal decorator against a DTO whose column names are
// NOT entity_id / valid_from / valid_until. The trait specialisation
// supplied via OATPP_AUTHKIT_REGISTER_TEMPORAL bridges the canonical
// names used by TemporalRepository to whatever the DTO actually calls
// them — here `id`, `effective_from`, `effective_until`. Same save/close/
// history flow as the existing decorator tests; only the field names move.
#include "oatpp-authkit/repo/TemporalRepository.hpp"
#include "oatpp-authkit/repo/Repository.hpp"
#include "oatpp-authkit/repo/TemporalFieldTraits.hpp"
#include "oatpp-authkit/repo/TemporalAt.hpp"
#include "oatpp/core/macro/codegen.hpp"
#include "oatpp/core/Types.hpp"
#include <cstdio>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include OATPP_CODEGEN_BEGIN(DTO)
namespace {
// DTO with intentionally non-canonical column names. Without the trait,
// TemporalRepository<T> couldn't reach these fields.
class OddNamesDto : public oatpp::DTO {
DTO_INIT(OddNamesDto, DTO)
DTO_FIELD(String, id);
DTO_FIELD(String, effective_from);
DTO_FIELD(String, effective_until);
DTO_FIELD(String, payload);
};
#include OATPP_CODEGEN_END(DTO)
} // namespace
OATPP_AUTHKIT_REGISTER_TEMPORAL(OddNamesDto, id, effective_from, effective_until)
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)
// Same in-memory adapter shape as the decorator tests — keys rows by
// (id, effective_from), exposes ALL rows via list().
class InMemoryAllRows : public oatpp_authkit::repo::Repository<OddNamesDto> {
std::map<std::pair<std::string, std::string>, oatpp::Object<OddNamesDto>> rows;
public:
oatpp::Object<OddNamesDto> findByEntityId(const oatpp::String& id) override {
for (auto& kv : rows) if (kv.first.first == std::string(*id)) return kv.second;
return nullptr;
}
oatpp::Vector<oatpp::Object<OddNamesDto>> list() override {
auto v = oatpp::Vector<oatpp::Object<OddNamesDto>>::createShared();
for (auto& kv : rows) v->push_back(kv.second);
return v;
}
void save(const oatpp::Object<OddNamesDto>& dto) override {
rows[{*dto->id, *dto->effective_from}] = dto;
}
void softDelete(const oatpp::String& id) override {
for (auto it = rows.begin(); it != rows.end(); ) {
if (it->first.first == std::string(*id)) it = rows.erase(it); else ++it;
}
}
};
struct StepClock {
int64_t ms{1700000000000LL};
int64_t operator()() { int64_t v = ms; ms += 1000; return v; }
};
void test_save_close_and_history_against_renamed_columns() {
using namespace oatpp_authkit::repo;
auto inner = std::make_shared<InMemoryAllRows>();
auto clock = std::make_shared<StepClock>();
TemporalRepository<OddNamesDto> repo(inner,
[clock]{ return (*clock)(); });
// First save — id auto-allocated, effective_from = now, effective_until = SENTINEL.
auto v1 = OddNamesDto::createShared();
v1->payload = oatpp::String("first");
repo.save(v1);
REQUIRE(v1->id);
REQUIRE(std::string(*v1->effective_until)
== TemporalRepository<OddNamesDto>::SENTINEL);
// Second save — close prior, insert new live.
auto v2 = OddNamesDto::createShared();
v2->id = v1->id;
v2->payload = oatpp::String("second");
repo.save(v2);
auto live = repo.findByEntityId(v1->id);
REQUIRE(live);
REQUIRE(std::string(*live->payload) == "second");
// history() returns both versions, oldest first.
auto h = repo.history(v1->id);
REQUIRE(h->size() == 2);
REQUIRE(std::string(*(*h)[0]->payload) == "first");
REQUIRE(std::string(*(*h)[1]->payload) == "second");
// softDelete closes live.
repo.softDelete(v1->id);
REQUIRE(!repo.findByEntityId(v1->id));
}
} // namespace
int main() {
test_save_close_and_history_against_renamed_columns();
std::printf("%s (%d failures)\n", g_failures ? "FAIL" : "OK", g_failures);
return g_failures ? 1 : 0;
}