// Tests for authkit#15 — RedactedFieldRepository decorator. #include "oatpp-authkit/repo/RedactedFieldRepository.hpp" #include "oatpp-authkit/repo/Repository.hpp" #include "oatpp-authkit/repo/TemporalFieldTraits.hpp" #include "oatpp-authkit/repo/TemporalRepository.hpp" #include "oatpp/core/macro/codegen.hpp" #include "oatpp/core/Types.hpp" #include #include #include #include #include #define REQUIRE(cond) do { \ if (!(cond)) { std::fprintf(stderr, "REQUIRE failed: %s @ %s:%d\n", \ #cond, __FILE__, __LINE__); std::abort(); } } while (0) #include OATPP_CODEGEN_BEGIN(DTO) namespace { class CredDto : public oatpp::DTO { DTO_INIT(CredDto, DTO) DTO_FIELD(String, id); DTO_FIELD(String, entity_id); DTO_FIELD(String, valid_from); DTO_FIELD(String, valid_until); DTO_FIELD(String, username); DTO_FIELD(String, passwordHash); DTO_FIELD(String, tlsCertDn); }; } // namespace #include OATPP_CODEGEN_END(DTO) OATPP_AUTHKIT_REGISTER_TEMPORAL(CredDto, id, entity_id, valid_from, valid_until) namespace { using namespace oatpp_authkit::repo; // In-memory inner that just records what got saved, for inspection. class FakeInner : public Repository { public: std::vector> saved; oatpp::Object findByEntityId(const oatpp::String&) override { return nullptr; } oatpp::Vector> list() override { return oatpp::Vector>::createShared(); } void save(const oatpp::Object& dto) override { saved.push_back(dto); } void softDelete(const oatpp::String&) override {} }; oatpp::Object makeRow(const std::string& vu, const std::string& password, const std::string& certDn) { auto d = CredDto::createShared(); d->id = "id1"; d->entity_id = "ent1"; d->valid_from = "2026-01-01T00:00:00Z"; d->valid_until = vu; d->username = "alice"; d->passwordHash = password; d->tlsCertDn = certDn; return d; } void test_live_row_passes_through_unchanged() { auto inner = std::make_shared(); RedactedFieldRepository redacted( inner, {"passwordHash", "tlsCertDn"}); // Live row: valid_until == SENTINEL. auto live = makeRow(TemporalRepository::SENTINEL, "$bcrypt$secret", "CN=alice"); redacted.save(live); REQUIRE(inner->saved.size() == 1); auto& got = inner->saved[0]; REQUIRE(got->passwordHash); REQUIRE(std::string(*got->passwordHash) == "$bcrypt$secret"); REQUIRE(got->tlsCertDn); REQUIRE(std::string(*got->tlsCertDn) == "CN=alice"); } void test_historical_row_redacts_named_fields() { auto inner = std::make_shared(); RedactedFieldRepository redacted( inner, {"passwordHash", "tlsCertDn"}); // Historical row: valid_until is a real timestamp, not the sentinel. auto historical = makeRow("2026-05-06T12:00:00Z", "$bcrypt$secret", "CN=alice"); redacted.save(historical); REQUIRE(inner->saved.size() == 1); auto& got = inner->saved[0]; REQUIRE(!got->passwordHash); // redacted to null REQUIRE(!got->tlsCertDn); // redacted to null // Non-redacted fields survive. REQUIRE(got->username); REQUIRE(std::string(*got->username) == "alice"); REQUIRE(got->valid_until); REQUIRE(std::string(*got->valid_until) == "2026-05-06T12:00:00Z"); } void test_partial_redaction_list() { auto inner = std::make_shared(); RedactedFieldRepository redacted(inner, {"passwordHash"}); auto historical = makeRow("2026-05-06T12:00:00Z", "$bcrypt$secret", "CN=alice"); redacted.save(historical); auto& got = inner->saved[0]; REQUIRE(!got->passwordHash); // redacted REQUIRE(got->tlsCertDn); // NOT redacted (not in list) REQUIRE(std::string(*got->tlsCertDn) == "CN=alice"); } void test_empty_redaction_list_passes_everything_through() { auto inner = std::make_shared(); RedactedFieldRepository redacted(inner, {}); auto historical = makeRow("2026-05-06T12:00:00Z", "$bcrypt$secret", "CN=alice"); redacted.save(historical); auto& got = inner->saved[0]; REQUIRE(got->passwordHash); REQUIRE(std::string(*got->passwordHash) == "$bcrypt$secret"); REQUIRE(got->tlsCertDn); } void test_null_valid_until_treated_as_live() { auto inner = std::make_shared(); RedactedFieldRepository redacted( inner, {"passwordHash", "tlsCertDn"}); // valid_until null — treat as live (the temporal decorator hasn't // set it yet on a fresh insert, before deciding sentinel). auto fresh = CredDto::createShared(); fresh->id = "id2"; fresh->entity_id = "ent2"; fresh->passwordHash = "$bcrypt$fresh"; fresh->tlsCertDn = "CN=bob"; redacted.save(fresh); auto& got = inner->saved[0]; REQUIRE(got->passwordHash); // not redacted REQUIRE(got->tlsCertDn); } } // namespace int main() { test_live_row_passes_through_unchanged(); test_historical_row_redacts_named_fields(); test_partial_redaction_list(); test_empty_redaction_list_passes_everything_through(); test_null_valid_until_treated_as_live(); std::printf("test_redacted_field_repository: OK\n"); return 0; }