// Copyright (c) 2025 YAMS Contributors // SPDX-License-Identifier: GPL-3.0-or-later /** * @file repository_foundation_test.cpp * @brief Unit tests for MetadataRepository refactor foundation (Phase 1) * * Tests entity_traits.hpp, result_helpers.hpp, and crud_ops.hpp */ #include #include "src/metadata/repository/crud_ops.hpp" #include "src/metadata/repository/entity_traits.hpp" #include "src/metadata/repository/result_helpers.hpp" #include #include using namespace yams; using namespace yams::metadata; using namespace yams::metadata::repository; // ============================================================================ // Entity Traits Tests // ============================================================================ TEST_CASE("EntityTraits DocumentInfo has correct table name", "[entity_traits]") { REQUIRE(EntityTraits::table == "documents"); REQUIRE(EntityTraits::primary_key == "id"); } TEST_CASE("EntityTraits DocumentContent has correct table name", "[entity_traits]") { REQUIRE(EntityTraits::table == "document_content"); REQUIRE(EntityTraits::primary_key == "document_id"); } TEST_CASE("EntityTraits SearchHistoryEntry has correct table name", "[entity_traits]") { REQUIRE(EntityTraits::table == "search_history"); REQUIRE(EntityTraits::primary_key == "id"); } TEST_CASE("buildSelectSql generates valid SQL", "[entity_traits]") { auto sql = buildSelectSql(); REQUIRE(sql.find("SELECT") != std::string::npos); REQUIRE(sql.find("FROM search_history") != std::string::npos); } TEST_CASE("buildInsertSql generates valid SQL", "[entity_traits]") { auto sql = buildInsertSql(); REQUIRE(sql.find("INSERT INTO search_history") != std::string::npos); REQUIRE(sql.find("VALUES") != std::string::npos); } // ============================================================================ // Result Helpers Tests // ============================================================================ TEST_CASE("YAMS_TRY returns error on failure", "[result_helpers]") { auto failingFunc = []() -> Result { return Error{ErrorCode::NotFound, "test error"}; }; auto testFunc = [&]() -> Result { YAMS_TRY(failingFunc()); return {}; // Should not reach here }; auto result = testFunc(); REQUIRE(!result.has_value()); REQUIRE(result.error().code == ErrorCode::NotFound); } TEST_CASE("YAMS_TRY passes through on success", "[result_helpers]") { auto successFunc = []() -> Result { return {}; }; auto testFunc = [&]() -> Result { YAMS_TRY(successFunc()); return 42; }; auto result = testFunc(); REQUIRE(result.has_value()); REQUIRE(result.value() == 42); } TEST_CASE("YAMS_TRY_UNWRAP extracts value on success", "[result_helpers]") { auto getValue = []() -> Result { return 123; }; auto testFunc = [&]() -> Result { YAMS_TRY_UNWRAP(val, getValue()); return val * 2; }; auto result = testFunc(); REQUIRE(result.has_value()); REQUIRE(result.value() == 246); } TEST_CASE("YAMS_TRY_UNWRAP returns error on failure", "[result_helpers]") { auto getValue = []() -> Result { return Error{ErrorCode::InvalidArgument, "bad input"}; }; auto testFunc = [&]() -> Result { YAMS_TRY_UNWRAP(val, getValue()); return val * 2; // Should not reach here }; auto result = testFunc(); REQUIRE(!result.has_value()); REQUIRE(result.error().code == ErrorCode::InvalidArgument); } TEST_CASE("transform maps Result value", "[result_helpers]") { Result success = 10; auto transformed = transform(std::move(success), [](int x) { return x * 2; }); REQUIRE(transformed.has_value()); REQUIRE(transformed.value() == 20); } TEST_CASE("transform propagates error", "[result_helpers]") { Result failure = Error{ErrorCode::NotFound}; auto transformed = transform(std::move(failure), [](int x) { return x * 2; }); REQUIRE(!transformed.has_value()); REQUIRE(transformed.error().code == ErrorCode::NotFound); } TEST_CASE("value_or returns value on success", "[result_helpers]") { Result success = 42; REQUIRE(value_or(std::move(success), 0) == 42); } TEST_CASE("value_or returns default on failure", "[result_helpers]") { Result failure = Error{ErrorCode::NotFound}; REQUIRE(value_or(std::move(failure), 99) == 99); } TEST_CASE("to_optional converts success to optional", "[result_helpers]") { Result success = 42; auto opt = to_optional(std::move(success)); REQUIRE(opt.has_value()); REQUIRE(opt.value() == 42); } TEST_CASE("to_optional converts failure to nullopt", "[result_helpers]") { Result failure = Error{ErrorCode::NotFound}; auto opt = to_optional(std::move(failure)); REQUIRE(!opt.has_value()); } TEST_CASE("scope_exit runs on scope exit", "[result_helpers]") { bool executed = false; { auto guard = scope_exit([&]() { executed = true; }); REQUIRE(!executed); } REQUIRE(executed); } TEST_CASE("scope_exit can be dismissed", "[result_helpers]") { bool executed = false; { auto guard = scope_exit([&]() { executed = true; }); guard.dismiss(); } REQUIRE(!executed); } // ============================================================================ // HasEntityTraits Concept Tests // ============================================================================ TEST_CASE("HasEntityTraits concept validates correctly", "[entity_traits]") { STATIC_REQUIRE(HasEntityTraits); STATIC_REQUIRE(HasEntityTraits); STATIC_REQUIRE(HasEntityTraits); STATIC_REQUIRE(HasEntityTraits); STATIC_REQUIRE(HasEntityTraits); STATIC_REQUIRE(HasEntityTraits); } // ============================================================================ // Phase 5A: New Entity Traits Tests // ============================================================================ TEST_CASE("EntityTraits DocumentRelationship has correct table name", "[entity_traits][phase5]") { REQUIRE(EntityTraits::table == "document_relationships"); REQUIRE(EntityTraits::primary_key == "id"); } TEST_CASE("EntityTraits MetadataEntry has correct table name", "[entity_traits][phase5]") { REQUIRE(EntityTraits::table == "metadata"); REQUIRE(EntityTraits::primary_key == "document_id"); } TEST_CASE("DocumentRelationship column count is correct", "[entity_traits][phase5]") { // id, parent_id, child_id, relationship_type, created_time constexpr auto count = columnCount(); STATIC_REQUIRE(count == 5); } TEST_CASE("MetadataEntry column count is correct", "[entity_traits][phase5]") { // document_id, key, value, value_type constexpr auto count = columnCount(); STATIC_REQUIRE(count == 4); } TEST_CASE("MetadataEntry columns have valid names", "[entity_traits][phase5]") { using namespace sql_validation; STATIC_REQUIRE(validateEntityTraits()); STATIC_REQUIRE(entity_traits_valid); } TEST_CASE("DocumentRelationship columns have valid names", "[entity_traits][phase5]") { using namespace sql_validation; STATIC_REQUIRE(validateEntityTraits()); STATIC_REQUIRE(entity_traits_valid); } // ============================================================================ // Phase 4: Compile-Time Column Index Tests // ============================================================================ TEST_CASE("columnIndex finds column by name at compile time", "[entity_traits][phase4]") { // DocumentInfo columns constexpr int idIdx = columnIndex("id"); constexpr int filePathIdx = columnIndex("file_path"); constexpr int fileNameIdx = columnIndex("file_name"); STATIC_REQUIRE(idIdx == 0); STATIC_REQUIRE(filePathIdx == 1); STATIC_REQUIRE(fileNameIdx == 2); // SearchHistoryEntry columns constexpr int queryIdx = columnIndex("query"); constexpr int queryTimeIdx = columnIndex("query_time"); STATIC_REQUIRE(queryIdx == 1); STATIC_REQUIRE(queryTimeIdx == 2); } TEST_CASE("columnIndex returns -1 for non-existent column", "[entity_traits][phase4]") { constexpr int notFound = columnIndex("nonexistent_column"); STATIC_REQUIRE(notFound == -1); } TEST_CASE("columnCount returns correct count", "[entity_traits][phase4]") { constexpr auto docCount = columnCount(); constexpr auto contentCount = columnCount(); constexpr auto historyCount = columnCount(); STATIC_REQUIRE(docCount == 9); // id, file_path, file_name, file_extension, file_size, // sha256_hash, mime_type, path_prefix, path_depth STATIC_REQUIRE(contentCount == 5); // document_id, content_text, content_length, // extraction_method, language STATIC_REQUIRE(historyCount == 6); // id, query, query_time, results_count, execution_time_ms, // user_context } TEST_CASE("columnName returns name by index", "[entity_traits][phase4]") { constexpr auto name0 = columnName(); constexpr auto name1 = columnName(); constexpr auto name2 = columnName(); STATIC_REQUIRE(name0 == "id"); STATIC_REQUIRE(name1 == "file_path"); STATIC_REQUIRE(name2 == "file_name"); } TEST_CASE("hasColumn checks column existence", "[entity_traits][phase4]") { STATIC_REQUIRE(hasColumn("id")); STATIC_REQUIRE(hasColumn("file_path")); STATIC_REQUIRE(hasColumn("sha256_hash")); STATIC_REQUIRE(!hasColumn("nonexistent")); STATIC_REQUIRE(!hasColumn("")); } TEST_CASE("primaryKeyIndex returns correct index", "[entity_traits][phase4]") { constexpr int docPkIdx = primaryKeyIndex(); constexpr int contentPkIdx = primaryKeyIndex(); constexpr int historyPkIdx = primaryKeyIndex(); STATIC_REQUIRE(docPkIdx == 0); // "id" is first column STATIC_REQUIRE(contentPkIdx == 0); // "document_id" is first column STATIC_REQUIRE(historyPkIdx == 0); // "id" is first column } // ============================================================================ // Phase 4: SQL Validation Tests // ============================================================================ TEST_CASE("isValidIdentifier validates SQL identifiers", "[sql_validation][phase4]") { using namespace sql_validation; // Valid identifiers STATIC_REQUIRE(isValidIdentifier("id")); STATIC_REQUIRE(isValidIdentifier("file_path")); STATIC_REQUIRE(isValidIdentifier("Column123")); STATIC_REQUIRE(isValidIdentifier("_private")); STATIC_REQUIRE(isValidIdentifier("A")); // Invalid identifiers STATIC_REQUIRE(!isValidIdentifier("")); STATIC_REQUIRE(!isValidIdentifier("123abc")); // Starts with digit STATIC_REQUIRE(!isValidIdentifier("has-dash")); // Contains dash STATIC_REQUIRE(!isValidIdentifier("has space")); // Contains space STATIC_REQUIRE(!isValidIdentifier("has.dot")); // Contains dot STATIC_REQUIRE(!isValidIdentifier("has;semicolon")); // Contains semicolon } TEST_CASE("isSqlKeyword detects SQL keywords", "[sql_validation][phase4]") { using namespace sql_validation; // Keywords (case-insensitive) STATIC_REQUIRE(isSqlKeyword("SELECT")); STATIC_REQUIRE(isSqlKeyword("select")); STATIC_REQUIRE(isSqlKeyword("INSERT")); STATIC_REQUIRE(isSqlKeyword("DELETE")); STATIC_REQUIRE(isSqlKeyword("DROP")); STATIC_REQUIRE(isSqlKeyword("UNION")); // Not keywords STATIC_REQUIRE(!isSqlKeyword("id")); STATIC_REQUIRE(!isSqlKeyword("name")); STATIC_REQUIRE(!isSqlKeyword("documents")); } TEST_CASE("validateEntityTraits validates all entity traits", "[sql_validation][phase4]") { using namespace sql_validation; // All our defined entities should be valid STATIC_REQUIRE(validateEntityTraits()); STATIC_REQUIRE(validateEntityTraits()); STATIC_REQUIRE(validateEntityTraits()); STATIC_REQUIRE(validateEntityTraits()); } TEST_CASE("entity_traits_valid constant is true for valid entities", "[sql_validation][phase4]") { using namespace sql_validation; STATIC_REQUIRE(entity_traits_valid); STATIC_REQUIRE(entity_traits_valid); STATIC_REQUIRE(entity_traits_valid); STATIC_REQUIRE(entity_traits_valid); }