|
17 | 17 |
|
18 | 18 | #include <QAbstractSocket> |
19 | 19 | #include <QFile> |
| 20 | +#include <QElapsedTimer> |
20 | 21 | #include <QHostAddress> |
21 | 22 | #include <QRandomGenerator> |
22 | 23 | #include <QSignalSpy> |
|
26 | 27 | #include <QTest> |
27 | 28 |
|
28 | 29 | #include <atomic> |
| 30 | +#include <functional> |
29 | 31 | #include <memory> |
30 | 32 | #include <mutex> |
31 | 33 |
|
| 34 | +namespace { |
| 35 | +// QTest's QTRY_* macros abort the test function without registering a failure |
| 36 | +// with GoogleTest, so a timed out condition would silently pass here. |
| 37 | +bool waitFor(const std::function<bool()> &condition, int timeoutMs = 3000) |
| 38 | +{ |
| 39 | + QElapsedTimer timer; |
| 40 | + timer.start(); |
| 41 | + while (!condition() && timer.elapsed() < timeoutMs) |
| 42 | + QTest::qWait(10); |
| 43 | + return condition(); |
| 44 | +} |
| 45 | +} |
| 46 | + |
32 | 47 | TEST(TunnelManagerTest, EchoThroughReverseTunnel) |
33 | 48 | { |
34 | 49 | QTcpServer echo; |
@@ -258,6 +273,75 @@ TEST(TunnelManagerTest, MultipleForwardsRunIndependently) |
258 | 273 | a.stopAll(); |
259 | 274 | } |
260 | 275 |
|
| 276 | +TEST(TunnelManagerTest, BothSidesCanInitiateStreams) |
| 277 | +{ |
| 278 | + auto makeEcho = [](QTcpServer &server) { |
| 279 | + EXPECT_TRUE(server.listen(QHostAddress::LocalHost, 0)); |
| 280 | + QObject::connect(&server, &QTcpServer::newConnection, &server, [&server] { |
| 281 | + while (QTcpSocket *socket = server.nextPendingConnection()) { |
| 282 | + QObject::connect(socket, &QTcpSocket::readyRead, socket, [socket] { |
| 283 | + socket->write(socket->readAll()); |
| 284 | + }); |
| 285 | + QObject::connect(socket, &QTcpSocket::disconnected, socket, &QObject::deleteLater); |
| 286 | + } |
| 287 | + }); |
| 288 | + }; |
| 289 | + |
| 290 | + QTcpServer echoA; |
| 291 | + QTcpServer echoB; |
| 292 | + makeEcho(echoA); |
| 293 | + makeEcho(echoB); |
| 294 | + |
| 295 | + TunnelManager a; |
| 296 | + TunnelManager b; |
| 297 | + a.setPeer(QStringLiteral("B")); |
| 298 | + b.setPeer(QStringLiteral("A")); |
| 299 | + // A hosts the barrier web service, B hosts a file transfer service. |
| 300 | + a.registerService(QStringLiteral("barrier"), QStringLiteral("127.0.0.1"), echoA.serverPort()); |
| 301 | + b.registerService(QStringLiteral("transfer"), QStringLiteral("127.0.0.1"), echoB.serverPort()); |
| 302 | + |
| 303 | + a.setSendHandler([&b](const QString &, int mask, const QByteArray &payload) { |
| 304 | + b.onTunnelMessage(mask, payload, QStringLiteral("A")); |
| 305 | + }); |
| 306 | + b.setSendHandler([&a](const QString &, int mask, const QByteArray &payload) { |
| 307 | + a.onTunnelMessage(mask, payload, QStringLiteral("B")); |
| 308 | + }); |
| 309 | + |
| 310 | + // B opens the barrier stream first, as in an A -> B collaboration. |
| 311 | + const quint16 barrierPort = b.startLocalForward(QStringLiteral("barrier")); |
| 312 | + ASSERT_NE(barrierPort, 0); |
| 313 | + QTcpSocket barrierClient; |
| 314 | + barrierClient.connectToHost(QHostAddress::LocalHost, barrierPort); |
| 315 | + ASSERT_TRUE(waitFor([&] { |
| 316 | + return barrierClient.state() == QAbstractSocket::ConnectedState; |
| 317 | + })); |
| 318 | + barrierClient.write(QByteArrayLiteral("barrier")); |
| 319 | + ASSERT_TRUE(waitFor([&] { return barrierClient.bytesAvailable() >= 7; })); |
| 320 | + EXPECT_EQ(barrierClient.readAll(), QByteArrayLiteral("barrier")); |
| 321 | + |
| 322 | + // While that stream is still open, A opens the transfer stream, as in a |
| 323 | + // B -> A file transfer. Both sides used to allocate stream id 1 here and |
| 324 | + // B rejected the second OPEN as a duplicate. |
| 325 | + const quint16 transferPort = a.startLocalForward(QStringLiteral("transfer")); |
| 326 | + ASSERT_NE(transferPort, 0); |
| 327 | + QTcpSocket transferClient; |
| 328 | + transferClient.connectToHost(QHostAddress::LocalHost, transferPort); |
| 329 | + ASSERT_TRUE(waitFor([&] { |
| 330 | + return transferClient.state() == QAbstractSocket::ConnectedState; |
| 331 | + })); |
| 332 | + transferClient.write(QByteArrayLiteral("transfer")); |
| 333 | + ASSERT_TRUE(waitFor([&] { return transferClient.bytesAvailable() >= 8; })); |
| 334 | + EXPECT_EQ(transferClient.readAll(), QByteArrayLiteral("transfer")); |
| 335 | + |
| 336 | + // The first stream must survive the second one being opened. |
| 337 | + barrierClient.write(QByteArrayLiteral("still-alive")); |
| 338 | + ASSERT_TRUE(waitFor([&] { return barrierClient.bytesAvailable() >= 11; })); |
| 339 | + EXPECT_EQ(barrierClient.readAll(), QByteArrayLiteral("still-alive")); |
| 340 | + |
| 341 | + b.stopAll(); |
| 342 | + a.stopAll(); |
| 343 | +} |
| 344 | + |
261 | 345 | TEST(TunnelManagerTest, IncomingRequestIsNotSwallowedByPendingAsyncRequest) |
262 | 346 | { |
263 | 347 | auto service = std::make_shared<AsioService>(); |
|
0 commit comments