#include "domain/helpers/Specification.h" #include #include #include #include #include #include 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()) { return "'" + v.as() + "'"; } else if (v.is()) { return "'" + std::string(v.as()) + "'"; } else if (v.is()) { return v.as() ? "TRUE" : "FALSE"; } else if (v.is()) { return std::to_string(v.as()); } else if (v.is()) { return std::to_string(v.as()); } else if (v.is()) { auto time = v.as(); 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()); REQUIRE_FALSE(value.is()); REQUIRE(value.as() == "test"); } SECTION("when ConditionValue is created with int then it stores and " "retrieves correctly") { // Given ConditionValue value(42); // Then REQUIRE(value.is()); REQUIRE_FALSE(value.is()); REQUIRE(value.as() == 42); } SECTION("when ConditionValue is created with double then it stores and " "retrieves correctly") { // Given ConditionValue value(3.14); // Then REQUIRE(value.is()); REQUIRE_FALSE(value.is()); REQUIRE(value.as() == 3.14); } SECTION("when ConditionValue is created with bool then it stores and " "retrieves correctly") { // Given ConditionValue value(true); // Then REQUIRE(value.is()); REQUIRE_FALSE(value.is()); REQUIRE(value.as() == 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()); REQUIRE_FALSE(value.is()); REQUIRE(value.as() == 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()) { return "\"" + v.as() + "\""; } else if (v.is()) { return "\"" + std::string(v.as()) + "\""; } else if (v.is()) { return "INT:" + std::to_string(v.as()); } 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()) { return "'" + v.as() + "'"; } else if (v.is()) { return "'" + std::string(v.as()) + "'"; } else if (v.is()) { return std::to_string(v.as()); } 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()) { return "'" + v.as() + "'"; } else if (v.is()) { return "'" + std::string(v.as()) + "'"; } else if (v.is()) { return std::to_string(v.as()); } else if (v.is()) { return v.as() ? "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')"); } }