#include #include #include #include #include #include #include #include #include using namespace yams::integrity; using namespace yams::storage; namespace { std::vector toBytes(const std::string& s) { std::vector out(s.size()); std::memcpy(out.data(), s.data(), s.size()); return out; } struct TempDir { std::filesystem::path path; TempDir() { path = std::filesystem::temp_directory_path() / ("yams_test_" + std::to_string(std::hash{}(std::this_thread::get_id())) + "_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); std::filesystem::create_directories(path); } ~TempDir() { std::error_code ec; std::filesystem::remove_all(path, ec); } TempDir(const TempDir&) = delete; TempDir& operator=(const TempDir&) = delete; }; struct RepairFixture { TempDir tempDir; StorageConfig storageConfig; std::unique_ptr storage; RepairManagerConfig repairConfig; std::unique_ptr manager; RepairFixture() { storageConfig.basePath = tempDir.path / "storage"; std::filesystem::create_directories(storageConfig.basePath); storage = std::make_unique(storageConfig); // Set up repair config with dummy fetchers repairConfig.backupFetcher = [](const std::string&) -> yams::Result> { return yams::Result>(yams::ErrorCode::NotFound); }; repairConfig.p2pFetcher = [](const std::string&) -> yams::Result> { return yams::Result>(yams::ErrorCode::NotFound); }; manager = std::make_unique(*storage, repairConfig); } void storeTestData(const std::string& hash, const std::string& data) { auto bytes = toBytes(data); storage->store(hash, std::span(bytes.data(), bytes.size())); } }; } // namespace TEST_CASE_METHOD(RepairFixture, "RepairManager basic operations", "[integrity][repair]") { SECTION("canRepair checks availability of repair sources") { // canRepair returns true when repair sources are configured, // even if the block doesn't exist (it checks capability, not block existence) (void)manager->canRepair("some-hash"); // Just verify the method is callable without crashing CHECK(true); } SECTION("attemptRepair returns false when no repair sources available") { storeTestData("test-hash", "test data"); // Repair should fail since all fetchers return NotFound CHECK_FALSE(manager->attemptRepair("unknown-hash")); } } TEST_CASE("RepairManager with working backup fetcher", "[integrity][repair]") { TempDir tempDir; StorageConfig storageConfig; storageConfig.basePath = tempDir.path / "storage"; std::filesystem::create_directories(storageConfig.basePath); StorageEngine storage(storageConfig); RepairManagerConfig config; config.backupFetcher = [](const std::string& hash) -> yams::Result> { if (hash == "recoverable-hash") { std::string data = "recovered data"; std::vector bytes(data.size()); std::memcpy(bytes.data(), data.data(), data.size()); return bytes; } return yams::Result>(yams::ErrorCode::NotFound); }; RepairManager manager(storage, config); SECTION("attemptRepair succeeds when backup fetcher returns data") { // The repair should attempt to use the backup fetcher (void)manager.attemptRepair("recoverable-hash"); // Note: actual success depends on hash validation, but we test the flow CHECK(true); // Verify no exception thrown } } TEST_CASE("RepairManager repair strategy order", "[integrity][repair]") { TempDir tempDir; StorageConfig storageConfig; storageConfig.basePath = tempDir.path / "storage"; std::filesystem::create_directories(storageConfig.basePath); StorageEngine storage(storageConfig); std::vector callOrder; RepairManagerConfig config; config.backupFetcher = [&callOrder](const std::string&) -> yams::Result> { callOrder.push_back("backup"); return yams::Result>(yams::ErrorCode::NotFound); }; config.p2pFetcher = [&callOrder](const std::string&) -> yams::Result> { callOrder.push_back("p2p"); return yams::Result>(yams::ErrorCode::NotFound); }; config.defaultOrder = {RepairStrategy::FromBackup, RepairStrategy::FromP2P}; RepairManager manager(storage, config); SECTION("attempts repair strategies in configured order") { (void)manager.attemptRepair("some-hash"); REQUIRE(callOrder.size() >= 1u); CHECK(callOrder[0] == "backup"); if (callOrder.size() >= 2u) { CHECK(callOrder[1] == "p2p"); } } } TEST_CASE("RepairManager rejects P2P data with wrong hash", "[integrity][repair]") { TempDir tempDir; StorageConfig storageConfig; storageConfig.basePath = tempDir.path / "storage"; std::filesystem::create_directories(storageConfig.basePath); StorageEngine storage(storageConfig); const std::string targetHash = "a1b2c3d4e5f60001112223334444555566667777888899990000aaaabbbbcccc"; std::string wrongData = "wrong data that produces a different hash"; RepairManagerConfig config; config.backupFetcher = [](const std::string&) -> yams::Result> { return yams::Result>(yams::ErrorCode::NotFound); }; config.p2pFetcher = [&wrongData](const std::string&) -> yams::Result> { return toBytes(wrongData); }; config.defaultOrder = {RepairStrategy::FromP2P}; RepairManager manager(storage, config); auto repaired = manager.attemptRepair(targetHash); CHECK_FALSE(repaired); CHECK_FALSE(storage.exists(targetHash).value()); } TEST_CASE("RepairManager recovers block from P2P with verified hash", "[integrity][repair]") { TempDir tempDir; StorageConfig storageConfig; storageConfig.basePath = tempDir.path / "storage"; std::filesystem::create_directories(storageConfig.basePath); StorageEngine storage(storageConfig); // Pre-compute hash of the recovery data so it matches const std::string recoveryData = "known-good-recovery-data-0123456789"; std::string recoveryHash; { auto hasher = yams::crypto::createSHA256Hasher(); auto bytes = toBytes(recoveryData); recoveryHash = hasher->hash(bytes); } RepairManagerConfig config; config.backupFetcher = [](const std::string&) -> yams::Result> { return yams::Result>(yams::ErrorCode::NotFound); }; config.p2pFetcher = [&recoveryData](const std::string&) -> yams::Result> { return toBytes(recoveryData); }; config.defaultOrder = {RepairStrategy::FromP2P}; RepairManager manager(storage, config); auto repaired = manager.attemptRepair(recoveryHash); CHECK(repaired); // Verify the data was actually stored with the correct hash CHECK(storage.exists(recoveryHash).value()); // Content should match auto retrieved = storage.retrieve(recoveryHash); REQUIRE(retrieved.has_value()); std::string retrievedStr(reinterpret_cast(retrieved.value().data()), retrieved.value().size()); CHECK(retrievedStr == recoveryData); } TEST_CASE("RepairManager: backup fetcher preferred over P2P", "[integrity][repair]") { TempDir tempDir; StorageConfig storageConfig; storageConfig.basePath = tempDir.path / "storage"; std::filesystem::create_directories(storageConfig.basePath); StorageEngine storage(storageConfig); const std::string backupData = "data-from-backup-0123456789"; const std::string p2pData = "data-from-p2p-0123456789"; std::string recoveryHash; { auto hasher = yams::crypto::createSHA256Hasher(); auto bytes = toBytes(backupData); recoveryHash = hasher->hash(bytes); } bool backupUsed = false; bool p2pUsed = false; RepairManagerConfig config; config.backupFetcher = [&backupUsed, &backupData](const std::string&) -> yams::Result> { backupUsed = true; return toBytes(backupData); }; config.p2pFetcher = [&p2pUsed](const std::string&) -> yams::Result> { p2pUsed = true; return yams::Result>(yams::ErrorCode::NotFound); }; config.defaultOrder = {RepairStrategy::FromBackup, RepairStrategy::FromP2P}; RepairManager manager(storage, config); auto repaired = manager.attemptRepair(recoveryHash); REQUIRE(repaired); // Backup should have been tried (and succeeded) CHECK(backupUsed); // P2P should NOT have been tried if backup succeeded CHECK_FALSE(p2pUsed); CHECK(storage.exists(recoveryHash).value()); } TEST_CASE("RepairManager: canRepair reflects source availability", "[integrity][repair]") { TempDir tempDir; StorageConfig storageConfig; storageConfig.basePath = tempDir.path / "storage"; std::filesystem::create_directories(storageConfig.basePath); StorageEngine storage(storageConfig); SECTION("canRepair false when no repair sources configured") { RepairManagerConfig config; // no fetchers set at all RepairManager manager(storage, config); CHECK_FALSE(manager.canRepair("any-hash")); } SECTION("canRepair true when backup fetcher is configured") { RepairManagerConfig config; config.backupFetcher = [](const std::string&) -> yams::Result> { std::string data = "backup-data"; return toBytes(data); }; config.p2pFetcher = [](const std::string&) -> yams::Result> { return yams::Result>(yams::ErrorCode::NotFound); }; RepairManager manager(storage, config); CHECK(manager.canRepair("any-hash")); } SECTION("canRepair true when p2p fetcher returns data") { RepairManagerConfig config; config.backupFetcher = [](const std::string&) -> yams::Result> { return yams::Result>(yams::ErrorCode::NotFound); }; config.p2pFetcher = [](const std::string&) -> yams::Result> { std::string data = "p2p-data"; return toBytes(data); }; RepairManager manager(storage, config); CHECK(manager.canRepair("any-hash")); } }