Multiple implementations of the same back-end application. The aim is to provide quick, side-by-side comparisons of different technologies (languages, frameworks, libraries) while preserving consistent business logic across all implementations.
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

#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')");
}
}