// Tests for the oatpp-authkit#9 IQueryable capability. // // Verifies that the AST emits the expected parameterised SQL and that the // bind bag captures the values in order. No real database is involved — // these tests exercise the SQL emitter and the builder API. #include "oatpp-authkit/repo/IQueryable.hpp" #include "oatpp/core/macro/codegen.hpp" #include "oatpp/core/Types.hpp" #include #include #include #include #include OATPP_CODEGEN_BEGIN(DTO) namespace { class MockQueryDto : public oatpp::DTO { DTO_INIT(MockQueryDto, DTO) DTO_FIELD(String, entity_id); DTO_FIELD(String, name); DTO_FIELD(String, email); DTO_FIELD(Int64, age); DTO_FIELD(Boolean, active); }; #include OATPP_CODEGEN_END(DTO) } // namespace OATPP_AUTHKIT_REGISTER_TABLE(MockQueryDto, "mock_query") OATPP_AUTHKIT_REGISTER_FIELD(MockQueryDto, entity_id, "entity_id") OATPP_AUTHKIT_REGISTER_FIELD(MockQueryDto, name, "name") OATPP_AUTHKIT_REGISTER_FIELD(MockQueryDto, email, "email") OATPP_AUTHKIT_REGISTER_FIELD(MockQueryDto, age, "age") OATPP_AUTHKIT_REGISTER_FIELD(MockQueryDto, active, "active") 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) #define REQUIRE_EQ(a, b) do { \ auto _av = (a); auto _bv = (b); \ if (!(_av == _bv)) { \ std::fprintf(stderr, "FAIL %s:%d %s == %s ('%s' vs '%s')\n", \ __FILE__, __LINE__, #a, #b, \ std::string(_av).c_str(), std::string(_bv).c_str()); \ ++g_failures; \ } \ } while (0) using namespace oatpp_authkit::repo; // Helper: pull a typed value out of a BindValue, or "" / "". template std::string bindAsString(const BindValue& v) { if (auto* p = std::get_if(&v)) { if constexpr (std::is_same_v) return *p; else return std::to_string(*p); } if (std::holds_alternative(v)) return ""; return ""; } void test_equality_emits_parameterised_sql() { auto sql = Query() .where(field<&MockQueryDto::email>().eq("foo@bar")) .toSql(); REQUIRE_EQ(sql.text, std::string("SELECT * FROM mock_query WHERE email = ?")); REQUIRE(sql.binds.size() == 1); REQUIRE_EQ(bindAsString(sql.binds[0]), std::string("foo@bar")); } void test_and_or_combines_predicates() { auto sql = Query() .where(field<&MockQueryDto::active>().eq(true) && (field<&MockQueryDto::age>().gt(18) || field<&MockQueryDto::age>().lt(5))) .toSql(); REQUIRE_EQ(sql.text, std::string( "SELECT * FROM mock_query WHERE " "(active = ? AND (age > ? OR age < ?))")); REQUIRE(sql.binds.size() == 3); REQUIRE(std::get(sql.binds[0]) == true); REQUIRE(std::get(sql.binds[1]) == 18); REQUIRE(std::get(sql.binds[2]) == 5); } void test_repeated_where_implies_and() { auto sql = Query() .where(field<&MockQueryDto::email>().eq("foo@bar")) .where(field<&MockQueryDto::active>().eq(true)) .toSql(); REQUIRE_EQ(sql.text, std::string( "SELECT * FROM mock_query WHERE " "(email = ? AND active = ?)")); } void test_range_emits_inclusive_and_exclusive() { auto sql = Query() .where(field<&MockQueryDto::age>().ge(18) && field<&MockQueryDto::age>().le(65)) .toSql(); REQUIRE_EQ(sql.text, std::string( "SELECT * FROM mock_query WHERE (age >= ? AND age <= ?)")); REQUIRE(std::get(sql.binds[0]) == 18); REQUIRE(std::get(sql.binds[1]) == 65); } void test_in_with_multiple_values() { auto sql = Query() .where(field<&MockQueryDto::email>().in({"a@x", "b@x", "c@x"})) .toSql(); REQUIRE_EQ(sql.text, std::string( "SELECT * FROM mock_query WHERE email IN (?, ?, ?)")); REQUIRE(sql.binds.size() == 3); REQUIRE_EQ(std::get(sql.binds[0]), std::string("a@x")); REQUIRE_EQ(std::get(sql.binds[2]), std::string("c@x")); } void test_in_with_empty_list_is_always_false() { std::vector empty; auto sql = Query() .where(field<&MockQueryDto::email>().in(empty)) .toSql(); REQUIRE_EQ(sql.text, std::string("SELECT * FROM mock_query WHERE 0")); REQUIRE(sql.binds.empty()); } void test_like_pattern_is_bound_not_interpolated() { auto sql = Query() .where(field<&MockQueryDto::name>().like("Al%")) .toSql(); REQUIRE_EQ(sql.text, std::string( "SELECT * FROM mock_query WHERE name LIKE ?")); REQUIRE_EQ(std::get(sql.binds[0]), std::string("Al%")); } void test_like_contains_escapes_wildcards() { // authkit#16 L-8: a user term with %/_/\ must be matched literally via an // explicit ESCAPE clause, not treated as wildcards. auto sql = Query() .where(field<&MockQueryDto::name>().likeContains("50%_off\\x")) .toSql(); REQUIRE_EQ(sql.text, std::string( "SELECT * FROM mock_query WHERE name LIKE ? ESCAPE '\\'")); REQUIRE_EQ(std::get(sql.binds[0]), std::string("%50\\%\\_off\\\\x%")); auto pfx = Query() .where(field<&MockQueryDto::name>().likePrefix("a_b")) .toSql(); REQUIRE_EQ(pfx.text, std::string( "SELECT * FROM mock_query WHERE name LIKE ? ESCAPE '\\'")); REQUIRE_EQ(std::get(pfx.binds[0]), std::string("a\\_b%")); // The bare likeEscape helper. REQUIRE_EQ(likeEscape("100%_\\"), std::string("100\\%\\_\\\\")); REQUIRE_EQ(likeEscape("plain"), std::string("plain")); } void test_is_null_and_is_not_null() { auto a = Query() .where(field<&MockQueryDto::email>().isNull()) .toSql(); REQUIRE_EQ(a.text, std::string( "SELECT * FROM mock_query WHERE email IS NULL")); REQUIRE(a.binds.empty()); auto b = Query() .where(field<&MockQueryDto::email>().isNotNull()) .toSql(); REQUIRE_EQ(b.text, std::string( "SELECT * FROM mock_query WHERE email IS NOT NULL")); } void test_not_negates_predicate() { auto sql = Query() .where(!field<&MockQueryDto::active>().eq(true)) .toSql(); REQUIRE_EQ(sql.text, std::string( "SELECT * FROM mock_query WHERE NOT (active = ?)")); } void test_order_by_and_limit_offset() { auto sql = Query() .where(field<&MockQueryDto::active>().eq(true)) .orderBy(field<&MockQueryDto::name>()) .orderByDesc(field<&MockQueryDto::age>()) .limit(50) .offset(100) .toSql(); REQUIRE_EQ(sql.text, std::string( "SELECT * FROM mock_query WHERE active = ? " "ORDER BY name ASC, age DESC LIMIT 50 OFFSET 100")); REQUIRE(std::get(sql.binds[0]) == true); } void test_no_where_no_clauses_is_plain_select() { auto sql = Query().toSql(); REQUIRE_EQ(sql.text, std::string("SELECT * FROM mock_query")); REQUIRE(sql.binds.empty()); } void test_oatpp_string_value_is_unwrapped() { auto sql = Query() .where(field<&MockQueryDto::email>().eq(oatpp::String("z@x"))) .toSql(); REQUIRE_EQ(std::get(sql.binds[0]), std::string("z@x")); } } // namespace int main() { test_equality_emits_parameterised_sql(); test_and_or_combines_predicates(); test_repeated_where_implies_and(); test_range_emits_inclusive_and_exclusive(); test_in_with_multiple_values(); test_in_with_empty_list_is_always_false(); test_like_pattern_is_bound_not_interpolated(); test_like_contains_escapes_wildcards(); test_is_null_and_is_not_null(); test_not_negates_predicate(); test_order_by_and_limit_offset(); test_no_where_no_clauses_is_plain_select(); test_oatpp_string_value_is_unwrapped(); std::printf("%s (%d failures)\n", g_failures ? "FAIL" : "OK", g_failures); return g_failures ? 1 : 0; }