#include "../syncthingconnection.h" #include "../syncthingconnectionsettings.h" #include "../../testhelper/helper.h" #include "../../testhelper/syncthingtestinstance.h" #include #include #include #include #include #include using namespace std; using namespace Data; using namespace CppUtilities; using namespace CppUtilities::Literals; using namespace CPPUNIT_NS; class WaitForConnected : private function, public SignalInfo> { public: WaitForConnected(const SyncthingConnection &connection); operator bool() const; private: const SyncthingConnection &m_connection; bool m_connectedAgain; }; WaitForConnected::WaitForConnected(const SyncthingConnection &connection) : function([this] { m_connectedAgain = m_connectedAgain || m_connection.isConnected(); }) , SignalInfo>( &connection, &SyncthingConnection::statusChanged, (*static_cast *>(this)), &m_connectedAgain) , m_connection(connection) , m_connectedAgain(false) { } WaitForConnected::operator bool() const { (*static_cast *>(this))(); // if the connection has already been connected it is ok, too return m_connectedAgain; } /*! * \brief The ConnectionTests class tests the SyncthingConnector. */ class ConnectionTests : public TestFixture, private SyncthingTestInstance { CPPUNIT_TEST_SUITE(ConnectionTests); CPPUNIT_TEST(testConnection); CPPUNIT_TEST_SUITE_END(); public: ConnectionTests(); void testConnection(); void testErrorCases(); void testInitialConnection(); void testSendingError(); void checkDevices(); void checkDirectories() const; void testReconnecting(); void testResumingAllDevices(); void testResumingDirectory(); void testPausingDirectory(); void testRequestingLog(); void testRequestingQrCode(); void testDisconnecting(); void testConnectingWithSettings(); void testRequestingRescan(); void testDealingWithArbitraryConfig(); void setUp() override; void tearDown() override; private: template void waitForConnection(Action action, int timeout, const Signalinfo &... signalInfo); template void waitForConnectionOrFail(Action action, int timeout, const FailureSignalInfo &failureSignalInfo, const Signalinfo &... signalInfo); template > SignalInfo connectionSignal( Signal signal, const Handler &handler = function(), bool *correctSignalEmitted = nullptr); static void (SyncthingConnection::*defaultConnect())(void); static void (SyncthingConnection::*defaultReconnect())(void); static void (SyncthingConnection::*defaultDisconnect())(void); template TemporaryConnection handleNewDevices(const Handler &handler); template TemporaryConnection handleNewDirs(const Handler &handler); WaitForConnected connectedSignal() const; void waitForConnected(int timeout = 5000); void waitForAllDirsAndDevsReady(bool initialConfig = false); SyncthingConnection m_connection; QString m_ownDevId; QString m_ownDevName; }; CPPUNIT_TEST_SUITE_REGISTRATION(ConnectionTests); ConnectionTests::ConnectionTests() { } // // test setup // /*! * \brief Starts Syncthing and prepares connecting. */ void ConnectionTests::setUp() { setInterleavedOutputEnabledFromEnv(); SyncthingTestInstance::start(); cerr << "\n - Preparing connection ..." << endl; m_connection.setSyncthingUrl(QStringLiteral("http://localhost:") + syncthingPort()); // keep track of status changes QObject::connect(&m_connection, &SyncthingConnection::statusChanged, [this] { cerr << " - Connection status changed to: " << m_connection.statusText().toLocal8Bit().data() << endl; }); // log configuration change if (qEnvironmentVariableIsSet("SYNCTHING_TEST_DUMP_CONFIG_UPDATES")) { QObject::connect(&m_connection, &SyncthingConnection::newConfig, [](const QJsonObject &config) { cerr << " - New config: " << QJsonDocument(config).toJson(QJsonDocument::Indented).data() << endl; }); } // log errors QObject::connect(&m_connection, &SyncthingConnection::error, [](const QString &message) { cerr << " - Connection error: " << message.toLocal8Bit().data() << endl; }); // reduce traffic poll interval to 10 seconds m_connection.setTrafficPollInterval(10000); } /*! * \brief Terminates Syncthing and prints stdout/stderr from Syncthing. */ void ConnectionTests::tearDown() { SyncthingTestInstance::stop(); } // // test helper // /*! * \brief Variant of waitForSignal() where the sender is the connection and the action is a method of the connection. */ template void ConnectionTests::waitForConnection(Action action, int timeout, const SignalInfo &... signalInfo) { waitForSignals(bind(action, &m_connection), timeout, signalInfo...); } /*! * \brief Variant of waitForSignalOrFail() where the sender is the connection and the action is a method of the connection. */ template void ConnectionTests::waitForConnectionOrFail( Action action, int timeout, const FailureSignalInfo &failureSignalInfo, const SignalInfo &... signalInfo) { waitForSignalsOrFail(bind(action, &m_connection), timeout, failureSignalInfo, signalInfo...); } /*! * \brief Returns a SignalInfo for the test's connection. */ template SignalInfo ConnectionTests::connectionSignal(Signal signal, const Handler &handler, bool *correctSignalEmitted) { return SignalInfo(&m_connection, signal, handler, correctSignalEmitted); } /*! * \brief Returns the default connect() signal (no args) for the test's connection. */ void (SyncthingConnection::*ConnectionTests::defaultConnect())(void) { return static_cast(&SyncthingConnection::connect); } /*! * \brief Returns the default reconnect() signal (no args) for the test's connection. */ void (SyncthingConnection::*ConnectionTests::defaultReconnect())(void) { return static_cast(&SyncthingConnection::reconnect); } /*! * \brief Returns the default disconnect() signal (no args) for the test's connection. */ void (SyncthingConnection::*ConnectionTests::defaultDisconnect())(void) { return static_cast(&SyncthingConnection::disconnect); } /*! * \brief Returns a SignalInfo to wait until the connected (again). */ WaitForConnected ConnectionTests::connectedSignal() const { return WaitForConnected(m_connection); } /*! * \brief Waits until connected (again). * \remarks * - Does nothing if already connected. * - Used to keep tests passing even though Syncthing dies and restarts during the testrun. */ void ConnectionTests::waitForConnected(int timeout) { waitForConnection(defaultConnect(), timeout, connectedSignal()); } /*! * \brief Ensures the connection is established and waits till all dirs and devs are ready. * \param initialConfig Whether to check for initial config (at least one dir and one dev is paused). */ void ConnectionTests::waitForAllDirsAndDevsReady(const bool initialConfig) { bool allDirsReady, allDevsReady; bool isConnected = m_connection.isConnected(); const auto checkAllDirsReady([this, &allDirsReady, &initialConfig] { bool oneDirPaused = false; for (const SyncthingDir &dir : m_connection.dirInfo()) { if (dir.status == SyncthingDirStatus::Unknown && !dir.paused) { allDirsReady = false; return; } oneDirPaused = oneDirPaused || dir.paused; } allDirsReady = !initialConfig || oneDirPaused; }); const auto checkAllDevsReady([this, &allDevsReady, &initialConfig] { bool oneDevPaused = false; for (const SyncthingDev &dev : m_connection.devInfo()) { if (dev.status == SyncthingDevStatus::Unknown && !dev.paused) { allDevsReady = false; return; } oneDevPaused = oneDevPaused || dev.paused; } allDevsReady = !initialConfig || oneDevPaused; }); auto checkStatus([this, &isConnected](SyncthingStatus) { isConnected = m_connection.isConnected(); }); checkAllDirsReady(); checkAllDevsReady(); if (allDirsReady && allDevsReady) { return; } waitForSignalsOrFail(bind(defaultConnect(), &m_connection), 10000, connectionSignal(&SyncthingConnection::error), connectionSignal(&SyncthingConnection::statusChanged, checkStatus, &isConnected), connectionSignal(&SyncthingConnection::dirStatusChanged, checkAllDirsReady, &allDirsReady), connectionSignal(&SyncthingConnection::newDirs, checkAllDirsReady, &allDirsReady), connectionSignal(&SyncthingConnection::devStatusChanged, checkAllDevsReady, &allDevsReady), connectionSignal(&SyncthingConnection::newDevices, checkAllDevsReady, &allDevsReady)); } /*! * \brief Helps handling newDevices() signal when waiting for device change. */ template TemporaryConnection ConnectionTests::handleNewDevices(const Handler &handler) { return QObject::connect(&m_connection, &SyncthingConnection::newDevices, [&handler](const std::vector &devs) { for (const SyncthingDev &dev : devs) { handler(dev, 0); } }); } /*! * \brief Helps handling newDirs() signal when waiting for directory change. */ template TemporaryConnection ConnectionTests::handleNewDirs(const Handler &handler) { return QObject::connect(&m_connection, &SyncthingConnection::newDirs, [&handler](const std::vector &dirs) { for (const SyncthingDir &dir : dirs) { handler(dir, 0); } }); } // // actual test // /*! * \brief Tests basic behaviour of the SyncthingConnection class. * \remarks Some tests are currently disabled for release mode because they sometimes fail. * \todo Find out why some tests are flaky. */ void ConnectionTests::testConnection() { testErrorCases(); testInitialConnection(); checkDevices(); checkDirectories(); testSendingError(); testReconnecting(); testResumingAllDevices(); testResumingDirectory(); testPausingDirectory(); testRequestingLog(); testRequestingQrCode(); testDisconnecting(); testConnectingWithSettings(); testRequestingRescan(); testDealingWithArbitraryConfig(); } void ConnectionTests::testErrorCases() { cerr << "\n - Error handling in case of insufficient conficuration ..." << endl; waitForConnection(defaultConnect(), 1000, connectionSignal(&SyncthingConnection::error, [](const QString &errorMessage) { CPPUNIT_ASSERT_EQUAL(QStringLiteral("Connection configuration is insufficient."), errorMessage); })); // setup/define test for error handling m_connection.setApiKey(QByteArray("wrong API key")); bool syncthingAvailable = false; constexpr auto syncthingCheckInterval = TimeSpan::fromMilliseconds(200.0); const auto maxSyncthingStartupTime = TimeSpan::fromSeconds(15.0 * max(timeoutFactor, 5.0)); auto remainingTimeForSyncthingToComeUp = maxSyncthingStartupTime; bool authErrorStatus = false, authErrorConfig = false; bool apiKeyErrorStatus = false, apiKeyErrorConfig = false; bool allErrorsEmitted = false; const auto errorHandler = [&](const QString &errorMessage) { // check whether Syncthing is available if ((errorMessage == QStringLiteral("Unable to request Syncthing status: Connection refused")) || (errorMessage == QStringLiteral("Unable to request Syncthing config: Connection refused"))) { // consider test failed if we receive "Connection refused" when another error has already occured if (syncthingAvailable) { CPPUNIT_FAIL("Syncthing became unavailable after another error had already occured"); } // consider test failed if Syncthing takes too long to come up (or we fail to connect) if ((remainingTimeForSyncthingToComeUp -= syncthingCheckInterval).isNegative()) { CPPUNIT_FAIL( argsToString("unable to connect to Syncthing within ", maxSyncthingStartupTime.toString(TimeSpanOutputFormat::WithMeasures))); } // give Syncthing a bit more time and check again wait(static_cast(syncthingCheckInterval.totalMilliseconds())); return; } syncthingAvailable = true; // check for HTTP authentication error if (errorMessage == QStringLiteral("Unable to request Syncthing status: Host requires authentication")) { authErrorStatus = true; return; } if (errorMessage == QStringLiteral("Unable to request Syncthing config: Host requires authentication")) { authErrorConfig = true; return; } // check API key error if ((errorMessage.startsWith(QStringLiteral("Unable to request Syncthing status: Error transferring ")) && errorMessage.endsWith(QStringLiteral("/rest/system/status - server replied: Forbidden")))) { m_connection.setApiKey(apiKey().toUtf8()); apiKeyErrorStatus = true; return; } if ((errorMessage.startsWith(QStringLiteral("Unable to request Syncthing config: Error transferring ")) && errorMessage.endsWith(QStringLiteral("/rest/system/config - server replied: Forbidden")))) { apiKeyErrorConfig = true; return; } // fail on unexpected error messages allErrorsEmitted = authErrorStatus && authErrorConfig && apiKeyErrorStatus && apiKeyErrorConfig; CPPUNIT_FAIL(argsToString("wrong error message: ", errorMessage.toLocal8Bit().data())); }; cerr << "\n - Error handling in case of inavailability ..." << endl; while (!syncthingAvailable) { waitForConnection(defaultConnect(), 5000, connectionSignal(&SyncthingConnection::error, errorHandler)); } cerr << "\n - Error handling in case of wrong credentials ..." << endl; waitForConnection(defaultConnect(), 5000, connectionSignal(&SyncthingConnection::error, errorHandler)); while (!authErrorStatus && !authErrorConfig) { waitForSignals(noop, 5000, connectionSignal(&SyncthingConnection::error, errorHandler)); } cerr << "\n - Error handling in case of wrong API key ..." << endl; m_connection.setCredentials(QStringLiteral("nobody"), QStringLiteral("supersecret")); waitForConnection(defaultConnect(), 5000, connectionSignal(&SyncthingConnection::error, errorHandler)); while (!apiKeyErrorStatus && !apiKeyErrorConfig) { waitForSignals(noop, 5000, connectionSignal(&SyncthingConnection::error, errorHandler)); } } void ConnectionTests::testInitialConnection() { cerr << "\n - Connecting initially ..." << endl; m_connection.setApiKey(apiKey().toUtf8()); waitForAllDirsAndDevsReady(true); CPPUNIT_ASSERT_EQUAL_MESSAGE( "connected and paused (one dev is initially paused)", QStringLiteral("connected, paused"), m_connection.statusText()); CPPUNIT_ASSERT_MESSAGE("no dirs out-of-sync", !m_connection.hasOutOfSyncDirs()); } void ConnectionTests::testSendingError() { auto newNotificationEmitted = false; const auto sentTime(DateTime::now()); const auto sentMessage(QStringLiteral("test notification")); const auto newNotificationHandler = [&](DateTime receivedTime, const QString &receivedMessage) { newNotificationEmitted |= receivedTime == sentTime && receivedMessage == sentMessage; }; waitForSignals([this, sentTime, &sentMessage] { m_connection.emitNotification(sentTime, sentMessage); }, 500, connectionSignal(&SyncthingConnection::newNotification, newNotificationHandler, &newNotificationEmitted)); } void ConnectionTests::checkDevices() { const auto &devInfo = m_connection.devInfo(); CPPUNIT_ASSERT_EQUAL_MESSAGE("3 devs present", 3_st, devInfo.size()); for (const SyncthingDev &dev : devInfo) { if (dev.id != QStringLiteral("MMGUI6U-WUEZQCP-XZZ6VYB-LCT4TVC-ER2HAVX-QYT6X7D-S6ZSG2B-323KLQ7") && dev.id != QStringLiteral("6EIS2PN-J2IHWGS-AXS3YUL-HC5FT3K-77ZXTLL-AKQLJ4C-7SWVPUS-AZW4RQ4")) { CPPUNIT_ASSERT_EQUAL_MESSAGE("own device", QStringLiteral("own device"), dev.statusString()); m_ownDevId = dev.id; m_ownDevName = dev.name; } } const SyncthingDev *dev1 = nullptr, *dev2 = nullptr; int index = 0, dev1Index, dev2Index; for (const SyncthingDev &dev : devInfo) { CPPUNIT_ASSERT(!dev.isConnected()); if (dev.id == QStringLiteral("MMGUI6U-WUEZQCP-XZZ6VYB-LCT4TVC-ER2HAVX-QYT6X7D-S6ZSG2B-323KLQ7")) { CPPUNIT_ASSERT_EQUAL_MESSAGE("paused device", QStringLiteral("paused"), dev.statusString()); CPPUNIT_ASSERT_EQUAL_MESSAGE("name", QStringLiteral("Test dev 2"), dev.name); CPPUNIT_ASSERT_MESSAGE("no introducer", !dev.introducer); CPPUNIT_ASSERT_EQUAL(1, dev.addresses.size()); CPPUNIT_ASSERT_EQUAL(QStringLiteral("tcp://192.168.2.2:22001"), dev.addresses.front()); dev2 = &dev; dev2Index = index; } else if (dev.id == QStringLiteral("6EIS2PN-J2IHWGS-AXS3YUL-HC5FT3K-77ZXTLL-AKQLJ4C-7SWVPUS-AZW4RQ4")) { CPPUNIT_ASSERT_EQUAL_MESSAGE("disconnected device", QStringLiteral("disconnected"), dev.statusString()); CPPUNIT_ASSERT_EQUAL_MESSAGE("name", QStringLiteral("Test dev 1"), dev.name); CPPUNIT_ASSERT_MESSAGE("introducer", dev.introducer); CPPUNIT_ASSERT_EQUAL(1, dev.addresses.size()); CPPUNIT_ASSERT_EQUAL(QStringLiteral("dynamic"), dev.addresses.front()); dev1 = &dev; dev1Index = index; } ++index; } CPPUNIT_ASSERT(dev1 && dev2); CPPUNIT_ASSERT(dev1 == m_connection.findDevInfo(QStringLiteral("6EIS2PN-J2IHWGS-AXS3YUL-HC5FT3K-77ZXTLL-AKQLJ4C-7SWVPUS-AZW4RQ4"), index)); CPPUNIT_ASSERT_EQUAL(dev1Index, index); CPPUNIT_ASSERT(dev2 == m_connection.findDevInfoByName(QStringLiteral("Test dev 2"), index)); CPPUNIT_ASSERT_EQUAL(dev2Index, index); CPPUNIT_ASSERT(!m_connection.findDevInfoByName(QStringLiteral("does not exist"), index)); } void ConnectionTests::checkDirectories() const { const auto &dirInfo = m_connection.dirInfo(); CPPUNIT_ASSERT_EQUAL_MESSAGE("2 dirs present", 2_st, dirInfo.size()); const SyncthingDir &dir1 = dirInfo.front(); CPPUNIT_ASSERT_EQUAL(QStringLiteral("test1"), dir1.id); CPPUNIT_ASSERT_EQUAL(QStringLiteral(""), dir1.label); CPPUNIT_ASSERT_EQUAL(QStringLiteral("test1"), dir1.displayName()); CPPUNIT_ASSERT_EQUAL(QStringLiteral("/tmp/some/path/1/"), dir1.path); CPPUNIT_ASSERT_EQUAL(QStringLiteral("idle"), dir1.statusString()); CPPUNIT_ASSERT_EQUAL(SyncthingDirType::SendReceive, dir1.dirType); CPPUNIT_ASSERT(!dir1.paused); CPPUNIT_ASSERT_EQUAL(QSet({ QStringLiteral("MMGUI6U-WUEZQCP-XZZ6VYB-LCT4TVC-ER2HAVX-QYT6X7D-S6ZSG2B-323KLQ7"), QStringLiteral("6EIS2PN-J2IHWGS-AXS3YUL-HC5FT3K-77ZXTLL-AKQLJ4C-7SWVPUS-AZW4RQ4") }), dir1.deviceIds.toSet()); CPPUNIT_ASSERT_EQUAL(QSet({ QStringLiteral("Test dev 2"), QStringLiteral("Test dev 1") }), dir1.deviceNames.toSet()); const SyncthingDir &dir2 = dirInfo.back(); CPPUNIT_ASSERT_EQUAL(QStringLiteral("test2"), dir2.id); CPPUNIT_ASSERT_EQUAL(QStringLiteral("Test dir 2"), dir2.label); CPPUNIT_ASSERT_EQUAL(QStringLiteral("Test dir 2"), dir2.displayName()); CPPUNIT_ASSERT_EQUAL(QStringLiteral("/tmp/some/path/2/"), dir2.path); CPPUNIT_ASSERT_EQUAL(QStringLiteral("/tmp/some/path/2"), dir2.pathWithoutTrailingSlash().toString()); CPPUNIT_ASSERT_EQUAL(QStringLiteral("paused"), dir2.statusString()); CPPUNIT_ASSERT_EQUAL(SyncthingDirType::SendReceive, dir2.dirType); CPPUNIT_ASSERT(dir2.paused); CPPUNIT_ASSERT_EQUAL( QSet({ QStringLiteral("MMGUI6U-WUEZQCP-XZZ6VYB-LCT4TVC-ER2HAVX-QYT6X7D-S6ZSG2B-323KLQ7") }), dir2.deviceIds.toSet()); CPPUNIT_ASSERT_EQUAL(QSet({ QStringLiteral("Test dev 2") }), dir2.deviceNames.toSet()); } void ConnectionTests::testReconnecting() { cerr << "\n - Reconnecting ...\n"; waitForConnection(defaultReconnect(), 1000, connectionSignal(&SyncthingConnection::statusChanged)); cerr << "\n - Waiting for dirs/devs after reconnect ...\n"; waitForAllDirsAndDevsReady(true); CPPUNIT_ASSERT_EQUAL_MESSAGE("connected again", QStringLiteral("connected, paused"), m_connection.statusText()); } void ConnectionTests::testResumingAllDevices() { cerr << "\n - Resuming all devices ..." << endl; bool devResumed = false; const auto devResumedHandler = [&devResumed](const SyncthingDev &dev, int) { if (dev.name == QStringLiteral("Test dev 2") && !dev.paused) { devResumed = true; } }; const auto newDevsHandler = [&devResumedHandler](const std::vector &devs) { for (const auto &dev : devs) { devResumedHandler(dev, 0); } }; const auto devResumedTriggeredHandler = [this](const QStringList &devIds) { CPPUNIT_ASSERT_EQUAL(m_connection.deviceIds(), devIds); }; const auto newDevsConnection = handleNewDevices(devResumedHandler); waitForConnected(); waitForConnection(&SyncthingConnection::resumeAllDevs, 7500, connectedSignal(), connectionSignal(&SyncthingConnection::devStatusChanged, devResumedHandler, &devResumed), connectionSignal(&SyncthingConnection::newDevices, newDevsHandler, &devResumed), connectionSignal(&SyncthingConnection::deviceResumeTriggered, devResumedTriggeredHandler)); CPPUNIT_ASSERT(devResumed); for (const QJsonValueRef devValue : m_connection.m_rawConfig.value(QStringLiteral("devices")).toArray()) { const QJsonObject &devObj(devValue.toObject()); CPPUNIT_ASSERT(!devObj.isEmpty()); CPPUNIT_ASSERT_MESSAGE("raw config updated accordingly", !devObj.value(QStringLiteral("paused")).toBool(true)); } CPPUNIT_ASSERT_MESSAGE("resuming all devs should not cause another request again", !m_connection.resumeAllDevs()); } void ConnectionTests::testResumingDirectory() { cerr << "\n - Resuming all dirs ..." << endl; bool dirResumed = false; const auto dirResumedHandler = [&dirResumed](const SyncthingDir &dir, int) { if (dir.id == QStringLiteral("test2") && !dir.paused) { dirResumed = true; } }; const auto newDirsHandler = [&dirResumedHandler](const std::vector &dirs) { for (const auto &dir : dirs) { dirResumedHandler(dir, 0); } }; const auto dirResumedTriggeredHandler = [this](const QStringList &devIds) { CPPUNIT_ASSERT_EQUAL(m_connection.directoryIds(), devIds); }; const auto newDirsConnection = handleNewDirs(dirResumedHandler); waitForConnected(); waitForConnection(&SyncthingConnection::resumeAllDirs, 7500, connectedSignal(), connectionSignal(&SyncthingConnection::dirStatusChanged, dirResumedHandler, &dirResumed), connectionSignal(&SyncthingConnection::newDirs, newDirsHandler, &dirResumed), connectionSignal(&SyncthingConnection::directoryResumeTriggered, dirResumedTriggeredHandler)); CPPUNIT_ASSERT(dirResumed); CPPUNIT_ASSERT_EQUAL_MESSAGE("still 2 dirs present", 2_st, m_connection.dirInfo().size()); CPPUNIT_ASSERT_MESSAGE("resuming all dirs should not cause another request again", !m_connection.resumeAllDirs()); } void ConnectionTests::testPausingDirectory() { cerr << "\n - Pause dir 1 ..." << endl; bool dirPaused = false; const auto dirPausedHandler = [&dirPaused](const SyncthingDir &dir, int) { if (dir.id == QStringLiteral("test1") && dir.paused) { dirPaused = true; } }; const QStringList ids({ QStringLiteral("test1") }); const auto dirPausedTriggeredHandler = [&ids](const QStringList &devIds) { CPPUNIT_ASSERT_EQUAL(ids, devIds); }; const auto newDirsConnection = handleNewDirs(dirPausedHandler); waitForConnected(); waitForSignals(bind(&SyncthingConnection::pauseDirectories, &m_connection, ids), 7500, connectedSignal(), connectionSignal(&SyncthingConnection::dirStatusChanged, dirPausedHandler, &dirPaused), connectionSignal(&SyncthingConnection::directoryPauseTriggered, dirPausedTriggeredHandler)); CPPUNIT_ASSERT(dirPaused); CPPUNIT_ASSERT_EQUAL_MESSAGE("still 2 dirs present", 2_st, m_connection.dirInfo().size()); CPPUNIT_ASSERT_MESSAGE("pausing should not cause another request again", !m_connection.pauseDirectories(ids)); } void ConnectionTests::testRequestingLog() { cerr << "\n - Requesting log ..." << endl; waitForConnected(); const auto handleLogAvailable = [](const vector &logEntries) { CPPUNIT_ASSERT(!logEntries.empty()); CPPUNIT_ASSERT(!logEntries[0].when.isEmpty()); CPPUNIT_ASSERT(!logEntries[0].message.isEmpty()); }; waitForConnectionOrFail(&SyncthingConnection::requestLog, 5000, connectionSignal(&SyncthingConnection::error), connectionSignal(&SyncthingConnection::logAvailable, handleLogAvailable)); } void ConnectionTests::testRequestingQrCode() { cerr << "\n - Requesting QR-Code for own device ID ..." << endl; waitForConnected(); const auto handleQrCodeAvailable = [](const QString &qrText, const QByteArray &data) { CPPUNIT_ASSERT_EQUAL(QStringLiteral("some text"), qrText); CPPUNIT_ASSERT(!data.isEmpty()); }; waitForSignalsOrFail(bind(&SyncthingConnection::requestQrCode, &m_connection, QStringLiteral("some text")), 5000, connectionSignal(&SyncthingConnection::error), connectionSignal(&SyncthingConnection::qrCodeAvailable, handleQrCodeAvailable)); } void ConnectionTests::testDisconnecting() { cerr << "\n - Disconnecting ..." << endl; waitForConnected(); waitForConnection(defaultDisconnect(), 1000, connectionSignal(&SyncthingConnection::statusChanged)); CPPUNIT_ASSERT_EQUAL_MESSAGE("disconnected", QStringLiteral("disconnected"), m_connection.statusText()); } void ConnectionTests::testConnectingWithSettings() { cerr << "\n - Connecting with settings ..." << endl; SyncthingConnectionSettings settings; settings.syncthingUrl = m_connection.syncthingUrl(); settings.apiKey = m_connection.apiKey(); settings.userName = m_connection.user(); settings.password = m_connection.password(); bool isConnected; const auto checkStatus([this, &isConnected](SyncthingStatus) { isConnected = m_connection.isConnected(); }); waitForSignals( bind(static_cast(&SyncthingConnection::connect), &m_connection, ref(settings)), 5000, connectionSignal(&SyncthingConnection::statusChanged, checkStatus, &isConnected)); } void ConnectionTests::testRequestingRescan() { cerr << "\n - Requesting rescan ..." << endl; waitForConnected(); bool rescanTriggered = false; const auto rescanTriggeredHandler = [&rescanTriggered](const QString &dir) { CPPUNIT_ASSERT_EQUAL(QStringLiteral("test2"), dir); rescanTriggered = true; }; waitForSignalsOrFail(bind(&SyncthingConnection::rescanAllDirs, &m_connection), 5000, connectionSignal(&SyncthingConnection::error), connectionSignal(&SyncthingConnection::rescanTriggered, rescanTriggeredHandler, &rescanTriggered)); bool errorOccured = false; const auto errorHandler = [&errorOccured](const QString &message) { errorOccured |= message.startsWith(QStringLiteral("Unable to request rescan: Error transferring")) && message.endsWith(QStringLiteral("/rest/db/scan?folder=non-existing-dir&sub=sub/path - server replied: Internal Server Error")); }; waitForSignals(bind(&SyncthingConnection::rescan, &m_connection, QStringLiteral("non-existing-dir"), QStringLiteral("sub/path")), 5000, connectionSignal(&SyncthingConnection::error, errorHandler, &errorOccured)); } void ConnectionTests::testDealingWithArbitraryConfig() { cerr << "\n - Changing arbitrary config ..." << endl; waitForConnected(); // read some value, eg. options.relayReconnectIntervalM auto rawConfig(m_connection.rawConfig()); auto optionsIterator(rawConfig.find(QLatin1String("options"))); CPPUNIT_ASSERT(optionsIterator != rawConfig.end()); auto optionsRef(optionsIterator.value()); CPPUNIT_ASSERT_EQUAL(QJsonValue::Object, optionsRef.type()); auto options(optionsRef.toObject()); CPPUNIT_ASSERT_EQUAL(10, options.value(QLatin1String("relayReconnectIntervalM")).toInt()); // change a value options.insert(QLatin1String("relayReconnectIntervalM"), 75); optionsRef = options; // expect the change via newConfig() signal bool hasNewConfig = false; const auto handleNewConfig([&hasNewConfig](const QJsonObject &newConfig) { const auto newIntervall(newConfig.value(QLatin1String("options")).toObject().value(QLatin1String("relayReconnectIntervalM")).toInt()); if (newIntervall == 75) { hasNewConfig = true; } }); // post new config waitForConnected(); waitForSignalsOrFail(bind(&SyncthingConnection::postConfigFromJsonObject, &m_connection, ref(rawConfig)), 10000, connectionSignal(&SyncthingConnection::error), connectionSignal(&SyncthingConnection::newConfigTriggered), connectionSignal(&SyncthingConnection::newConfig, handleNewConfig, &hasNewConfig)); }