You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
692 lines
18 KiB
692 lines
18 KiB
#include "domain/helpers/Specification.h" |
|
#include <catch2/catch_test_macros.hpp> |
|
#include <catch2/matchers/catch_matchers_string.hpp> |
|
#include <trompeloeil.hpp> |
|
#include <memory> |
|
#include <stdexcept> |
|
#include <chrono> |
|
|
|
using trompeloeil::_; |
|
using namespace nxl::helpers; |
|
|
|
TEST_CASE("Specification Unit Tests", "[unit][Specification]") |
|
{ |
|
const Renderer TEST_RENDERER{ |
|
.opEq = "=", |
|
.opNe = "!=", |
|
.opLt = "<", |
|
.opLe = "<=", |
|
.opGt = ">", |
|
.opGe = ">=", |
|
.opLike = "LIKE", |
|
.opIsNull = "IS NULL", |
|
.opIsNotNull = "IS NOT NULL", |
|
.opAnd = "AND", |
|
.opOr = "OR", |
|
.groupStart = "(", |
|
.groupEnd = ")", |
|
.formatValue = [](const ConditionValue& v) -> std::string { |
|
if (v.is<std::string>()) { |
|
return "'" + v.as<std::string>() + "'"; |
|
} else if (v.is<const char*>()) { |
|
return "'" + std::string(v.as<const char*>()) + "'"; |
|
} else if (v.is<bool>()) { |
|
return v.as<bool>() ? "TRUE" : "FALSE"; |
|
} else if (v.is<int>()) { |
|
return std::to_string(v.as<int>()); |
|
} else if (v.is<double>()) { |
|
return std::to_string(v.as<double>()); |
|
} else if (v.is<std::chrono::system_clock::time_point>()) { |
|
auto time = v.as<std::chrono::system_clock::time_point>(); |
|
auto time_t = std::chrono::system_clock::to_time_t(time); |
|
std::string time_str = std::ctime(&time_t); |
|
if (!time_str.empty() && time_str.back() == '\n') { |
|
time_str.pop_back(); |
|
} |
|
return "'" + time_str + "'"; |
|
} |
|
return "'unknown'"; |
|
}}; |
|
|
|
SECTION("when single equals string condition is added then proper operator " |
|
"is rendered") |
|
{ |
|
// Given |
|
auto spec = makeSpecification().field("F").equals("foo").build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(F = 'foo')"); |
|
} |
|
|
|
SECTION( |
|
"when condition with chrono time_point is used then it renders correctly") |
|
{ |
|
// Given |
|
auto now = std::chrono::system_clock::now(); |
|
auto spec = makeSpecification().field("timestamp").greaterThan(now).build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result.find("timestamp >") != std::string::npos); |
|
REQUIRE(result.find("'") != std::string::npos); |
|
} |
|
|
|
SECTION("when single not equals string condition is added then proper " |
|
"operator is rendered") |
|
{ |
|
// Given |
|
auto spec = makeSpecification().field("F").notEquals("foo").build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(F != 'foo')"); |
|
} |
|
|
|
SECTION( |
|
"when single less than condition is added then proper operator is rendered") |
|
{ |
|
// Given |
|
auto spec = makeSpecification().field("F").lessThan(42).build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(F < 42)"); |
|
} |
|
|
|
SECTION("when single less than or equal condition is added then proper " |
|
"operator is rendered") |
|
{ |
|
// Given |
|
auto spec = makeSpecification().field("F").lessOrEqual(42).build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(F <= 42)"); |
|
} |
|
|
|
SECTION("when single greater than condition is added then proper operator is " |
|
"rendered") |
|
{ |
|
// Given |
|
auto spec = makeSpecification().field("F").greaterThan(42).build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(F > 42)"); |
|
} |
|
|
|
SECTION("when single greater than or equal condition is added then proper " |
|
"operator is rendered") |
|
{ |
|
// Given |
|
auto spec = makeSpecification().field("F").greaterOrEqual(42).build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(F >= 42)"); |
|
} |
|
|
|
SECTION( |
|
"when single like condition is added then proper operator is rendered") |
|
{ |
|
// Given |
|
auto spec = makeSpecification().field("F").like("%foo%").build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(F LIKE '%foo%')"); |
|
} |
|
|
|
SECTION( |
|
"when single is null condition is added then proper operator is rendered") |
|
{ |
|
// Given |
|
auto spec = makeSpecification().field("F").isNull().build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(F IS NULL)"); |
|
} |
|
|
|
SECTION("when single is not null condition is added then proper operator is " |
|
"rendered") |
|
{ |
|
// Given |
|
auto spec = makeSpecification().field("F").isNotNull().build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(F IS NOT NULL)"); |
|
} |
|
|
|
SECTION("when condition with double value is used then it renders correctly") |
|
{ |
|
// Given |
|
auto spec = makeSpecification().field("price").equals(19.99).build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(price = 19.990000)"); |
|
} |
|
|
|
SECTION("when condition with boolean value is used then it renders correctly") |
|
{ |
|
// Given |
|
auto spec = makeSpecification().field("active").equals(true).build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(active = TRUE)"); |
|
} |
|
|
|
SECTION( |
|
"when condition with const char* value is used then it renders correctly") |
|
{ |
|
// Given |
|
auto spec = makeSpecification().field("name").equals("test").build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(name = 'test')"); |
|
} |
|
|
|
SECTION("when multiple AND conditions are added then they are rendered with " |
|
"AND operator") |
|
{ |
|
// Given |
|
auto spec = makeSpecification() |
|
.field("name") |
|
.equals("John") |
|
.field("age") |
|
.greaterThan(30) |
|
.field("active") |
|
.equals(true) |
|
.build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(name = 'John' AND age > 30 AND active = TRUE)"); |
|
} |
|
|
|
SECTION("when AND group is created then conditions are grouped properly") |
|
{ |
|
// Given |
|
auto spec = makeSpecification() |
|
.field("name") |
|
.equals("John") |
|
.andGroup() |
|
.field("age") |
|
.greaterThan(30) |
|
.field("active") |
|
.equals(true) |
|
.endGroup() |
|
.build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(name = 'John' AND (age > 30 AND active = TRUE))"); |
|
} |
|
|
|
SECTION( |
|
"when OR group is created then conditions are grouped with OR operator") |
|
{ |
|
// Given |
|
auto spec = makeSpecification() |
|
.orGroup() |
|
.field("name") |
|
.equals("John") |
|
.field("age") |
|
.greaterThan(30) |
|
.field("active") |
|
.equals(true) |
|
.endGroup() |
|
.build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "((name = 'John' OR age > 30 OR active = TRUE))"); |
|
} |
|
|
|
SECTION("when nested groups are created then they are rendered properly") |
|
{ |
|
// Given |
|
auto spec = makeSpecification() |
|
.field("category") |
|
.equals("electronics") |
|
.andGroup() |
|
.field("price") |
|
.lessThan(1000) |
|
.orGroup() |
|
.field("brand") |
|
.equals("Sony") |
|
.field("brand") |
|
.equals("Samsung") |
|
.endGroup() |
|
.endGroup() |
|
.build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE( |
|
result |
|
== "(category = 'electronics' AND (price < 1000 AND (brand = 'Sony' " |
|
"OR brand = 'Samsung')))"); |
|
} |
|
|
|
SECTION("when complex specification with multiple nested groups is created " |
|
"then it renders correctly") |
|
{ |
|
// Given |
|
auto spec = makeSpecification() |
|
.orGroup() |
|
.andGroup() |
|
.field("type") |
|
.equals("food") |
|
.field("expiration_date") |
|
.lessOrEqual("2023-01-01") |
|
.endGroup() |
|
.andGroup() |
|
.field("type") |
|
.equals("non-food") |
|
.field("expiration_date") |
|
.lessOrEqual("2023-03-01") |
|
.endGroup() |
|
.endGroup() |
|
.build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result |
|
== "(((type = 'food' AND expiration_date <= '2023-01-01') OR (type " |
|
"= 'non-food' AND expiration_date <= '2023-03-01')))"); |
|
} |
|
|
|
SECTION("when empty group is created then it renders as empty") |
|
{ |
|
// Given |
|
auto spec = makeSpecification() |
|
.field("name") |
|
.equals("John") |
|
.andGroup() |
|
.endGroup() |
|
.build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(name = 'John' AND )"); |
|
} |
|
|
|
SECTION("when endGroup is called without matching startGroup then it should " |
|
"handle gracefully") |
|
{ |
|
// Given |
|
auto spec = |
|
makeSpecification().field("name").equals("John").endGroup().build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(name = 'John')"); |
|
} |
|
|
|
SECTION("when condition is added without field then exception is thrown") |
|
{ |
|
// Given |
|
auto builder = makeSpecification(); |
|
|
|
// When/Then |
|
REQUIRE_THROWS_AS(builder.equals("value"), std::runtime_error); |
|
REQUIRE_THROWS_WITH(builder.equals("value"), |
|
"No field specified for condition"); |
|
} |
|
|
|
SECTION("when field is called multiple times then last field is used") |
|
{ |
|
// Given |
|
auto spec = makeSpecification() |
|
.field("field1") |
|
.field("field2") |
|
.equals("value") |
|
.build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(field2 = 'value')"); |
|
} |
|
|
|
SECTION("when multiple conditions with same field are added then they are " |
|
"rendered correctly") |
|
{ |
|
// Given |
|
auto spec = makeSpecification() |
|
.field("age") |
|
.greaterThan(18) |
|
.field("age") |
|
.lessThan(65) |
|
.build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == "(age > 18 AND age < 65)"); |
|
} |
|
|
|
SECTION("when specification has only conditions with no explicit grouping " |
|
"then AND is used") |
|
{ |
|
// Given |
|
auto spec = makeSpecification() |
|
.field("field1") |
|
.equals("value1") |
|
.field("field2") |
|
.equals("value2") |
|
.field("field3") |
|
.equals("value3") |
|
.build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE( |
|
result |
|
== "(field1 = 'value1' AND field2 = 'value2' AND field3 = 'value3')"); |
|
} |
|
|
|
SECTION("when mixed data types are used then they render correctly") |
|
{ |
|
// Given |
|
auto now = std::chrono::system_clock::now(); |
|
auto spec = makeSpecification() |
|
.field("name") |
|
.equals("John") |
|
.field("age") |
|
.greaterThan(30) |
|
.field("salary") |
|
.greaterOrEqual(50000.50) |
|
.field("active") |
|
.equals(true) |
|
.field("created_at") |
|
.lessThan(now) |
|
.build(); |
|
|
|
// When |
|
auto result = spec->render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result.find("(name = 'John' AND age > 30 AND salary >= " |
|
"50000.500000 AND active = TRUE AND created_at < '") |
|
!= std::string::npos); |
|
REQUIRE(result.find("'") != std::string::npos); |
|
} |
|
|
|
SECTION("when ConditionValue is created with string then it stores and " |
|
"retrieves correctly") |
|
{ |
|
// Given |
|
ConditionValue value(std::string("test")); |
|
|
|
// Then |
|
REQUIRE(value.is<std::string>()); |
|
REQUIRE_FALSE(value.is<int>()); |
|
REQUIRE(value.as<std::string>() == "test"); |
|
} |
|
|
|
SECTION("when ConditionValue is created with int then it stores and " |
|
"retrieves correctly") |
|
{ |
|
// Given |
|
ConditionValue value(42); |
|
|
|
// Then |
|
REQUIRE(value.is<int>()); |
|
REQUIRE_FALSE(value.is<std::string>()); |
|
REQUIRE(value.as<int>() == 42); |
|
} |
|
|
|
SECTION("when ConditionValue is created with double then it stores and " |
|
"retrieves correctly") |
|
{ |
|
// Given |
|
ConditionValue value(3.14); |
|
|
|
// Then |
|
REQUIRE(value.is<double>()); |
|
REQUIRE_FALSE(value.is<int>()); |
|
REQUIRE(value.as<double>() == 3.14); |
|
} |
|
|
|
SECTION("when ConditionValue is created with bool then it stores and " |
|
"retrieves correctly") |
|
{ |
|
// Given |
|
ConditionValue value(true); |
|
|
|
// Then |
|
REQUIRE(value.is<bool>()); |
|
REQUIRE_FALSE(value.is<int>()); |
|
REQUIRE(value.as<bool>() == true); |
|
} |
|
|
|
SECTION("when ConditionValue is created with time_point then it stores and " |
|
"retrieves correctly") |
|
{ |
|
// Given |
|
auto now = std::chrono::system_clock::now(); |
|
ConditionValue value(now); |
|
|
|
// Then |
|
REQUIRE(value.is<std::chrono::system_clock::time_point>()); |
|
REQUIRE_FALSE(value.is<int>()); |
|
REQUIRE(value.as<std::chrono::system_clock::time_point>() == now); |
|
} |
|
|
|
SECTION( |
|
"when renderer has custom formatValue function then it is used correctly") |
|
{ |
|
// Given |
|
Renderer customRenderer{ |
|
.opEq = "=", |
|
.opNe = "!=", |
|
.opLt = "<", |
|
.opLe = "<=", |
|
.opGt = ">", |
|
.opGe = ">=", |
|
.opLike = "LIKE", |
|
.opIsNull = "IS NULL", |
|
.opIsNotNull = "IS NOT NULL", |
|
.opAnd = "AND", |
|
.opOr = "OR", |
|
.groupStart = "[", |
|
.groupEnd = "]", |
|
.formatValue = [](const ConditionValue& v) -> std::string { |
|
if (v.is<std::string>()) { |
|
return "\"" + v.as<std::string>() + "\""; |
|
} else if (v.is<const char*>()) { |
|
return "\"" + std::string(v.as<const char*>()) + "\""; |
|
} else if (v.is<int>()) { |
|
return "INT:" + std::to_string(v.as<int>()); |
|
} |
|
return "CUSTOM"; |
|
}}; |
|
|
|
auto spec = makeSpecification() |
|
.field("name") |
|
.equals(std::string("John")) |
|
.field("age") |
|
.greaterThan(30) |
|
.build(); |
|
|
|
// When |
|
auto result = spec->render(customRenderer); |
|
|
|
// Then |
|
REQUIRE(result == "[name = \"John\" AND age > INT:30]"); |
|
} |
|
|
|
SECTION( |
|
"when renderer has custom grouping symbols then they are used correctly") |
|
{ |
|
// Given |
|
Renderer customRenderer{ |
|
.opEq = "=", |
|
.opNe = "!=", |
|
.opLt = "<", |
|
.opLe = "<=", |
|
.opGt = ">", |
|
.opGe = ">=", |
|
.opLike = "LIKE", |
|
.opIsNull = "IS NULL", |
|
.opIsNotNull = "IS NOT NULL", |
|
.opAnd = "AND", |
|
.opOr = "OR", |
|
.groupStart = "{", |
|
.groupEnd = "}", |
|
.formatValue = [](const ConditionValue& v) -> std::string { |
|
if (v.is<std::string>()) { |
|
return "'" + v.as<std::string>() + "'"; |
|
} else if (v.is<const char*>()) { |
|
return "'" + std::string(v.as<const char*>()) + "'"; |
|
} else if (v.is<int>()) { |
|
return std::to_string(v.as<int>()); |
|
} |
|
return "'unknown'"; |
|
}}; |
|
|
|
auto spec = makeSpecification() |
|
.field("name") |
|
.equals(std::string("John")) |
|
.andGroup() |
|
.field("age") |
|
.greaterThan(30) |
|
.endGroup() |
|
.build(); |
|
|
|
// When |
|
auto result = spec->render(customRenderer); |
|
|
|
// Then |
|
REQUIRE(result == "{name = 'John' AND {age > 30}}"); |
|
} |
|
|
|
SECTION( |
|
"when renderer has custom logical operators then they are used correctly") |
|
{ |
|
// Given |
|
Renderer customRenderer{ |
|
.opEq = "=", |
|
.opNe = "!=", |
|
.opLt = "<", |
|
.opLe = "<=", |
|
.opGt = ">", |
|
.opGe = ">=", |
|
.opLike = "LIKE", |
|
.opIsNull = "IS NULL", |
|
.opIsNotNull = "IS NOT NULL", |
|
.opAnd = "&&", |
|
.opOr = "||", |
|
.groupStart = "(", |
|
.groupEnd = ")", |
|
.formatValue = [](const ConditionValue& v) -> std::string { |
|
if (v.is<std::string>()) { |
|
return "'" + v.as<std::string>() + "'"; |
|
} else if (v.is<const char*>()) { |
|
return "'" + std::string(v.as<const char*>()) + "'"; |
|
} else if (v.is<int>()) { |
|
return std::to_string(v.as<int>()); |
|
} else if (v.is<bool>()) { |
|
return v.as<bool>() ? "TRUE" : "FALSE"; |
|
} |
|
return "'unknown'"; |
|
}}; |
|
|
|
auto spec = makeSpecification() |
|
.orGroup() |
|
.field("name") |
|
.equals(std::string("John")) |
|
.orGroup() |
|
.field("age") |
|
.greaterThan(30) |
|
.field("active") |
|
.equals(true) |
|
.endGroup() |
|
.endGroup() |
|
.build(); |
|
|
|
// When |
|
auto result = spec->render(customRenderer); |
|
|
|
// Then |
|
REQUIRE(result == "((name = 'John' || (age > 30 || active = TRUE)))"); |
|
} |
|
|
|
SECTION( |
|
"when ConditionGroup has no children then render returns empty string") |
|
{ |
|
// Given |
|
ConditionGroup group(LogicalOp::AND); |
|
|
|
// When |
|
auto result = group.render(TEST_RENDERER); |
|
|
|
// Then |
|
REQUIRE(result == ""); |
|
} |
|
|
|
SECTION("when makeSpecification helper is used then it creates valid builder") |
|
{ |
|
// Given |
|
auto builder = makeSpecification(); |
|
|
|
// When |
|
auto spec = builder.field("test").equals("value").build(); |
|
|
|
// Then |
|
REQUIRE(spec != nullptr); |
|
auto result = spec->render(TEST_RENDERER); |
|
REQUIRE(result == "(test = 'value')"); |
|
} |
|
} |