diff --git a/test/unitTest/Makefile.am b/test/unitTest/Makefile.am index 579f29ff8546477f58512c946ff03cf5b1768828..4747e2e5645eb9b67824f2053abeb950209de7fe 100644 --- a/test/unitTest/Makefile.am +++ b/test/unitTest/Makefile.am @@ -122,4 +122,10 @@ ut_call_SOURCES = call/call.cpp check_PROGRAMS += ut_connectionManager ut_connectionManager_SOURCES = connectionManager/connectionManager.cpp +# +# fileTransfer +# +check_PROGRAMS += ut_fileTransfer +ut_fileTransfer_SOURCES = fileTransfer/fileTransfer.cpp + TESTS = $(check_PROGRAMS) \ No newline at end of file diff --git a/test/unitTest/fileTransfer/fileTransfer.cpp b/test/unitTest/fileTransfer/fileTransfer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3021e28c671a2089ff976303bccb80624b0cf75e --- /dev/null +++ b/test/unitTest/fileTransfer/fileTransfer.cpp @@ -0,0 +1,358 @@ + /* + * Copyright (C) 2020 Savoir-faire Linux Inc. + * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <cppunit/TestAssert.h> +#include <cppunit/TestFixture.h> +#include <cppunit/extensions/HelperMacros.h> + +#include <condition_variable> +#include <string> + +#include "manager.h" +#include "jamidht/connectionmanager.h" +#include "jamidht/jamiaccount.h" +#include "../../test_runner.h" +#include "dring.h" +#include "data_transfer.h" +#include "dring/datatransfer_interface.h" +#include "account_const.h" + +using namespace DRing::Account; + +namespace jami { namespace test { + +class FileTransferTest : public CppUnit::TestFixture { +public: + FileTransferTest() { + // Init daemon + DRing::init(DRing::InitFlag(DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_CONSOLE_LOG)); + if (not Manager::instance().initialized) + CPPUNIT_ASSERT(DRing::start("dring-sample.yml")); + } + ~FileTransferTest() { + DRing::fini(); + } + static std::string name() { return "Call"; } + bool compare(const std::string& fileA, const std::string& fileB) const; + void setUp(); + void tearDown(); + + std::string aliceId; + std::string bobId; + +private: + void testCachedFileTransfer(); + void testMultipleFileTransfer(); + + CPPUNIT_TEST_SUITE(FileTransferTest); + CPPUNIT_TEST(testCachedFileTransfer); + CPPUNIT_TEST(testMultipleFileTransfer); + CPPUNIT_TEST_SUITE_END(); +}; + +CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(FileTransferTest, FileTransferTest::name()); + +bool +FileTransferTest::compare(const std::string& fileA, const std::string& fileB) const { + std::ifstream f1(fileA, std::ifstream::binary|std::ifstream::ate); + std::ifstream f2(fileB, std::ifstream::binary|std::ifstream::ate); + + if (f1.fail() || f2.fail() || f1.tellg() != f2.tellg()) { + return false; + } + + f1.seekg(0, std::ifstream::beg); + f2.seekg(0, std::ifstream::beg); + return std::equal(std::istreambuf_iterator<char>(f1.rdbuf()), + std::istreambuf_iterator<char>(), + std::istreambuf_iterator<char>(f2.rdbuf())); +} + +void +FileTransferTest::setUp() +{ + std::map<std::string, std::string> details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "ALICE"; + details[ConfProperties::ALIAS] = "ALICE"; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = ""; + aliceId = Manager::instance().addAccount(details); + + details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "BOB"; + details[ConfProperties::ALIAS] = "BOB"; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = ""; + bobId = Manager::instance().addAccount(details); + + JAMI_INFO("Initialize account..."); + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + std::mutex mtx; + std::unique_lock<std::mutex> lk{ mtx }; + std::condition_variable cv; + confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + bool ready = false; + auto details = aliceAccount->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + ready = (daemonStatus == "REGISTERED"); + details = bobAccount->getVolatileAccountDetails(); + daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + ready &= (daemonStatus == "REGISTERED"); + })); + DRing::registerSignalHandlers(confHandlers); + cv.wait_for(lk, std::chrono::seconds(30)); + DRing::unregisterSignalHandlers(); +} + +void +FileTransferTest::tearDown() +{ + JAMI_INFO("Remove created accounts..."); + + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + std::mutex mtx; + std::unique_lock<std::mutex> lk{ mtx }; + std::condition_variable cv; + auto currentAccSize = Manager::instance().getAccountList().size(); + confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::AccountsChanged>( + [&]() { + if (Manager::instance().getAccountList().size() <= currentAccSize - 2) { + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + + Manager::instance().removeAccount(aliceId, true); + Manager::instance().removeAccount(bobId, true); + // Because cppunit is not linked with dbus, just poll if removed + cv.wait_for(lk, std::chrono::seconds(30)); + + DRing::unregisterSignalHandlers(); +} + +void +FileTransferTest::testCachedFileTransfer() +{ + std::this_thread::sleep_for(std::chrono::seconds(5)); + // TODO remove. This sleeps is because it take some time for the DHT to be connected + // and account announced + JAMI_INFO("Waiting...."); + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getAccountDetails()[ConfProperties::USERNAME]; + bobUri = bobUri.substr(std::string("ring:").size()); + auto bobDeviceId = bobAccount->getAccountDetails()[ConfProperties::RING_DEVICE_ID]; + auto aliceUri = aliceAccount->getAccountDetails()[ConfProperties::USERNAME]; + + std::mutex mtx; + std::unique_lock<std::mutex> lk{ mtx }; + std::condition_variable cv; + std::condition_variable cv2; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + bool transferCreated = false, transferFinished = false; + DRing::DataTransferId finalId; + // Watch signals + confHandlers.insert(DRing::exportable_callback<DRing::DataTransferSignal::DataTransferEvent>( + [&](const long unsigned int& id, int code) { + if (code == static_cast<int>(DRing::DataTransferEventCode::created)) { + transferCreated = true; + finalId = id; + cv.notify_one(); + } else if (code == static_cast<int>(DRing::DataTransferEventCode::finished)) { + transferFinished = true; + finalId = id; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + + bool successfullyReceive = false; + bobAccount->connectionManager().onChannelRequest( + [&successfullyReceive, &cv](const std::string&, const std::string& name) { + successfullyReceive = name.substr(0, 7) == "file://"; + cv.notify_one(); + return true; + }); + + // Create file to send + std::ofstream sendFile("SEND"); + CPPUNIT_ASSERT(sendFile.is_open()); + sendFile << std::string("A", 64000); + sendFile.close(); + + // Send File + DRing::DataTransferInfo info; + uint64_t id; + info.accountId = aliceAccount->getAccountID(); + info.peer = bobUri; + info.path = "SEND"; + info.displayName = "SEND"; + info.bytesProgress = 0; + CPPUNIT_ASSERT(DRing::sendFile(info, id) == DRing::DataTransferError::success); + + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(successfullyReceive); + + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(transferCreated); + + auto rcv_path = "RECV"; + CPPUNIT_ASSERT(DRing::acceptFileTransfer(finalId, rcv_path, 0) == DRing::DataTransferError::success); + + // Wait 2 times, both sides will got a finished status + cv.wait_for(lk, std::chrono::seconds(30)); + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(transferFinished); + + CPPUNIT_ASSERT(compare(info.path, rcv_path)); + + // TODO FIX ME. The ICE take some time to stop and it doesn't seems to like + // when stopping the daemon and removing the accounts to soon. + std::remove("SEND"); + std::remove("RECV"); + JAMI_INFO("Waiting...."); + std::this_thread::sleep_for(std::chrono::seconds(3)); +} + +void +FileTransferTest::testMultipleFileTransfer() +{ + std::this_thread::sleep_for(std::chrono::seconds(5)); + // TODO remove. This sleeps is because it take some time for the DHT to be connected + // and account announced + JAMI_INFO("Waiting...."); + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getAccountDetails()[ConfProperties::USERNAME]; + bobUri = bobUri.substr(std::string("ring:").size()); + auto bobDeviceId = bobAccount->getAccountDetails()[ConfProperties::RING_DEVICE_ID]; + auto aliceUri = aliceAccount->getAccountDetails()[ConfProperties::USERNAME]; + + std::mutex mtx; + std::unique_lock<std::mutex> lk{ mtx }; + std::condition_variable cv; + std::condition_variable cv2; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + bool transferCreated = false, transferFinished = false; + DRing::DataTransferId finalId; + // Watch signals + confHandlers.insert(DRing::exportable_callback<DRing::DataTransferSignal::DataTransferEvent>( + [&](const long unsigned int& id, int code) { + if (code == static_cast<int>(DRing::DataTransferEventCode::created)) { + transferCreated = true; + finalId = id; + cv.notify_one(); + } else if (code == static_cast<int>(DRing::DataTransferEventCode::finished)) { + transferFinished = true; + finalId = id; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + + bool successfullyReceive = false; + bobAccount->connectionManager().onChannelRequest( + [&successfullyReceive, &cv](const std::string&, const std::string& name) { + successfullyReceive = name.substr(0, 7) == "file://"; + cv.notify_one(); + return true; + }); + + // Create file to send + std::ofstream sendFile("SEND"); + CPPUNIT_ASSERT(sendFile.is_open()); + sendFile << std::string("A", 64000); + sendFile.close(); + std::ofstream sendFile2("SEND2"); + CPPUNIT_ASSERT(sendFile2.is_open()); + sendFile2 << std::string("B", 64000); + sendFile2.close(); + + // Send first File + DRing::DataTransferInfo info; + uint64_t id; + info.accountId = aliceAccount->getAccountID(); + info.peer = bobUri; + info.path = "SEND"; + info.displayName = "SEND"; + info.bytesProgress = 0; + CPPUNIT_ASSERT(DRing::sendFile(info, id) == DRing::DataTransferError::success); + + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(successfullyReceive); + + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(transferCreated); + + auto rcv_path = "RECV"; + CPPUNIT_ASSERT(DRing::acceptFileTransfer(finalId, rcv_path, 0) == DRing::DataTransferError::success); + + // Wait 2 times, both sides will got a finished status + cv.wait_for(lk, std::chrono::seconds(30)); + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(transferFinished); + + CPPUNIT_ASSERT(compare(info.path, rcv_path)); + + // Send File + DRing::DataTransferInfo info2; + info2.accountId = aliceAccount->getAccountID(); + info2.peer = bobUri; + info2.path = "SEND2"; + info2.displayName = "SEND2"; + info2.bytesProgress = 0; + CPPUNIT_ASSERT(DRing::sendFile(info2, id) == DRing::DataTransferError::success); + + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(successfullyReceive); + + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(transferCreated); + + rcv_path = "RECV2"; + CPPUNIT_ASSERT(DRing::acceptFileTransfer(finalId, rcv_path, 0) == DRing::DataTransferError::success); + + // Wait 2 times, both sides will got a finished status + cv.wait_for(lk, std::chrono::seconds(30)); + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(transferFinished); + + CPPUNIT_ASSERT(compare(info2.path, rcv_path)); + + // TODO FIX ME. The ICE take some time to stop and it doesn't seems to like + // when stopping the daemon and removing the accounts to soon. + std::remove("SEND"); + std::remove("SEND2"); + std::remove("RECV"); + std::remove("RECV2"); + JAMI_INFO("Waiting...."); + std::this_thread::sleep_for(std::chrono::seconds(3)); +} + +}} // namespace test + +RING_TEST_RUNNER(jami::test::FileTransferTest::name()) \ No newline at end of file