// Tests for the oatpp-authkit#7 Repository 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 #include #include #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 { std::unordered_map> live; std::unordered_map> deleted; public: oatpp::Object findByEntityId(const oatpp::String& id) override { auto it = live.find(*id); return it == live.end() ? nullptr : it->second; } oatpp::Vector> list() override { auto v = oatpp::Vector>::createShared(); for (auto& kv : live) v->push_back(kv.second); return v; } void save(const oatpp::Object& 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; }