#include #include #include #include #include #include #include #include #include #include #include #include #include "detection_test_helpers.h" using namespace yams::detection; using namespace yams::cli; using yams::detection::test_utils::ScopedCurrentPath; using yams::detection::test_utils::TestDirectory; namespace fs = std::filesystem; namespace { struct CommandIntegrationFixture { CommandIntegrationFixture() : cwdGuard(dir.root()) { auto repoMagic = dir.repoMagicNumbers(); if (repoMagic.empty()) { dir.createDataFile("data/magic_numbers.json", R"({"version":"1.0","patterns":[]})"); } createFile("document.pdf", {std::byte{0x25}, std::byte{0x50}, std::byte{0x44}, std::byte{0x46}}); createFile("image.jpg", {std::byte{0xFF}, std::byte{0xD8}, std::byte{0xFF}}); createFile("text.txt", {std::byte{0x48}, std::byte{0x65}, std::byte{0x6C}, std::byte{0x6C}}); cli = std::make_unique(); cli->setDataPath(dir.testDataDir()); } ~CommandIntegrationFixture() { FileTypeDetector::instance().clearCache(); } fs::path createFile(const std::string& name, std::initializer_list bytes) { auto path = dir.createBinaryFile(name, std::span(bytes.begin(), bytes.size())); std::ofstream file(path, std::ios::app); file << " additional content for testing"; testFiles.push_back(path); return path; } FileTypeDetectorConfig detectorConfig() const { FileTypeDetectorConfig config{}; auto magic = YamsCLI::findMagicNumbersFile(); config.patternsFile = magic; config.useCustomPatterns = !magic.empty(); config.useBuiltinPatterns = true; return config; } TestDirectory dir{"detection_cli"}; ScopedCurrentPath cwdGuard; std::unique_ptr cli; std::vector testFiles; }; } // namespace TEST_CASE_METHOD(CommandIntegrationFixture, "findMagicNumbersFile resolves local data", "[detection][cli]") { auto foundPath = YamsCLI::findMagicNumbersFile(); REQUIRE_NOTHROW(std::string(foundPath.string())); if (!foundPath.empty()) { CHECK(fs::exists(foundPath)); CHECK(fs::is_regular_file(foundPath)); } } TEST_CASE_METHOD(CommandIntegrationFixture, "findMagicNumbersFile respects search order", "[detection][cli]") { auto localMagic = dir.createDataFile("data/magic_numbers.json", R"({"version":"1.0","patterns":[]})"); auto foundPath = YamsCLI::findMagicNumbersFile(); #if defined(__APPLE__) // macOS: /var is a symlink to /private/var, so canonicalize both paths std::error_code ec; auto canonicalFound = fs::canonical(foundPath, ec); auto canonicalLocal = fs::canonical(localMagic, ec); CHECK(canonicalFound == canonicalLocal); #elif defined(_WIN32) auto normalizedPathString = [](const fs::path& path) { std::error_code ec; auto resolved = fs::weakly_canonical(path, ec); if (ec) { resolved = path.lexically_normal(); } auto normalized = resolved.generic_string(); std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return normalized; }; CHECK(normalizedPathString(foundPath) == normalizedPathString(localMagic)); #else // Linux: direct comparison CHECK(foundPath == localMagic); #endif } TEST_CASE_METHOD(CommandIntegrationFixture, "detector initializes from CLI config", "[detection][cli]") { auto config = detectorConfig(); auto result = FileTypeDetector::instance().initialize(config); CHECK((result.has_value() || !result.has_value())); bool isText = FileTypeDetector::instance().isTextMimeType("text/plain"); bool isBinary = FileTypeDetector::instance().isBinaryMimeType("image/jpeg"); auto category = FileTypeDetector::instance().getFileTypeCategory("application/pdf"); CHECK(isText); CHECK(isBinary); CHECK_FALSE(category.empty()); } TEST_CASE_METHOD(CommandIntegrationFixture, "detector falls back when JSON missing", "[detection][cli]") { FileTypeDetectorConfig config{}; config.patternsFile = fs::path("/nonexistent/path/magic_numbers.json"); config.useCustomPatterns = true; config.useBuiltinPatterns = true; auto initResult = FileTypeDetector::instance().initialize(config); CHECK(initResult); auto category = FileTypeDetector::instance().getFileTypeCategory("image/jpeg"); CHECK_FALSE(category.empty()); } TEST_CASE_METHOD(CommandIntegrationFixture, "file detection handles CLI files", "[detection][cli]") { auto config = detectorConfig(); FileTypeDetector::instance().initialize(config); for (const auto& testFile : testFiles) { auto result = FileTypeDetector::instance().detectFromFile(testFile); if (result) { const auto sig = result.value(); CHECK_FALSE(sig.mimeType.empty()); CHECK_FALSE(sig.fileType.empty()); } } } TEST_CASE_METHOD(CommandIntegrationFixture, "stats-style filtering uses classification", "[detection][cli]") { auto config = detectorConfig(); auto init = FileTypeDetector::instance().initialize(config); CHECK((init.has_value() || !init.has_value())); for (const auto& testFile : testFiles) { auto extension = testFile.extension().string(); auto mimeType = FileTypeDetector::getMimeTypeFromExtension(extension); auto fileType = FileTypeDetector::instance().getFileTypeCategory(mimeType); auto isBinary = FileTypeDetector::instance().isBinaryMimeType(mimeType); CHECK_FALSE(fileType.empty()); CHECK((isBinary || !isBinary)); } } TEST_CASE_METHOD(CommandIntegrationFixture, "list command filtering remains consistent", "[detection][cli]") { auto config = detectorConfig(); auto init = FileTypeDetector::instance().initialize(config); CHECK((init.has_value() || !init.has_value())); std::vector mimeTypes = {"image/jpeg", "application/pdf", "text/plain", "application/json"}; for (const auto& mimeType : mimeTypes) { auto isText = FileTypeDetector::instance().isTextMimeType(mimeType); auto isBinary = FileTypeDetector::instance().isBinaryMimeType(mimeType); auto category = FileTypeDetector::instance().getFileTypeCategory(mimeType); CHECK(isText != isBinary); CHECK_FALSE(category.empty()); } } TEST_CASE_METHOD(CommandIntegrationFixture, "get command filtering aligns with detection", "[detection][cli]") { auto config = detectorConfig(); FileTypeDetector::instance().initialize(config); for (const auto& testFile : testFiles) { if (!fs::exists(testFile)) { continue; } auto detectResult = FileTypeDetector::instance().detectFromFile(testFile); if (detectResult) { const auto sig = detectResult.value(); [[maybe_unused]] auto isText = FileTypeDetector::instance().isTextMimeType(sig.mimeType); auto isBinary = FileTypeDetector::instance().isBinaryMimeType(sig.mimeType); auto category = FileTypeDetector::instance().getFileTypeCategory(sig.mimeType); CHECK(sig.isBinary == isBinary); CHECK(sig.fileType == category); } } } TEST_CASE_METHOD(CommandIntegrationFixture, "initialization failure degrades gracefully", "[detection][cli]") { FileTypeDetectorConfig badConfig{}; badConfig.useLibMagic = false; badConfig.useBuiltinPatterns = false; badConfig.useCustomPatterns = true; badConfig.patternsFile = fs::path("/definitely/does/not/exist/anywhere.json"); auto result = FileTypeDetector::instance().initialize(badConfig); CHECK((result.has_value() || !result.has_value())); auto mime = FileTypeDetector::getMimeTypeFromExtension(".txt"); CHECK(mime == "text/plain"); auto isText = FileTypeDetector::instance().isTextMimeType("text/plain"); auto isBinary = FileTypeDetector::instance().isBinaryMimeType("image/jpeg"); CHECK((isText || !isText)); CHECK((isBinary || !isBinary)); }