#pragma once #ifndef MESSMER_CRYFS_TEST_CLI_TESTUTILS_CLITEST_H #define MESSMER_CRYFS_TEST_CLI_TESTUTILS_CLITEST_H #if defined(_MSC_VER) #include #include #endif #include #include #include #include #include #include #include #include #include #include #include "../../cryfs/impl/testutils/MockConsole.h" #include "../../cryfs/impl/testutils/TestWithFakeHomeDirectory.h" #include #include #include #include #include class CliTest : public ::testing::Test, TestWithFakeHomeDirectory { public: CliTest(): _basedir(), _mountdir(), basedir(_basedir.path()), mountdir(_mountdir.path()), logfile(), configfile(false), console(std::make_shared()) {} cpputils::TempDir _basedir; cpputils::TempDir _mountdir; boost::filesystem::path basedir; boost::filesystem::path mountdir; cpputils::TempFile logfile; cpputils::TempFile configfile; std::shared_ptr console; cpputils::unique_ref _httpClient() { cpputils::unique_ref httpClient = cpputils::make_unique_ref(); httpClient->addWebsite("https://www.cryfs.org/version_info.json", "{\"version_info\":{\"current\":\"0.8.5\"}}"); return httpClient; } int run(const std::vector& args, std::function onMounted) { std::vector _args; _args.reserve(args.size() + 1); _args.emplace_back("cryfs"); for (const std::string& arg : args) { _args.emplace_back(arg.c_str()); } auto &keyGenerator = cpputils::Random::PseudoRandom(); ON_CALL(*console, askPassword(testing::StrEq("Password: "))).WillByDefault(testing::Return("pass")); ON_CALL(*console, askPassword(testing::StrEq("Confirm Password: "))).WillByDefault(testing::Return("pass")); // Run Cryfs return cryfs_cli::Cli(keyGenerator, cpputils::SCrypt::TestSettings, console).main(_args.size(), _args.data(), _httpClient(), std::move(onMounted)); } void EXPECT_EXIT_WITH_HELP_MESSAGE(const std::vector& args, const std::string &message, cryfs::ErrorCode errorCode) { EXPECT_RUN_ERROR(args, "Usage:[^\\x00]*"+message, errorCode); } void EXPECT_RUN_ERROR(const std::vector& args, const std::string& message, cryfs::ErrorCode errorCode, std::function onMounted = [] {}) { const FilesystemOutput filesystem_output = run_filesystem(args, boost::none, std::move(onMounted)); EXPECT_EQ(exitCode(errorCode), filesystem_output.exit_code); if (!std::regex_search(filesystem_output.stderr_, std::regex(message))) { std::cerr << filesystem_output.stderr_ << std::endl; EXPECT_TRUE(false); } } void EXPECT_RUN_SUCCESS(const std::vector& args, const boost::optional &mountDir, std::function onMounted = [] {}) { //TODO Make this work when run in background ASSERT(std::find(args.begin(), args.end(), string("-f")) != args.end(), "Currently only works if run in foreground"); bool successfully_mounted = false; const FilesystemOutput filesystem_output = run_filesystem(args, mountDir, [&] { successfully_mounted = true; onMounted(); }); EXPECT_EQ(0, filesystem_output.exit_code); if (!std::regex_search(filesystem_output.stdout_, std::regex("Mounting filesystem"))) { std::cerr << "STDOUT:\n" << filesystem_output.stdout_ << "STDERR:\n" << filesystem_output.stderr_ << std::endl; EXPECT_TRUE(false) << "Filesystem did not output the 'Mounting filesystem' message, probably wasn't successfully mounted."; } if (!successfully_mounted) { EXPECT_TRUE(false) << "Filesystem did not call onMounted callback, probably wasn't successfully mounted."; } } struct FilesystemOutput final { int exit_code; std::string stdout_; std::string stderr_; }; static void _unmount(const boost::filesystem::path &mountDir) { fspp::fuse::Fuse::unmount(mountDir, true); } FilesystemOutput run_filesystem(const std::vector& args, boost::optional mountDirForUnmounting, std::function onMounted) { testing::internal::CaptureStdout(); testing::internal::CaptureStderr(); bool exited = false; cpputils::ConditionBarrier isMountedOrFailedBarrier; std::future exit_code = std::async(std::launch::async, [&] { const int exit_code = run(args, [&] { isMountedOrFailedBarrier.release(); }); // just in case it fails, we also want to release the barrier. // if it succeeds, this will release it a second time, which doesn't hurt. exited = true; isMountedOrFailedBarrier.release(); return exit_code; }); std::future on_mounted_success = std::async(std::launch::async, [&] { isMountedOrFailedBarrier.wait(); if (exited) { // file system already exited on its own, this indicates an error. It should have stayed mounted. // while the exit_code from run() will signal an error in this case, we didn't encounter another // error in the onMounted future, so return true here. return true; } // now we know the filesystem stayed online, so we can call the onMounted callback onMounted(); // and unmount it afterwards if (mountDirForUnmounting.is_initialized()) { _unmount(*mountDirForUnmounting); } return true; }); if(std::future_status::ready != on_mounted_success.wait_for(std::chrono::seconds(1000))) { testing::internal::GetCapturedStdout(); // stop capturing stdout testing::internal::GetCapturedStderr(); // stop capturing stderr std::cerr << "onMounted thread (e.g. used for unmount) didn't finish" << std::endl; // The std::future destructor of a future created with std::async blocks until the future is ready. // so, instead of causing a deadlock, rather abort exit(EXIT_FAILURE); } EXPECT_TRUE(on_mounted_success.get()); // this also re-throws any potential exceptions if(std::future_status::ready != exit_code.wait_for(std::chrono::seconds(1000))) { testing::internal::GetCapturedStdout(); // stop capturing stdout testing::internal::GetCapturedStderr(); // stop capturing stderr std::cerr << "Filesystem thread didn't finish" << std::endl; // The std::future destructor of a future created with std::async blocks until the future is ready. // so, instead of causing a deadlock, rather abort exit(EXIT_FAILURE); } return { exit_code.get(), // this also re-throws any potential exceptions testing::internal::GetCapturedStdout(), testing::internal::GetCapturedStderr() }; } }; #endif