Closes #9 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
219 lines
7.7 KiB
C++
219 lines
7.7 KiB
C++
// Tests for the oatpp-authkit#9 IQueryable<T> 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 <cstdio>
|
|
#include <string>
|
|
#include <variant>
|
|
#include <vector>
|
|
|
|
#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 "<null>" / "<wrong>".
|
|
template <typename T>
|
|
std::string bindAsString(const BindValue& v) {
|
|
if (auto* p = std::get_if<T>(&v)) {
|
|
if constexpr (std::is_same_v<T, std::string>) return *p;
|
|
else return std::to_string(*p);
|
|
}
|
|
if (std::holds_alternative<std::monostate>(v)) return "<null>";
|
|
return "<wrong-type>";
|
|
}
|
|
|
|
void test_equality_emits_parameterised_sql() {
|
|
auto sql = Query<MockQueryDto>()
|
|
.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<std::string>(sql.binds[0]),
|
|
std::string("foo@bar"));
|
|
}
|
|
|
|
void test_and_or_combines_predicates() {
|
|
auto sql = Query<MockQueryDto>()
|
|
.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<bool>(sql.binds[0]) == true);
|
|
REQUIRE(std::get<std::int64_t>(sql.binds[1]) == 18);
|
|
REQUIRE(std::get<std::int64_t>(sql.binds[2]) == 5);
|
|
}
|
|
|
|
void test_repeated_where_implies_and() {
|
|
auto sql = Query<MockQueryDto>()
|
|
.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<MockQueryDto>()
|
|
.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<std::int64_t>(sql.binds[0]) == 18);
|
|
REQUIRE(std::get<std::int64_t>(sql.binds[1]) == 65);
|
|
}
|
|
|
|
void test_in_with_multiple_values() {
|
|
auto sql = Query<MockQueryDto>()
|
|
.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<std::string>(sql.binds[0]), std::string("a@x"));
|
|
REQUIRE_EQ(std::get<std::string>(sql.binds[2]), std::string("c@x"));
|
|
}
|
|
|
|
void test_in_with_empty_list_is_always_false() {
|
|
std::vector<std::string> empty;
|
|
auto sql = Query<MockQueryDto>()
|
|
.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<MockQueryDto>()
|
|
.where(field<&MockQueryDto::name>().like("Al%"))
|
|
.toSql();
|
|
REQUIRE_EQ(sql.text, std::string(
|
|
"SELECT * FROM mock_query WHERE name LIKE ?"));
|
|
REQUIRE_EQ(std::get<std::string>(sql.binds[0]), std::string("Al%"));
|
|
}
|
|
|
|
void test_is_null_and_is_not_null() {
|
|
auto a = Query<MockQueryDto>()
|
|
.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<MockQueryDto>()
|
|
.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<MockQueryDto>()
|
|
.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<MockQueryDto>()
|
|
.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<bool>(sql.binds[0]) == true);
|
|
}
|
|
|
|
void test_no_where_no_clauses_is_plain_select() {
|
|
auto sql = Query<MockQueryDto>().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<MockQueryDto>()
|
|
.where(field<&MockQueryDto::email>().eq(oatpp::String("z@x")))
|
|
.toSql();
|
|
REQUIRE_EQ(std::get<std::string>(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_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;
|
|
}
|