File: connectiontests.cpp

package info (click to toggle)
syncthingtray 1.7.5-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 6,804 kB
  • sloc: cpp: 31,085; xml: 1,694; java: 570; sh: 81; javascript: 53; makefile: 25
file content (718 lines) | stat: -rw-r--r-- 31,734 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
#include "../syncthingconnection.h"
#include "../syncthingconnectionsettings.h"

#include "../../testhelper/helper.h"
#include "../../testhelper/syncthingtestinstance.h"

#include <qtutilities/misc/conversion.h>

#include <c++utilities/tests/testutils.h>

#include <cppunit/TestFixture.h>

#include <QDir>
#include <QJsonArray>
#include <QJsonDocument>
#include <QStringBuilder>

using namespace std;
using namespace Data;
using namespace CppUtilities;
using namespace CppUtilities::Literals;

using namespace CPPUNIT_NS;

class WaitForConnected : private function<void(void)>, public SignalInfo<decltype(&SyncthingConnection::statusChanged), function<void(void)>> {
public:
    WaitForConnected(const SyncthingConnection &connection);
    operator bool() const;

private:
    const SyncthingConnection &m_connection;
    bool m_connectedAgain;
};

WaitForConnected::WaitForConnected(const SyncthingConnection &connection)
    : function<void(void)>([this] { m_connectedAgain = m_connectedAgain || m_connection.isConnected(); })
    , SignalInfo<decltype(&SyncthingConnection::statusChanged), function<void(void)>>(
          &connection, &SyncthingConnection::statusChanged, (*static_cast<const function<void(void)> *>(this)), &m_connectedAgain)
    , m_connection(connection)
    , m_connectedAgain(false)
{
}

WaitForConnected::operator bool() const
{
    (*static_cast<const function<void(void)> *>(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 <typename Action, typename... Signalinfo> void waitForConnection(Action action, int timeout, const Signalinfo &...signalInfo);
    template <typename Action, typename FailureSignalInfo, typename... Signalinfo>
    void waitForConnectionOrFail(Action action, int timeout, const FailureSignalInfo &failureSignalInfo, const Signalinfo &...signalInfo);
    template <typename Signal, typename Handler = function<void(void)>>
    SignalInfo<Signal, Handler> connectionSignal(
        Signal signal, const Handler &handler = function<void(void)>(), bool *correctSignalEmitted = nullptr);
    static void (SyncthingConnection::*defaultConnect())(void);
    static void (SyncthingConnection::*defaultReconnect())(void);
    static void (SyncthingConnection::*defaultDisconnect())(void);

    template <typename Handler> TemporaryConnection handleNewDevices(const Handler &handler);
    template <typename Handler> 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://127.0.0.1:") + syncthingPort());

    // keep track of status changes
    QObject::connect(&m_connection, &SyncthingConnection::statusChanged, &m_connection,
        [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, &m_connection,
            [](const QJsonObject &config) { cerr << " - New config: " << QJsonDocument(config).toJson(QJsonDocument::Indented).data() << endl; });
    }

    // log errors
    QObject::connect(&m_connection, &SyncthingConnection::error, &m_connection,
        [](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 <typename Action, typename... SignalInfo>
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 <typename Action, typename FailureSignalInfo, typename... SignalInfo>
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 <typename Signal, typename Handler>
SignalInfo<Signal, Handler> ConnectionTests::connectionSignal(Signal signal, const Handler &handler, bool *correctSignalEmitted)
{
    return SignalInfo<Signal, Handler>(&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<void (SyncthingConnection::*)(void)>(&SyncthingConnection::connect);
}

/*!
 * \brief Returns the default reconnect() signal (no args) for the test's connection.
 */
void (SyncthingConnection::*ConnectionTests::defaultReconnect())(void)
{
    return static_cast<void (SyncthingConnection::*)(void)>(&SyncthingConnection::reconnect);
}

/*!
 * \brief Returns the default disconnect() signal (no args) for the test's connection.
 */
void (SyncthingConnection::*ConnectionTests::defaultDisconnect())(void)
{
    return static_cast<void (SyncthingConnection::*)(void)>(&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 <typename Handler> TemporaryConnection ConnectionTests::handleNewDevices(const Handler &handler)
{
    return QObject::connect(&m_connection, &SyncthingConnection::newDevices, [&handler](const std::vector<SyncthingDev> &devs) {
        for (const SyncthingDev &dev : devs) {
            handler(dev, 0);
        }
    });
}

/*!
 * \brief Helps handling newDirs() signal when waiting for directory change.
 */
template <typename Handler> TemporaryConnection ConnectionTests::handleNewDirs(const Handler &handler)
{
    return QObject::connect(&m_connection, &SyncthingConnection::newDirs, [&handler](const std::vector<SyncthingDir> &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 configuration ..." << 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(QByteArrayLiteral("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 occurred
            if (syncthingAvailable) {
                CPPUNIT_FAIL("Syncthing became unavailable after another error had already occurred");
            }

            // 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<int>(syncthingCheckInterval.totalMilliseconds()));
            return;
        }
        syncthingAvailable = true;

        // check API key error
        if (errorMessage.contains(QStringLiteral("nobody:supersecret@")) && errorMessage.endsWith(QStringLiteral("server replied: Forbidden"))) {
            if (errorMessage.startsWith(QStringLiteral("Unable to request Syncthing status: "))) {
                apiKeyErrorStatus = true;
                return;
            }
            if (errorMessage.startsWith(QStringLiteral("Unable to request Syncthing config: "))) {
                apiKeyErrorConfig = true;
                return;
            }
        }

        // check for HTTP authentication error
        if (errorMessage.endsWith(QStringLiteral("Host requires authentication"))
            || errorMessage.endsWith(QStringLiteral("server replied: Forbidden"))) {
            if (errorMessage.startsWith(QStringLiteral("Unable to request Syncthing status: "))) {
                authErrorStatus = true;
                return;
            }
            if (errorMessage.startsWith(QStringLiteral("Unable to request Syncthing config: "))) {
                authErrorConfig = 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] { emit m_connection.newNotification(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("this device", QStringLiteral("This 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(static_cast<decltype(dev.addresses.size())>(2), dev.addresses.size());
            CPPUNIT_ASSERT_EQUAL(QStringLiteral("tcp://192.168.2.2:22001"), dev.addresses.front());
            CPPUNIT_ASSERT_EQUAL(QStringLiteral("tcp://192.168.2.2:22002"), dev.addresses.back());
            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(static_cast<decltype(dev.addresses.size())>(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();
    const auto tempDir = QtUtilities::fromNativeFileName(tempDirectory());
    CPPUNIT_ASSERT_EQUAL(QStringLiteral("test1"), dir1.id);
    CPPUNIT_ASSERT_EQUAL(QString(), dir1.label);
    CPPUNIT_ASSERT_EQUAL(QStringLiteral("test1"), dir1.displayName());
    CPPUNIT_ASSERT_EQUAL(tempDir + QStringLiteral("some/path/1/"), dir1.path);
    CPPUNIT_ASSERT_EQUAL(QStringLiteral("Up to Date"), dir1.statusString());
    CPPUNIT_ASSERT_EQUAL(SyncthingDirType::SendReceive, dir1.dirType);
    CPPUNIT_ASSERT(!dir1.paused);
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
    const auto devIds = dir1.deviceIds.toSet();
    const auto devNames = dir1.deviceNames.toSet();
#else
    const auto devIds = QSet(dir1.deviceIds.begin(), dir1.deviceIds.end());
    const auto devNames = QSet(dir1.deviceNames.begin(), dir1.deviceNames.end());
#endif
    CPPUNIT_ASSERT_EQUAL(QSet<QString>({ QStringLiteral("MMGUI6U-WUEZQCP-XZZ6VYB-LCT4TVC-ER2HAVX-QYT6X7D-S6ZSG2B-323KLQ7"),
                             QStringLiteral("6EIS2PN-J2IHWGS-AXS3YUL-HC5FT3K-77ZXTLL-AKQLJ4C-7SWVPUS-AZW4RQ4") }),
        devIds);
    CPPUNIT_ASSERT_EQUAL(QSet<QString>({ QStringLiteral("Test dev 2"), QStringLiteral("Test dev 1") }), devNames);
    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(tempDir + QStringLiteral("some/path/2/"), dir2.path);
    CPPUNIT_ASSERT_EQUAL(tempDir + QStringLiteral("some/path/2"), dir2.pathWithoutTrailingSlash().toString());
    CPPUNIT_ASSERT_EQUAL(QStringLiteral("Paused"), dir2.statusString());
    CPPUNIT_ASSERT_EQUAL(SyncthingDirType::SendReceive, dir2.dirType);
    CPPUNIT_ASSERT(dir2.paused);
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
    const auto devIds2 = dir2.deviceIds.toSet();
    const auto devNames2 = dir2.deviceNames.toSet();
#else
    const auto devIds2 = QSet(dir2.deviceIds.begin(), dir2.deviceIds.end());
    const auto devNames2 = QSet(dir2.deviceNames.begin(), dir2.deviceNames.end());
#endif
    CPPUNIT_ASSERT_EQUAL(QSet<QString>({ QStringLiteral("MMGUI6U-WUEZQCP-XZZ6VYB-LCT4TVC-ER2HAVX-QYT6X7D-S6ZSG2B-323KLQ7") }), devIds2);
    CPPUNIT_ASSERT_EQUAL(QSet<QString>({ QStringLiteral("Test dev 2") }), devNames2);
}

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<SyncthingDev> &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<SyncthingDir> &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<SyncthingLogEntry> &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 while there are outstanding requests ..." << endl;
    waitForConnected();
    m_connection.requestVersion();
    m_connection.requestDirStatistics();
    waitForSignals(
        [this] {
            m_connection.requestDeviceStatistics();
            m_connection.requestCompletion("MMGUI6U-WUEZQCP-XZZ6VYB-LCT4TVC-ER2HAVX-QYT6X7D-S6ZSG2B-323KLQ7", "test1");
            QTimer::singleShot(0, &m_connection, defaultDisconnect());
        },
        5000, 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<void (SyncthingConnection::*)(SyncthingConnectionSettings &)>(&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.contains(QStringLiteral("/rest/db/scan?folder=non-existing-dir&sub=sub%2Fpath - server replied: "));
    };
    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([this, &rawConfig] { m_connection.postConfigFromJsonObject(rawConfig); }, 10000,
        connectionSignal(&SyncthingConnection::error), connectionSignal(&SyncthingConnection::newConfigTriggered),
        connectionSignal(&SyncthingConnection::newConfig, handleNewConfig, &hasNewConfig));
}