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>
126 lines
4.4 KiB
C++
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;
|
|
}
|