Header-only foundation for the structural refactor that moves fewo-webapp from per-entity *Db clients to a shared Repository<TDto> abstraction. This ships interfaces only — no concrete implementations, no callers updated. Decisions baked in (all settled in the issue body): - Mixed entity_id allocation: caller may supply, otherwise the concrete repo generates a UUID inside save(). - UnitOfWork / cross-repo transactions: explicitly out of scope. - Repository<T> is a virtual-method interface, not a C++20 concept. - History queries live on a separate IHistoryRepository<T> so non-temporal repos don't have to implement a stub. Decorators (TemporalRepository<T>, ScopeGuardRepository<T>) follow in #8; the optional IQueryable<T> capability for typed filtering follows in #9. The fewo-webapp Person pilot (uwe.admin/fewo-webapp#457) and the wider 26-entity rollout (uwe.admin/fewo-webapp#458) build on this. Closes #7 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
200 lines
6.2 KiB
C++
200 lines
6.2 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/ITemporalEntity.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: ITemporalEntity is usable as a base marker.
|
|
#include OATPP_CODEGEN_BEGIN(DTO)
|
|
|
|
class TemporalDto : public oatpp::DTO, public oatpp_authkit::repo::ITemporalEntity {
|
|
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_marker_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_marker_compiles();
|
|
|
|
std::printf("%s (%d failures)\n", g_failures ? "FAIL" : "OK", g_failures);
|
|
return g_failures ? 1 : 0;
|
|
}
|