oatpp-authkit/test/test_repository_interface.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

199 lines
6.1 KiB
C++

// Tests for the oatpp-authkit#7 Repository<T> interface set. Exercises the
// contract through a trivial in-memory fake — confirms the abstract methods
// compile against an oatpp DTO, the mixed-id allocation branch on save() is
// implementable, and findByEntityId/list/softDelete round-trip as documented.
//
// No SQL involvement — that's the concrete adapters' job (out of scope).
#include "oatpp-authkit/repo/Repository.hpp"
#include "oatpp-authkit/repo/IHistoryRepository.hpp"
#include "oatpp-authkit/repo/TemporalAt.hpp"
#include "oatpp-authkit/repo/ActorContext.hpp"
#include "oatpp/core/macro/codegen.hpp"
#include "oatpp/core/Types.hpp"
#include <cstdio>
#include <random>
#include <unordered_map>
#include OATPP_CODEGEN_BEGIN(DTO)
namespace {
class MockDto : public oatpp::DTO {
DTO_INIT(MockDto, DTO)
DTO_FIELD(String, entity_id);
DTO_FIELD(String, name);
};
#include OATPP_CODEGEN_END(DTO)
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)
// Trivial UUID-ish generator — sufficient for the in-memory fake; concrete
// adapters can use libuuid or similar in production.
std::string generateId() {
static std::mt19937_64 rng{std::random_device{}()};
char buf[33];
std::snprintf(buf, sizeof(buf), "%016llx%016llx",
(unsigned long long)rng(), (unsigned long long)rng());
return std::string(buf);
}
class InMemoryRepo : public oatpp_authkit::repo::Repository<MockDto> {
std::unordered_map<std::string, oatpp::Object<MockDto>> live;
std::unordered_map<std::string, oatpp::Object<MockDto>> deleted;
public:
oatpp::Object<MockDto> findByEntityId(const oatpp::String& id) override {
auto it = live.find(*id);
return it == live.end() ? nullptr : it->second;
}
oatpp::Vector<oatpp::Object<MockDto>> list() override {
auto v = oatpp::Vector<oatpp::Object<MockDto>>::createShared();
for (auto& kv : live) v->push_back(kv.second);
return v;
}
void save(const oatpp::Object<MockDto>& dto) override {
if (!dto->entity_id) {
dto->entity_id = generateId();
}
live[*dto->entity_id] = dto;
}
void softDelete(const oatpp::String& id) override {
auto it = live.find(*id);
if (it != live.end()) {
deleted[*id] = it->second;
live.erase(it);
}
}
};
void test_save_allocates_uuid_when_id_null() {
InMemoryRepo repo;
auto dto = MockDto::createShared();
dto->name = oatpp::String("alice");
REQUIRE(!dto->entity_id); // precondition: id is null
repo.save(dto);
REQUIRE(dto->entity_id); // id was filled in
REQUIRE(std::string(*dto->entity_id).size() > 0);
}
void test_save_uses_supplied_id_when_present() {
InMemoryRepo repo;
auto dto = MockDto::createShared();
dto->entity_id = oatpp::String("supplied-id-42");
dto->name = oatpp::String("bob");
repo.save(dto);
REQUIRE(std::string(*dto->entity_id) == "supplied-id-42");
auto loaded = repo.findByEntityId(oatpp::String("supplied-id-42"));
REQUIRE(loaded);
REQUIRE(std::string(*loaded->name) == "bob");
}
void test_find_by_entity_id_round_trip() {
InMemoryRepo repo;
auto dto = MockDto::createShared();
dto->entity_id = oatpp::String("abc");
dto->name = oatpp::String("carol");
repo.save(dto);
auto found = repo.findByEntityId(oatpp::String("abc"));
REQUIRE(found);
REQUIRE(std::string(*found->name) == "carol");
auto missing = repo.findByEntityId(oatpp::String("does-not-exist"));
REQUIRE(!missing);
}
void test_list_returns_all_live_rows() {
InMemoryRepo repo;
for (const char* n : {"a", "b", "c"}) {
auto dto = MockDto::createShared();
dto->entity_id = oatpp::String(n);
dto->name = oatpp::String(n);
repo.save(dto);
}
auto all = repo.list();
REQUIRE(all->size() == 3);
}
void test_soft_delete_removes_from_live_view() {
InMemoryRepo repo;
auto dto = MockDto::createShared();
dto->entity_id = oatpp::String("delete-me");
dto->name = oatpp::String("doomed");
repo.save(dto);
REQUIRE(repo.findByEntityId(oatpp::String("delete-me")));
repo.softDelete(oatpp::String("delete-me"));
REQUIRE(!repo.findByEntityId(oatpp::String("delete-me")));
REQUIRE(repo.list()->size() == 0);
}
void test_temporal_at_value_type() {
using oatpp_authkit::repo::TemporalAt;
auto live = TemporalAt::live();
REQUIRE(live.kind == TemporalAt::Kind::Live);
auto pin = TemporalAt::at(1700000000000LL);
REQUIRE(pin.kind == TemporalAt::Kind::At);
REQUIRE(pin.timestamp == 1700000000000LL);
}
void test_actor_context_minimal() {
oatpp_authkit::repo::ActorContext ctx;
ctx.userId = "user-1";
ctx.allowedScopes = {"prop-A", "prop-B"};
REQUIRE(ctx.userId == "user-1");
REQUIRE(ctx.allowedScopes.size() == 2);
}
// Compile-time check: a temporal DTO with all three canonical fields builds.
#include OATPP_CODEGEN_BEGIN(DTO)
class TemporalDto : public oatpp::DTO {
DTO_INIT(TemporalDto, DTO)
DTO_FIELD(String, entity_id);
DTO_FIELD(String, valid_from);
DTO_FIELD(String, valid_until);
};
#include OATPP_CODEGEN_END(DTO)
void test_temporal_dto_compiles() {
auto dto = TemporalDto::createShared();
dto->entity_id = oatpp::String("t");
REQUIRE(std::string(*dto->entity_id) == "t");
}
} // namespace
int main() {
test_save_allocates_uuid_when_id_null();
test_save_uses_supplied_id_when_present();
test_find_by_entity_id_round_trip();
test_list_returns_all_live_rows();
test_soft_delete_removes_from_live_view();
test_temporal_at_value_type();
test_actor_context_minimal();
test_temporal_dto_compiles();
std::printf("%s (%d failures)\n", g_failures ? "FAIL" : "OK", g_failures);
return g_failures ? 1 : 0;
}