// Tests for the oatpp-authkit#10 TemporalFieldTraits 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 #include #include #include #include #include OATPP_CODEGEN_BEGIN(DTO) namespace { // DTO with intentionally non-canonical column names. Without the trait, // TemporalRepository couldn't reach these fields. class OddNamesDto : public oatpp::DTO { DTO_INIT(OddNamesDto, DTO) DTO_FIELD(String, row_pk); DTO_FIELD(String, id); // entity_id (logical), per the original test intent 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, row_pk, 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 { std::map> rows; // keyed by row_pk public: oatpp::Object findByEntityId(const oatpp::String& id) override { for (auto& kv : rows) if (kv.second->id && std::string(*kv.second->id) == std::string(*id)) return kv.second; return nullptr; } oatpp::Vector> list() override { auto v = oatpp::Vector>::createShared(); for (auto& kv : rows) v->push_back(kv.second); return v; } void save(const oatpp::Object& dto) override { rows[std::string(*dto->row_pk)] = dto; } void softDelete(const oatpp::String& id) override { for (auto it = rows.begin(); it != rows.end(); ) { if (it->second->id && std::string(*it->second->id) == 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(); auto clock = std::make_shared(); TemporalRepository 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::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; }