2023-05-09 18:59:16 -04:00
# include "TestNetworkRequest.h"
2023-05-31 15:40:12 -04:00
# include "core/NetworkManager.h"
2023-05-09 18:59:16 -04:00
# include "core/NetworkRequest.h"
# include "mock/MockNetworkAccessManager.h"
# include <QSignalSpy>
# include <QTest>
# include <QUrl>
QTEST_GUILESS_MAIN ( TestNetworkRequest )
2023-05-10 16:30:42 -04:00
using ContentTypeParameters_t = QHash < QString , QString > ;
Q_DECLARE_METATYPE ( ContentTypeParameters_t ) ;
2023-05-25 11:10:00 -04:00
Q_DECLARE_METATYPE ( std : : chrono : : milliseconds ) ;
2023-05-10 16:30:42 -04:00
2023-06-01 11:40:32 -04:00
static constexpr auto TIMEOUT_GRACE_MS = 100 ;
2023-05-31 14:15:22 -04:00
2023-05-09 18:59:16 -04:00
void TestNetworkRequest : : testNetworkRequest ( )
{
QFETCH ( QUrl , requestedURL ) ;
2023-05-31 18:50:32 -04:00
QFETCH ( QUrl , expectedURL ) ;
2023-05-09 18:59:16 -04:00
QFETCH ( QByteArray , expectedContent ) ;
2023-05-10 16:30:42 -04:00
QFETCH ( QString , responseContentType ) ;
QFETCH ( QString , expectedContentType ) ;
QFETCH ( ContentTypeParameters_t , expectedContentTypeParameters ) ;
2023-05-09 18:59:16 -04:00
QFETCH ( QString , expectedUserAgent ) ;
QFETCH ( bool , expectError ) ;
QFETCH ( QNetworkReply : : NetworkError , error ) ;
// Create mock reply
// Create and configure the mocked network access manager
MockNetworkAccess : : Manager < QNetworkAccessManager > manager ;
auto & reply = manager
2023-05-31 18:50:32 -04:00
. whenGet ( expectedURL )
2023-05-09 18:59:16 -04:00
// Has right user agent?
. has ( MockNetworkAccess : : Predicates : : HeaderMatching ( QNetworkRequest : : UserAgentHeader ,
QRegularExpression ( expectedUserAgent ) ) )
. reply ( ) ;
if ( ! expectError ) {
2023-05-10 16:30:42 -04:00
reply . withBody ( expectedContent ) . withHeader ( QNetworkRequest : : ContentTypeHeader , responseContentType ) ;
2023-05-09 18:59:16 -04:00
} else {
reply . withError ( error ) ;
}
// Create request
2023-05-31 15:40:12 -04:00
NetworkRequest request = buildRequest ( requestedURL ) . setManager ( & manager ) . build ( ) ;
2023-05-09 18:59:16 -04:00
2023-06-01 11:40:32 -04:00
QByteArray actualContent ;
2023-05-09 18:59:16 -04:00
bool didError = false , didSucceed = false ;
// Check request
QSignalSpy spy ( & request , & NetworkRequest : : success ) ;
connect ( & request , & NetworkRequest : : success , [ & actualContent , & didSucceed ] ( QByteArray content ) {
2023-06-01 11:40:32 -04:00
actualContent = content ;
2023-05-09 18:59:16 -04:00
didSucceed = true ;
} ) ;
QSignalSpy errorSpy ( & request , & NetworkRequest : : failure ) ;
connect ( & request , & NetworkRequest : : failure , [ & didError ] ( ) { didError = true ; } ) ;
2023-05-31 14:15:22 -04:00
request . fetch ( ) ;
2023-05-31 15:31:57 -04:00
QTest : : qWait ( 300 ) ;
2023-05-09 18:59:16 -04:00
// Ensures that predicates match - i.e., the header was set correctly
QCOMPARE ( manager . matchedRequests ( ) . length ( ) , 1 ) ;
2023-05-31 18:50:32 -04:00
QCOMPARE ( request . URL ( ) , expectedURL ) ;
2023-05-09 18:59:16 -04:00
if ( ! expectError ) {
QCOMPARE ( actualContent , expectedContent ) ;
2023-05-10 16:30:42 -04:00
QCOMPARE ( request . ContentType ( ) , expectedContentType ) ;
QCOMPARE ( request . ContentTypeParameters ( ) , expectedContentTypeParameters ) ;
2023-05-09 18:59:16 -04:00
QCOMPARE ( didSucceed , true ) ;
QCOMPARE ( didError , false ) ;
2023-05-10 16:30:42 -04:00
QCOMPARE ( request . Reply ( ) - > isFinished ( ) , true ) ;
2023-05-09 18:59:16 -04:00
} else {
QCOMPARE ( didSucceed , false ) ;
QCOMPARE ( didError , true ) ;
}
}
void TestNetworkRequest : : testNetworkRequest_data ( )
{
QTest : : addColumn < QUrl > ( " requestedURL " ) ;
2023-05-31 18:50:32 -04:00
QTest : : addColumn < QUrl > ( " expectedURL " ) ;
2023-05-09 18:59:16 -04:00
QTest : : addColumn < QByteArray > ( " expectedContent " ) ;
2023-05-10 16:30:42 -04:00
QTest : : addColumn < QString > ( " responseContentType " ) ;
QTest : : addColumn < QString > ( " expectedContentType " ) ;
QTest : : addColumn < ContentTypeParameters_t > ( " expectedContentTypeParameters " ) ;
2023-05-09 18:59:16 -04:00
QTest : : addColumn < QString > ( " expectedUserAgent " ) ;
QTest : : addColumn < bool > ( " expectError " ) ;
QTest : : addColumn < QNetworkReply : : NetworkError > ( " error " ) ;
QString defaultUserAgent ( " KeePassXC " ) ;
2023-05-31 18:50:32 -04:00
//const QUrl& exampleURL = QUrl{"https://example.com"};
2023-05-09 18:59:16 -04:00
const QUrl & exampleURL = QUrl { " https://example.com " } ;
const QByteArray & exampleContent = QString { " test-content " } . toUtf8 ( ) ;
2023-05-31 18:50:32 -04:00
QTest : : newRow ( " successful request " ) < < exampleURL < < exampleURL < < exampleContent < < " text/plain "
2023-05-10 16:30:42 -04:00
< < " text/plain " < < ContentTypeParameters_t { }
< < defaultUserAgent < < false < < QNetworkReply : : NetworkError : : NoError ;
2023-05-31 18:50:32 -04:00
QTest : : newRow ( " content type " ) < < exampleURL < < exampleURL < < exampleContent < < " application/test-content-type "
2023-05-10 16:30:42 -04:00
< < " application/test-content-type " < < ContentTypeParameters_t { }
< < defaultUserAgent < < false < < QNetworkReply : : NetworkError : : NoError ;
2023-05-31 18:50:32 -04:00
QTest : : newRow ( " empty content type " ) < < exampleURL < < exampleURL < < QByteArray { } < < " " < < " " < < ContentTypeParameters_t { }
2023-05-25 11:10:00 -04:00
< < defaultUserAgent < < false < < QNetworkReply : : NetworkError : : NoError ;
2023-05-31 18:50:32 -04:00
QTest : : newRow ( " content type parameters " ) < < exampleURL < < exampleURL < < exampleContent < < " application/test-content-type;test-param=test-value "
2023-05-10 16:30:42 -04:00
< < " application/test-content-type " < < ContentTypeParameters_t { { " test-param " , " test-value " } }
< < defaultUserAgent < < false < < QNetworkReply : : NetworkError : : NoError ;
2023-05-31 18:50:32 -04:00
QTest : : newRow ( " content type parameters trimmed " ) < < exampleURL < < exampleURL < < exampleContent < < " application/test-content-type; test-param = test-value "
2023-05-10 16:30:42 -04:00
< < " application/test-content-type " < < ContentTypeParameters_t { { " test-param " , " test-value " } }
< < defaultUserAgent < < false < < QNetworkReply : : NetworkError : : NoError ;
2023-05-31 18:50:32 -04:00
QTest : : newRow ( " request without schema should add https " ) < < QUrl ( " example.com " ) < < QUrl ( " https://example.com " ) < < exampleContent < < " text/plain "
< < " text/plain " < < ContentTypeParameters_t { }
< < defaultUserAgent < < false < < QNetworkReply : : NetworkError : : NoError ;
QTest : : newRow ( " request without schema should add https (edge case with // but no scheme) " ) < < QUrl ( " //example.com " ) < < QUrl ( " https://example.com " ) < < exampleContent < < " text/plain "
< < " text/plain " < < ContentTypeParameters_t { }
< < defaultUserAgent < < false < < QNetworkReply : : NetworkError : : NoError ;
2023-05-09 18:59:16 -04:00
}
void TestNetworkRequest : : testNetworkRequestTimeout ( )
{
2023-05-10 16:30:42 -04:00
// Timeout should work for single request
// Timeout should capture entire duration, including redirects
2023-05-25 11:10:00 -04:00
QFETCH ( bool , expectError ) ;
QFETCH ( std : : chrono : : milliseconds , delay ) ;
QFETCH ( std : : chrono : : milliseconds , timeout ) ;
const auto requestedURL = QUrl ( " https://example.com " ) ;
const auto expectedUserAgent = QString ( " KeePassXC " ) ;
// Create mock reply
// Create and configure the mocked network access manager
MockNetworkAccess : : Manager < QNetworkAccessManager > manager ;
auto & reply = manager
. whenGet ( requestedURL )
// Has right user agent?
. has ( MockNetworkAccess : : Predicates : : HeaderMatching ( QNetworkRequest : : UserAgentHeader ,
QRegularExpression ( expectedUserAgent ) ) )
. reply ( ) ;
// Timeout
QTimer timer ;
timer . setSingleShot ( true ) ;
timer . setInterval ( delay ) ;
reply . withFinishDelayUntil ( & timer , & QTimer : : timeout ) ;
// Create request
2023-05-31 15:40:12 -04:00
NetworkRequest request = buildRequest ( requestedURL ) . setManager ( & manager ) . setTimeout ( timeout ) . build ( ) ;
2023-05-25 11:10:00 -04:00
// Start timer
timer . start ( ) ;
bool didSucceed = false , didError = false ;
// Check request
QSignalSpy spy ( & request , & NetworkRequest : : success ) ;
connect ( & request , & NetworkRequest : : success , [ & didSucceed ] ( const QByteArray & ) {
didSucceed = true ;
} ) ;
QSignalSpy errorSpy ( & request , & NetworkRequest : : failure ) ;
connect ( & request , & NetworkRequest : : failure , [ & didError ] ( ) { didError = true ; } ) ;
2023-05-31 14:15:22 -04:00
request . fetch ( ) ;
2023-05-31 15:31:57 -04:00
// Wait until the timeout should have (or not) occured
QTest : : qWait ( std : : chrono : : duration_cast < std : : chrono : : milliseconds > ( timeout ) . count ( ) + TIMEOUT_GRACE_MS ) ;
2023-05-25 11:10:00 -04:00
QTEST_ASSERT ( didError | | didSucceed ) ;
// Ensures that predicates match - i.e., the header was set correctly
QCOMPARE ( manager . matchedRequests ( ) . length ( ) , 1 ) ;
QCOMPARE ( request . URL ( ) , requestedURL ) ;
QCOMPARE ( didSucceed , ! expectError ) ;
QCOMPARE ( didError , expectError ) ;
}
void TestNetworkRequest : : testNetworkRequestTimeout_data ( )
{
QTest : : addColumn < bool > ( " expectError " ) ;
QTest : : addColumn < std : : chrono : : milliseconds > ( " delay " ) ;
QTest : : addColumn < std : : chrono : : milliseconds > ( " timeout " ) ;
2023-05-31 14:15:22 -04:00
QTest : : newRow ( " timeout " ) < < true < < std : : chrono : : milliseconds { 100 } < < std : : chrono : : milliseconds { 50 } ;
QTest : : newRow ( " no timeout " ) < < false < < std : : chrono : : milliseconds { 50 } < < std : : chrono : : milliseconds { 100 } ;
2023-05-09 18:59:16 -04:00
}
2023-05-25 11:10:00 -04:00
2023-05-09 18:59:16 -04:00
void TestNetworkRequest : : testNetworkRequestRedirects ( )
{
2023-05-10 16:30:42 -04:00
// Should respect max number of redirects
// Headers, Reply, etc. should reflect final request
2023-05-26 10:08:59 -04:00
QFETCH ( int , numRedirects ) ;
QFETCH ( int , maxRedirects ) ;
const bool expectError = numRedirects > maxRedirects ;
const auto requestedURL = QUrl ( " https://example.com " ) ;
const auto expectedUserAgent = QString ( " KeePassXC " ) ;
// Create mock reply
// Create and configure the mocked network access manager
MockNetworkAccess : : Manager < QNetworkAccessManager > manager ;
QStringList requestedUrls ;
auto * reply = & manager
. whenGet ( requestedURL )
// Has right user agent?
. has ( MockNetworkAccess : : Predicates : : HeaderMatching ( QNetworkRequest : : UserAgentHeader ,
QRegularExpression ( expectedUserAgent ) ) )
. reply ( ) ;
for ( int i = 0 ; i < numRedirects ; + + i ) {
auto redirectTarget = QUrl ( " https://example.com/redirect " + QString : : number ( i ) ) ;
reply - > withRedirect ( redirectTarget ) ;
reply = & manager . whenGet ( redirectTarget )
// Has right user agent?
. has ( MockNetworkAccess : : Predicates : : HeaderMatching ( QNetworkRequest : : UserAgentHeader ,
QRegularExpression ( expectedUserAgent ) ) )
. reply ( ) ;
}
reply - > withBody ( QString { " test-content " } . toUtf8 ( ) ) ;
// Create request
2023-05-31 15:40:12 -04:00
NetworkRequest request = buildRequest ( requestedURL ) . setManager ( & manager )
2023-05-31 14:15:22 -04:00
. setMaxRedirects ( maxRedirects ) . build ( ) ;
2023-05-26 10:08:59 -04:00
bool didSucceed = false , didError = false ;
// Check request
QSignalSpy spy ( & request , & NetworkRequest : : success ) ;
connect ( & request , & NetworkRequest : : success , [ & didSucceed ] ( const QByteArray & ) {
didSucceed = true ;
} ) ;
QSignalSpy errorSpy ( & request , & NetworkRequest : : failure ) ;
connect ( & request , & NetworkRequest : : failure , [ & didError ] ( ) { didError = true ; } ) ;
2023-05-31 14:15:22 -04:00
request . fetch ( ) ;
2023-05-31 15:31:57 -04:00
QTest : : qWait ( 300 ) ;
2023-05-26 10:08:59 -04:00
QTEST_ASSERT ( didError | | didSucceed ) ;
// Ensures that predicates match - i.e., the header was set correctly
QCOMPARE ( didSucceed , ! expectError ) ;
QCOMPARE ( didError , expectError ) ;
if ( didSucceed ) {
QCOMPARE ( manager . matchedRequests ( ) . length ( ) , numRedirects + 1 ) ;
QCOMPARE ( request . URL ( ) , requestedURL ) ;
}
2023-05-09 18:59:16 -04:00
}
2023-05-26 10:08:59 -04:00
void TestNetworkRequest : : testNetworkRequestRedirects_data ( )
{
QTest : : addColumn < int > ( " numRedirects " ) ;
QTest : : addColumn < int > ( " maxRedirects " ) ;
2023-05-31 14:15:22 -04:00
QTest : : newRow ( " fewer redirects than allowed (0) " ) < < 0 < < 5 ;
QTest : : newRow ( " fewer redirects than allowed (1) " ) < < 1 < < 5 ;
QTest : : newRow ( " fewer redirects than allowed (2) " ) < < 2 < < 5 ;
QTest : : newRow ( " more redirects than allowed (1, 0) " ) < < 1 < < 0 ;
QTest : : newRow ( " more redirects than allowed (2, 1) " ) < < 2 < < 1 ;
QTest : : newRow ( " more redirects than allowed (3, 2) " ) < < 3 < < 2 ;
2023-05-31 15:31:57 -04:00
}
void TestNetworkRequest : : testNetworkRequestTimeoutWithRedirects ( )
{
// Test that the timeout parameter is respected even when redirects are involved:
// - Set up a request that will redirect 2 times
// - Each request should have a delay of 250ms
// - The timeout should be 400ms
// -> The request should fail
const int numRedirects = 3 ;
const auto delayPerRequest = std : : chrono : : milliseconds { 250 } ;
const auto timeout = std : : chrono : : milliseconds { 400 } ;
const auto requestedURL = QUrl ( " https://example.com " ) ;
// Create mock reply
// Create and configure the mocked network access manager
MockNetworkAccess : : Manager < QNetworkAccessManager > manager ;
QStringList requestedUrls ;
auto * reply = & manager . whenGet ( requestedURL ) . reply ( ) ;
std : : vector < std : : unique_ptr < QTimer > > timers ;
auto nextDelay = delayPerRequest ;
for ( int i = 0 ; i < numRedirects ; + + i ) {
auto redirectTarget = QUrl ( " https://example.com/redirect " + QString : : number ( i ) ) ;
auto timer ( std : : make_unique < QTimer > ( ) ) ;
timer - > setSingleShot ( true ) ;
timer - > start ( delayPerRequest ) ;
nextDelay + = delayPerRequest ;
reply - > withRedirect ( redirectTarget ) . withFinishDelayUntil ( timer . get ( ) , & QTimer : : timeout ) ;
reply = & manager . whenGet ( redirectTarget ) . reply ( ) ;
timers . push_back ( std : : move ( timer ) ) ;
}
reply - > withBody ( QString { " test-content " } . toUtf8 ( ) ) ;
// Create request
2023-05-31 15:40:12 -04:00
NetworkRequest request = buildRequest ( requestedURL ) . setManager ( & manager )
2023-05-31 15:31:57 -04:00
. setTimeout ( timeout )
. setMaxRedirects ( NetworkRequest : : UNLIMITED_REDIRECTS ) . build ( ) ;
bool didSucceed = false , didError = false ;
// Check request
QSignalSpy spy ( & request , & NetworkRequest : : success ) ;
connect ( & request , & NetworkRequest : : success , [ & didSucceed ] ( const QByteArray & ) {
didSucceed = true ;
} ) ;
QSignalSpy errorSpy ( & request , & NetworkRequest : : failure ) ;
connect ( & request , & NetworkRequest : : failure , [ & didError ] ( ) { didError = true ; } ) ;
request . fetch ( ) ;
// Wait until the timeout should have occured
QTest : : qWait ( std : : chrono : : duration_cast < std : : chrono : : milliseconds > ( timeout ) . count ( ) + TIMEOUT_GRACE_MS ) ;
QTEST_ASSERT ( didError | | didSucceed ) ;
QCOMPARE ( didSucceed , false ) ;
QCOMPARE ( didError , true ) ;
}
2023-05-31 18:50:32 -04:00
void TestNetworkRequest : : testNetworkRequestSecurityParameter ( )
{
// Test that requests with allowInsecure() set to false fail when the URL uses an insecure schema
QFETCH ( QUrl , targetURL ) ;
QFETCH ( bool , allowInsecure ) ;
QFETCH ( bool , shouldSucceed ) ;
// Create mock reply
// Create and configure the mocked network access manager
MockNetworkAccess : : Manager < QNetworkAccessManager > manager ;
QStringList requestedUrls ;
auto * reply = & manager . whenGet ( targetURL ) . reply ( ) ;
reply - > withBody ( QString { " test-content " } . toUtf8 ( ) ) ;
// Create request
NetworkRequest request = buildRequest ( targetURL ) . setManager ( & manager )
. setAllowInsecure ( allowInsecure ) . build ( ) ;
bool didSucceed = false , didError = false ;
// Check request
QSignalSpy spy ( & request , & NetworkRequest : : success ) ;
connect ( & request , & NetworkRequest : : success , [ & didSucceed ] ( const QByteArray & ) {
didSucceed = true ;
} ) ;
QSignalSpy errorSpy ( & request , & NetworkRequest : : failure ) ;
connect ( & request , & NetworkRequest : : failure , [ & didError ] ( ) { didError = true ; } ) ;
request . fetch ( ) ;
QTest : : qWait ( 300 ) ;
QTEST_ASSERT ( didError | | didSucceed ) ;
QCOMPARE ( didSucceed , shouldSucceed ) ;
QCOMPARE ( didError , ! shouldSucceed ) ;
}
void TestNetworkRequest : : testNetworkRequestSecurityParameter_data ( )
{
QTest : : addColumn < QUrl > ( " targetURL " ) ;
QTest : : addColumn < bool > ( " allowInsecure " ) ;
QTest : : addColumn < bool > ( " shouldSucceed " ) ;
QTest : : newRow ( " secure protocol with allowInsecure=false succeeds " ) < < QUrl ( " https://example.com " ) < < false < < true ;
QTest : : newRow ( " secure protocol with allowInsecure=true succeeds " ) < < QUrl ( " https://example.com " ) < < true < < true ;
QTest : : newRow ( " insecure protocol with allowInsecure=false fails " ) < < QUrl ( " http://example.com " ) < < false < < false ;
QTest : : newRow ( " insecure protocol with allowInsecure=true succeeds " ) < < QUrl ( " http://example.com " ) < < true < < true ;
}