1281 lines
36 KiB
C++
1281 lines
36 KiB
C++
/////////////////////////////////////////////////////////////////////////////
|
|
// Name: tests/net/webrequest.cpp
|
|
// Purpose: wxWebRequest test
|
|
// Author: Tobias Taschner
|
|
// Created: 2018-10-24
|
|
// Copyright: (c) 2018 wxWidgets development team
|
|
// Licence: wxWindows licence
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// headers
|
|
// ----------------------------------------------------------------------------
|
|
|
|
#include "testprec.h"
|
|
|
|
#ifndef WX_PRECOMP
|
|
#include "wx/wx.h"
|
|
#endif // WX_PRECOMP
|
|
|
|
#if wxUSE_WEBREQUEST
|
|
|
|
#include "wx/webrequest.h"
|
|
#include "wx/filename.h"
|
|
#include "wx/uri.h"
|
|
#include "wx/wfstream.h"
|
|
|
|
#include <memory>
|
|
#include <unordered_map>
|
|
|
|
// This test uses httpbin service and by default uses the mirror at the
|
|
// location below, which seems to be more reliable than the main site at
|
|
// https://httpbin.org. Any other mirror, including a local one, which can be
|
|
// set by running kennethreitz/httpbin Docker container, can be used by setting
|
|
// WX_TEST_WEBREQUEST_URL environment variable to its URL.
|
|
//
|
|
// This variable can also be set to a special value "0" to disable running the
|
|
// test entirely.
|
|
static const char* WX_TEST_WEBREQUEST_URL_DEFAULT = "https://nghttp2.org/httpbin";
|
|
|
|
// Other environment variables used by this test:
|
|
//
|
|
// - WX_TEST_WEBREQUEST_USE_BADSSL: setting this to 1 is equivalent to setting
|
|
// all the variables below to their example values using badssl.com.
|
|
//
|
|
// - WX_TEST_WEBREQUEST_URL_SELF_SIGNED: set to https://self-signed.badssl.com/
|
|
// or any other server using self-signed certificate to test disabling SSL
|
|
// certificate trust chain verification.
|
|
//
|
|
// - WX_TEST_WEBREQUEST_URL_EXPIRED: set to https://expired.badssl.com/ or any
|
|
// other server using expired certificate to test disabling SSL certificate
|
|
// date validity verification.
|
|
//
|
|
// - WX_TEST_WEBREQUEST_URL_BADHOST: set to https://wrong.host.badssl.com/ or
|
|
// any other server using certificate with wrong host name to test disabling
|
|
// SSL host name verification.
|
|
|
|
// Common base for sync and async tests fixtures.
|
|
class BaseRequestFixture
|
|
{
|
|
protected:
|
|
// All tests should call this function first and skip the test entirely if
|
|
// it returns false, as this indicates that web requests tests are disabled.
|
|
bool InitBaseURL()
|
|
{
|
|
if ( wxGetEnv("WX_TEST_WEBREQUEST_URL", &baseURL) )
|
|
{
|
|
if ( baseURL == "0" )
|
|
return false;
|
|
|
|
static bool s_shown = false;
|
|
if ( !s_shown )
|
|
{
|
|
s_shown = true;
|
|
WARN("Using non-default root URL " << baseURL);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
baseURL = WX_TEST_WEBREQUEST_URL_DEFAULT;
|
|
}
|
|
|
|
REQUIRE( GetSession().SetBaseURL(baseURL) );
|
|
|
|
return true;
|
|
}
|
|
|
|
enum class BadSSLKind
|
|
{
|
|
SelfSigned,
|
|
Expired,
|
|
BadHost
|
|
};
|
|
|
|
// Return true if the test is enabled by setting the corresponding
|
|
// environment variable, false if it should be skipped.
|
|
bool GetBadSSLURL(BadSSLKind kind, wxString* url) const
|
|
{
|
|
const auto useBadSSL = [](const char* badSSLURL, wxString* url)
|
|
{
|
|
wxString s;
|
|
if ( !wxGetEnv("WX_TEST_WEBREQUEST_USE_BADSSL", &s) || s != "1" )
|
|
return false;
|
|
|
|
*url = badSSLURL;
|
|
|
|
return true;
|
|
};
|
|
|
|
switch ( kind )
|
|
{
|
|
case BadSSLKind::SelfSigned:
|
|
if ( wxGetEnv("WX_TEST_WEBREQUEST_URL_SELF_SIGNED", url) )
|
|
return true;
|
|
|
|
if ( useBadSSL("https://self-signed.badssl.com/", url) )
|
|
return true;
|
|
break;
|
|
|
|
case BadSSLKind::Expired:
|
|
if ( wxGetEnv("WX_TEST_WEBREQUEST_URL_EXPIRED", url) )
|
|
return true;
|
|
|
|
if ( useBadSSL("https://expired.badssl.com/", url) )
|
|
return true;
|
|
break;
|
|
|
|
case BadSSLKind::BadHost:
|
|
if ( wxGetEnv("WX_TEST_WEBREQUEST_URL_BADHOST", url) )
|
|
return true;
|
|
|
|
if ( useBadSSL("https://wrong.host.badssl.com/", url) )
|
|
return true;
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
CreateWithAuth(const wxString& relURL,
|
|
const wxString& user,
|
|
const wxString& password)
|
|
{
|
|
wxString url = baseURL;
|
|
if ( !url.EndsWith('/') && !relURL.StartsWith('/') )
|
|
url += '/';
|
|
url += relURL;
|
|
|
|
wxURI uri(url);
|
|
uri.SetUserAndPassword(user, password);
|
|
|
|
Create(uri.BuildURI());
|
|
}
|
|
|
|
virtual void Create(const wxString& url) = 0;
|
|
|
|
virtual wxWebSessionBase& GetSession() = 0;
|
|
|
|
virtual wxWebRequestBase& GetRequest() = 0;
|
|
|
|
// Check that the response is a JSON object containing a specific key with the
|
|
// expected value.
|
|
void CheckExpectedJSON(const wxString& response, const wxString& key,
|
|
const wxString& value)
|
|
{
|
|
// We ought to really parse the returned JSON object, but to keep things as
|
|
// simple as possible for now we just treat it as a string.
|
|
INFO("Response: " << response);
|
|
|
|
wxString expectedKey = wxString::Format("\"%s\":", key);
|
|
size_t pos = response.find(expectedKey);
|
|
REQUIRE( pos != wxString::npos );
|
|
|
|
pos += expectedKey.size();
|
|
|
|
// There may, or not, be a space after it.
|
|
// And the value may be returned in an array.
|
|
while ( wxIsspace(response[pos]) ||
|
|
response[pos] == '"' ||
|
|
response[pos] == '[' )
|
|
{
|
|
++pos;
|
|
}
|
|
|
|
wxString actualValue = response.substr(pos, value.size());
|
|
REQUIRE( actualValue == value );
|
|
}
|
|
|
|
// Special helper for "manual" tests taking the URL from the environment.
|
|
void InitManualRequest()
|
|
{
|
|
// Allow getting 8-bit strings from the environment correctly.
|
|
setlocale(LC_ALL, "");
|
|
|
|
wxString url;
|
|
if ( !wxGetEnv("WX_TEST_WEBREQUEST_URL", &url) )
|
|
{
|
|
FAIL("Specify WX_TEST_WEBREQUEST_URL");
|
|
}
|
|
|
|
wxString proxyURL;
|
|
if ( wxGetEnv("WX_TEST_WEBREQUEST_PROXY", &proxyURL) )
|
|
{
|
|
wxWebProxy proxy = wxWebProxy::Default();
|
|
|
|
// Interpret some values specially.
|
|
if ( proxyURL == "0" )
|
|
proxy = wxWebProxy::Disable();
|
|
else if ( proxyURL != "1" )
|
|
proxy = wxWebProxy::FromURL(proxyURL);
|
|
|
|
REQUIRE( GetSession().SetProxy(proxy) );
|
|
}
|
|
|
|
Create(url);
|
|
|
|
wxString insecure;
|
|
if ( wxGetEnv("WX_TEST_WEBREQUEST_INSECURE", &insecure) )
|
|
{
|
|
int flags = 0;
|
|
REQUIRE( insecure.ToInt(&flags) );
|
|
|
|
GetRequest().MakeInsecure(flags);
|
|
}
|
|
}
|
|
|
|
private:
|
|
wxString baseURL;
|
|
};
|
|
|
|
class RequestFixture : public wxTimer, public BaseRequestFixture
|
|
{
|
|
public:
|
|
RequestFixture()
|
|
{
|
|
expectedFileSize = 0;
|
|
dataSize = 0;
|
|
stateFromEvent = wxWebRequest::State_Idle;
|
|
statusFromEvent = 0;
|
|
}
|
|
|
|
void Create(const wxString& url) override
|
|
{
|
|
request = wxWebSession::GetDefault().CreateRequest(this, url);
|
|
REQUIRE( request.IsOk() );
|
|
|
|
Bind(wxEVT_WEBREQUEST_STATE, &RequestFixture::OnRequestState, this);
|
|
Bind(wxEVT_WEBREQUEST_DATA, &RequestFixture::OnData, this);
|
|
}
|
|
|
|
wxWebSessionBase& GetSession() override
|
|
{
|
|
return wxWebSession::GetDefault();
|
|
}
|
|
|
|
wxWebRequestBase& GetRequest() override
|
|
{
|
|
return request;
|
|
}
|
|
|
|
void OnRequestState(wxWebRequestEvent& evt)
|
|
{
|
|
stateFromEvent = evt.GetState();
|
|
const wxWebResponse& response = evt.GetResponse();
|
|
if ( response.IsOk() )
|
|
{
|
|
// Note that the response object itself may be deleted if request
|
|
// using it is, so we need to copy its data to use it later.
|
|
statusFromEvent = response.GetStatus();
|
|
responseStringFromEvent = response.AsString();
|
|
}
|
|
|
|
switch ( stateFromEvent )
|
|
{
|
|
case wxWebRequest::State_Idle:
|
|
FAIL("should never get events with State_Idle");
|
|
break;
|
|
|
|
case wxWebRequest::State_Active:
|
|
CHECK( request.GetNativeHandle() );
|
|
break;
|
|
|
|
case wxWebRequest::State_Completed:
|
|
if ( request.IsOk() && request.GetStorage() == wxWebRequest::Storage_File )
|
|
{
|
|
wxFileName fn(evt.GetDataFile());
|
|
CHECK( fn.GetSize() == expectedFileSize );
|
|
}
|
|
wxFALLTHROUGH;
|
|
|
|
case wxWebRequest::State_Unauthorized:
|
|
case wxWebRequest::State_Failed:
|
|
case wxWebRequest::State_Cancelled:
|
|
errorDescription = evt.GetErrorDescription();
|
|
loop.Exit();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Notify() override
|
|
{
|
|
WARN("Exiting loop on timeout");
|
|
loop.Exit();
|
|
}
|
|
|
|
void OnData(wxWebRequestEvent& evt)
|
|
{
|
|
// Count all bytes received via data event for Storage_None
|
|
dataSize += evt.GetDataSize();
|
|
}
|
|
|
|
void RunLoopWithTimeout()
|
|
{
|
|
StartOnce(30000); // Ensure that we exit the loop after 30s.
|
|
loop.Run();
|
|
Stop();
|
|
}
|
|
|
|
void Run(wxWebRequest::State requiredState = wxWebRequest::State_Completed,
|
|
int requiredStatus = 200)
|
|
{
|
|
REQUIRE( request.GetState() == wxWebRequest::State_Idle );
|
|
request.Start();
|
|
RunLoopWithTimeout();
|
|
|
|
if ( stateFromEvent != requiredState )
|
|
{
|
|
errorDescription.Trim();
|
|
if ( !errorDescription.empty() )
|
|
WARN("Error: " << errorDescription);
|
|
}
|
|
|
|
REQUIRE( stateFromEvent == requiredState );
|
|
|
|
CHECK( request.GetState() == stateFromEvent );
|
|
|
|
if (requiredStatus)
|
|
{
|
|
CHECK( statusFromEvent == requiredStatus );
|
|
CHECK( request.GetResponse().GetStatus() == requiredStatus );
|
|
}
|
|
}
|
|
|
|
// Precondition: we must have an auth challenge.
|
|
void UseCredentials(const wxString& user, const wxString& password)
|
|
{
|
|
request.GetAuthChallenge().SetCredentials(
|
|
wxWebCredentials(user, wxSecretValue(password)));
|
|
}
|
|
|
|
wxEventLoop loop;
|
|
wxWebRequest request;
|
|
wxWebRequest::State stateFromEvent;
|
|
int statusFromEvent;
|
|
wxString responseStringFromEvent;
|
|
wxInt64 expectedFileSize;
|
|
wxInt64 dataSize;
|
|
wxString errorDescription;
|
|
};
|
|
|
|
// Download more than 64KiB bytes to test that downloading more than the
|
|
// default buffer size works correctly.
|
|
constexpr int DOWNLOAD_BYTES = 99999;
|
|
|
|
// Substring used to check that we got the expected response after
|
|
// authenticating successfully. It is so weird because httpbin and go-httpbin
|
|
// use different strings for this: one uses "authenticated" while the other
|
|
// ones uses "authorized", so we use a substring common to both of them.
|
|
constexpr char AUTHORIZED_SUBSTRING[] = R"(ed": true)";
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Get::Bytes", "[net][webrequest][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create(wxString::Format("bytes/%d", DOWNLOAD_BYTES));
|
|
Run();
|
|
CHECK( request.GetResponse().GetContentLength() == DOWNLOAD_BYTES );
|
|
CHECK( request.GetBytesExpectedToReceive() == DOWNLOAD_BYTES );
|
|
CHECK( request.GetBytesReceived() == DOWNLOAD_BYTES );
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Get::Simple", "[net][webrequest][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
// Note that the session may be initialized on demand, so don't check the
|
|
// native handle before actually using it.
|
|
wxWebSession& session = wxWebSession::GetDefault();
|
|
REQUIRE( session.IsOpened() );
|
|
|
|
// Request is not initialized yet.
|
|
CHECK( !request.IsOk() );
|
|
CHECK( !request.GetNativeHandle() );
|
|
|
|
Create("status/200");
|
|
CHECK( request.IsOk() );
|
|
CHECK( session.GetNativeHandle() );
|
|
|
|
// Note that the request must be started to have a valid native handle.
|
|
request.Start();
|
|
CHECK( request.GetNativeHandle() );
|
|
RunLoopWithTimeout();
|
|
CHECK( request.GetState() == wxWebRequest::State_Completed );
|
|
CHECK( request.GetResponse().GetStatus() == 200 );
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Get::String", "[net][webrequest][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("base64/VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==");
|
|
Run();
|
|
CHECK( request.GetResponse().AsString() == "The quick brown fox jumps over the lazy dog" );
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Get::Header", "[net][webrequest][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("response-headers?freeform=wxWidgets%20works!");
|
|
Run();
|
|
CHECK( request.GetResponse().GetHeader("freeform") == "wxWidgets works!" );
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Get::AllHeaderValues", "[net][webrequest][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("response-headers?freeform=wxWidgets&freeform=works!");
|
|
Run();
|
|
std::vector<wxString> headers = request.GetResponse().GetAllHeaderValues("freeform");
|
|
|
|
#if wxUSE_WEBREQUEST_URLSESSION
|
|
// The httpbin service concatenates the given parameters.
|
|
REQUIRE( headers.size() == 1 );
|
|
CHECK( headers[0] == "wxWidgets, works!" );
|
|
#else
|
|
REQUIRE( headers.size() == 2 );
|
|
CHECK( (headers[0] == "wxWidgets" && headers[1] == "works!") );
|
|
#endif
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Get::Param", "[net][webrequest][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
wxString key = "pi";
|
|
wxString value = "3.14159265358979323";
|
|
|
|
Create(wxString::Format("get?%s=%s", key, value));
|
|
Run();
|
|
|
|
CheckExpectedJSON( request.GetResponse().AsString(), key, value );
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Get::File", "[net][webrequest][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
expectedFileSize = 99 * 1024;
|
|
Create(wxString::Format("bytes/%lld", expectedFileSize));
|
|
request.SetStorage(wxWebRequest::Storage_File);
|
|
Run();
|
|
CHECK( request.GetBytesReceived() == expectedFileSize );
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Get::None", "[net][webrequest][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
int processingSize = 99 * 1024;
|
|
Create(wxString::Format("bytes/%d", processingSize));
|
|
request.SetStorage(wxWebRequest::Storage_None);
|
|
Run();
|
|
CHECK( request.GetBytesReceived() == processingSize );
|
|
CHECK( dataSize == processingSize );
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Error::HTTP", "[net][webrequest][error]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("status/404");
|
|
Run(wxWebRequest::State_Failed, 404);
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Error::Body", "[net][webrequest][error]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("status/418");
|
|
Run(wxWebRequest::State_Failed, 0);
|
|
|
|
CHECK( request.GetResponse().GetStatus() == 418 );
|
|
|
|
const wxString& response = request.GetResponse().AsString();
|
|
INFO( "Response: " << response);
|
|
CHECK( response.Contains("teapot") );
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Error::Connect", "[net][webrequest][error]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("http://127.0.0.1:51234");
|
|
Run(wxWebRequest::State_Failed, 0);
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::SSL::Error", "[net][webrequest][error]")
|
|
{
|
|
wxString url;
|
|
|
|
if ( GetBadSSLURL(BadSSLKind::SelfSigned, &url) )
|
|
{
|
|
INFO("Testing self-signed certificate at " << url);
|
|
|
|
Create(url);
|
|
Run(wxWebRequest::State_Failed, 0);
|
|
|
|
Create(url);
|
|
request.DisablePeerVerify();
|
|
Run(wxWebRequest::State_Completed, 200);
|
|
}
|
|
|
|
if ( GetBadSSLURL(BadSSLKind::Expired, &url) )
|
|
{
|
|
INFO("Testing expired certificate at " << url);
|
|
|
|
Create(url);
|
|
Run(wxWebRequest::State_Failed, 0);
|
|
|
|
Create(url);
|
|
request.MakeInsecure(wxWebRequest::Ignore_Certificate);
|
|
Run(wxWebRequest::State_Completed, 200);
|
|
}
|
|
|
|
if ( GetBadSSLURL(BadSSLKind::BadHost, &url) )
|
|
{
|
|
INFO("Testing certificate with bad host at " << url);
|
|
|
|
Create(url);
|
|
Run(wxWebRequest::State_Failed, 0);
|
|
|
|
// Currently disabling certificate verification also disables host name
|
|
// verification in NSURLSession backend, so skip this test with it.
|
|
if ( wxWebSession::GetDefault().GetLibraryVersionInfo().GetName()
|
|
!= "URLSession" )
|
|
{
|
|
Create(url);
|
|
request.MakeInsecure(wxWebRequest::Ignore_Certificate);
|
|
Run(wxWebRequest::State_Failed, 0);
|
|
}
|
|
|
|
Create(url);
|
|
request.MakeInsecure(wxWebRequest::Ignore_Host);
|
|
Run(wxWebRequest::State_Completed, 200);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Post", "[net][webrequest]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("post");
|
|
request.SetData("app=WebRequestSample&version=1", "application/x-www-form-urlencoded");
|
|
Run();
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Put", "[net][webrequest]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("put");
|
|
std::unique_ptr<wxInputStream> is(new wxFileInputStream("horse.png"));
|
|
REQUIRE( is->IsOk() );
|
|
|
|
request.SetData(is.release(), "image/png");
|
|
request.SetMethod("PUT");
|
|
Run();
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Delete", "[net][webrequest]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("delete");
|
|
request.SetData(R"({"bloordyblop": 17})", "application/json");
|
|
request.SetMethod("DELETE");
|
|
Run();
|
|
|
|
const wxString& response = request.GetResponse().AsString();
|
|
CHECK_THAT( response.utf8_string(),
|
|
Catch::Contains(R"("bloordyblop": 17)") );
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Auth::Basic", "[net][webrequest][auth]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("basic-auth/wxtest/wxwidgets");
|
|
Run(wxWebRequest::State_Unauthorized, 401);
|
|
REQUIRE( request.GetAuthChallenge().IsOk() );
|
|
|
|
SECTION("Good password")
|
|
{
|
|
UseCredentials("wxtest", "wxwidgets");
|
|
RunLoopWithTimeout();
|
|
CHECK( request.GetState() == wxWebRequest::State_Completed );
|
|
|
|
const auto& response = request.GetResponse();
|
|
CHECK( response.GetStatus() == 200 );
|
|
CHECK_THAT( response.AsString().utf8_string(),
|
|
Catch::Contains(AUTHORIZED_SUBSTRING) );
|
|
}
|
|
|
|
SECTION("Bad password")
|
|
{
|
|
UseCredentials("wxtest", "foobar");
|
|
RunLoopWithTimeout();
|
|
CHECK( request.GetResponse().GetStatus() == 401 );
|
|
CHECK( request.GetState() == wxWebRequest::State_Unauthorized );
|
|
}
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Auth::Basic/Reserved", "[net][webrequest][auth]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
// Use some reserved (in the RFC 3986 sense) characters in the user name and
|
|
// the password (as well as a sub-delimiter character '=' in the password).
|
|
Create("basic-auth/u%40d/1%3d2%3f");
|
|
Run(wxWebRequest::State_Unauthorized, 401);
|
|
REQUIRE( request.GetAuthChallenge().IsOk() );
|
|
|
|
UseCredentials("u@d", "1=2?");
|
|
RunLoopWithTimeout();
|
|
CHECK( request.GetState() == wxWebRequest::State_Completed );
|
|
|
|
const auto& response = request.GetResponse();
|
|
CHECK( response.GetStatus() == 200 );
|
|
CHECK_THAT( response.AsString().utf8_string(),
|
|
Catch::Contains(AUTHORIZED_SUBSTRING) );
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Auth::Digest", "[net][webrequest][auth]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("digest-auth/auth/wxtest/wxwidgets");
|
|
Run(wxWebRequest::State_Unauthorized, 401);
|
|
REQUIRE( request.GetAuthChallenge().IsOk() );
|
|
|
|
SECTION("Good password")
|
|
{
|
|
UseCredentials("wxtest", "wxwidgets");
|
|
RunLoopWithTimeout();
|
|
CHECK( request.GetState() == wxWebRequest::State_Completed );
|
|
|
|
const auto& response = request.GetResponse();
|
|
CHECK( response.GetStatus() == 200 );
|
|
CHECK_THAT( response.AsString().utf8_string(),
|
|
Catch::Contains(AUTHORIZED_SUBSTRING) );
|
|
}
|
|
|
|
SECTION("Bad password")
|
|
{
|
|
UseCredentials("foo", "bar");
|
|
RunLoopWithTimeout();
|
|
CHECK( request.GetResponse().GetStatus() == 401 );
|
|
CHECK( request.GetState() == wxWebRequest::State_Unauthorized );
|
|
}
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Auth::BasicInURL", "[net][webrequest][auth]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
CreateWithAuth("basic-auth/wxtest/wxwidgets", "wxtest", "wxwidgets");
|
|
Run();
|
|
|
|
CHECK( request.GetState() == wxWebRequest::State_Completed );
|
|
|
|
const auto& response = request.GetResponse();
|
|
CHECK( response.GetStatus() == 200 );
|
|
CHECK_THAT( response.AsString().utf8_string(),
|
|
Catch::Contains(AUTHORIZED_SUBSTRING) );
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Auth::DigestInURL", "[net][webrequest][auth]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
CreateWithAuth("digest-auth/auth/wxtest/wxwidgets", "wxtest", "wxwidgets");
|
|
Run();
|
|
|
|
CHECK( request.GetState() == wxWebRequest::State_Completed );
|
|
|
|
const auto& response = request.GetResponse();
|
|
CHECK( response.GetStatus() == 200 );
|
|
CHECK_THAT( response.AsString().utf8_string(),
|
|
Catch::Contains(AUTHORIZED_SUBSTRING) );
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Cancel", "[net][webrequest]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("delay/10");
|
|
request.Start();
|
|
request.Cancel();
|
|
RunLoopWithTimeout();
|
|
|
|
#ifdef __WINDOWS__
|
|
// This is another weird test failure that happens only on AppVeyor:
|
|
// sometimes (perhaps because the test machine is too slow?) the request
|
|
// fails instead of (before?) being cancelled.
|
|
if ( IsAutomaticTest() )
|
|
{
|
|
if ( request.GetState() == wxWebRequest::State_Failed )
|
|
{
|
|
WARN("Request unexpectedly failed after cancelling.");
|
|
return;
|
|
}
|
|
}
|
|
#endif // __WINDOWS__
|
|
|
|
REQUIRE( request.GetState() == wxWebRequest::State_Cancelled );
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Destroy", "[net][webrequest]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("base64/U3RpbGwgYWxpdmUh");
|
|
request.Start();
|
|
|
|
// Destroy the original request: this shouldn't prevent it from running to
|
|
// the completion!
|
|
request = wxWebRequest();
|
|
|
|
RunLoopWithTimeout();
|
|
|
|
CHECK( stateFromEvent == wxWebRequest::State_Completed );
|
|
CHECK( statusFromEvent == 200 );
|
|
CHECK( responseStringFromEvent == "Still alive!" );
|
|
}
|
|
|
|
TEST_CASE_METHOD(RequestFixture, "WebRequest::LifeTime", "[net][webrequest]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("status/200");
|
|
Run();
|
|
|
|
// Close the session before the request is destroyed: this shouldn't result
|
|
// in a crash.
|
|
wxWebSession::GetDefault().Close();
|
|
|
|
CHECK( request.GetResponse().GetStatus() == 200 );
|
|
}
|
|
|
|
class SyncRequestFixture : public BaseRequestFixture
|
|
{
|
|
public:
|
|
void Create(const wxString& url) override
|
|
{
|
|
request = wxWebSessionSync::GetDefault().CreateRequest(url);
|
|
REQUIRE( request.IsOk() );
|
|
}
|
|
|
|
wxWebSessionBase& GetSession() override
|
|
{
|
|
return wxWebSessionSync::GetDefault();
|
|
}
|
|
|
|
wxWebRequestBase& GetRequest() override
|
|
{
|
|
return request;
|
|
}
|
|
|
|
bool Execute(const wxString& url)
|
|
{
|
|
Create(url);
|
|
return Execute();
|
|
}
|
|
|
|
bool Execute()
|
|
{
|
|
const auto result = request.Execute();
|
|
|
|
response = request.GetResponse();
|
|
state = result.state;
|
|
error = result.error;
|
|
|
|
switch ( state )
|
|
{
|
|
case wxWebRequest::State_Idle:
|
|
case wxWebRequest::State_Active:
|
|
case wxWebRequest::State_Cancelled:
|
|
wxFAIL_MSG("Unexpected state");
|
|
wxFALLTHROUGH;
|
|
|
|
case wxWebRequest::State_Failed:
|
|
case wxWebRequest::State_Unauthorized:
|
|
break;
|
|
|
|
case wxWebRequest::State_Completed:
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
wxWebRequestSync request;
|
|
wxWebResponse response;
|
|
wxWebRequest::State state = wxWebRequest::State_Idle;
|
|
wxString error;
|
|
};
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Get::Bytes", "[net][webrequest][sync][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
REQUIRE( Execute(wxString::Format("bytes/%d", DOWNLOAD_BYTES)) );
|
|
|
|
CHECK( response.GetStatus() == 200 );
|
|
CHECK( response.GetContentLength() == DOWNLOAD_BYTES );
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Get::Simple", "[net][webrequest][sync][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
// Note that the session may be initialized on demand, so don't check the
|
|
// native handle before actually using it.
|
|
wxWebSessionSync& session = wxWebSessionSync::GetDefault();
|
|
REQUIRE( session.IsOpened() );
|
|
|
|
// Request is not initialized yet.
|
|
CHECK( !request.IsOk() );
|
|
CHECK( !request.GetNativeHandle() );
|
|
|
|
REQUIRE( Execute("status/200") );
|
|
CHECK( request.IsOk() );
|
|
CHECK( request.GetNativeHandle() );
|
|
CHECK( session.GetNativeHandle() );
|
|
|
|
CHECK( response.GetStatus() == 200 );
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Get::String", "[net][webrequest][sync][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
REQUIRE( Execute("base64/VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==") );
|
|
|
|
CHECK( response.AsString() == "The quick brown fox jumps over the lazy dog" );
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Get::Header", "[net][webrequest][sync][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
REQUIRE( Execute("response-headers?freeform=wxWidgets%20works!") );
|
|
|
|
CHECK( response.GetHeader("freeform") == "wxWidgets works!" );
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Get::Param", "[net][webrequest][sync][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
wxString key = "pi";
|
|
wxString value = "3.14159265358979323";
|
|
|
|
REQUIRE( Execute(wxString::Format("get?%s=%s", key, value)) );
|
|
|
|
CheckExpectedJSON( response.AsString(), key, value );
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Get::File", "[net][webrequest][sync][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
wxInt64 expectedFileSize = 99 * 1024;
|
|
Create(wxString::Format("bytes/%lld", expectedFileSize));
|
|
request.SetStorage(wxWebRequest::Storage_File);
|
|
|
|
REQUIRE( Execute() );
|
|
|
|
CHECK( request.GetBytesReceived() == expectedFileSize );
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Get::None", "[net][webrequest][sync][get]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
int processingSize = 99 * 1024;
|
|
Create(wxString::Format("bytes/%d", processingSize));
|
|
request.SetStorage(wxWebRequest::Storage_None);
|
|
|
|
REQUIRE( Execute() );
|
|
|
|
CHECK( request.GetBytesReceived() == processingSize );
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Error::HTTP", "[net][webrequest][sync][error]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
CHECK_FALSE( Execute("status/404") );
|
|
|
|
CHECK( response.GetStatus() == 404 );
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Error::Body", "[net][webrequest][sync][error]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
CHECK_FALSE( Execute("status/418") );
|
|
|
|
CHECK( response.GetStatus() == 418 );
|
|
CHECK_THAT( response.AsString().utf8_string(), Catch::Contains("teapot") );
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Error::Connect", "[net][webrequest][sync][error]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("http://127.0.0.1:51234");
|
|
CHECK( !Execute() );
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::SSL::Error", "[net][webrequest][sync][error]")
|
|
{
|
|
if ( wxWebSession::GetDefault().GetLibraryVersionInfo().GetName()
|
|
== "URLSession" )
|
|
{
|
|
WARN("Disabling SSL verification is not supported with URLSession backend");
|
|
return;
|
|
}
|
|
|
|
wxString url;
|
|
|
|
if ( GetBadSSLURL(BadSSLKind::SelfSigned, &url) )
|
|
{
|
|
INFO("Testing self-signed certificate at " << url);
|
|
|
|
Create(url);
|
|
CHECK( !Execute() );
|
|
|
|
Create(url);
|
|
request.DisablePeerVerify();
|
|
|
|
REQUIRE( Execute() );
|
|
CHECK( response.GetStatus() == 200 );
|
|
}
|
|
|
|
if ( GetBadSSLURL(BadSSLKind::Expired, &url) )
|
|
{
|
|
INFO("Testing expired certificate at " << url);
|
|
|
|
Create(url);
|
|
CHECK( !Execute() );
|
|
|
|
Create(url);
|
|
request.MakeInsecure(wxWebRequest::Ignore_Certificate);
|
|
REQUIRE( Execute() );
|
|
CHECK( response.GetStatus() == 200 );
|
|
}
|
|
|
|
if ( GetBadSSLURL(BadSSLKind::BadHost, &url) )
|
|
{
|
|
INFO("Testing certificate with bad host at " << url);
|
|
|
|
Create(url);
|
|
CHECK( !Execute() );
|
|
|
|
Create(url);
|
|
request.MakeInsecure(wxWebRequest::Ignore_Certificate);
|
|
CHECK( !Execute() );
|
|
|
|
Create(url);
|
|
request.MakeInsecure(wxWebRequest::Ignore_Host);
|
|
REQUIRE( Execute() );
|
|
CHECK( response.GetStatus() == 200 );
|
|
}
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Post", "[net][webrequest][sync]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("post");
|
|
request.SetData("app=WebRequestSample&version=1", "application/x-www-form-urlencoded");
|
|
REQUIRE( Execute() );
|
|
CHECK( response.GetStatus() == 200 );
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Put", "[net][webrequest][sync]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("put");
|
|
std::unique_ptr<wxInputStream> is(new wxFileInputStream("horse.png"));
|
|
REQUIRE( is->IsOk() );
|
|
|
|
request.SetData(is.release(), "image/png");
|
|
request.SetMethod("PUT");
|
|
REQUIRE( Execute() );
|
|
CHECK( response.GetStatus() == 200 );
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Delete", "[net][webrequest][sync]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
Create("delete");
|
|
request.SetData(R"({"bloordyblop": 17})", "application/json");
|
|
request.SetMethod("DELETE");
|
|
REQUIRE( Execute() );
|
|
|
|
CHECK( response.GetStatus() == 200 );
|
|
CHECK_THAT( response.AsString().utf8_string(),
|
|
Catch::Contains(R"("bloordyblop": 17)") );
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Auth::Basic", "[net][webrequest][sync][auth]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
SECTION("No password")
|
|
{
|
|
Create("basic-auth/wxtest/wxwidgets");
|
|
CHECK_FALSE( Execute() );
|
|
CHECK( state == wxWebRequest::State_Unauthorized );
|
|
CHECK( response.GetStatus() == 401 );
|
|
}
|
|
|
|
SECTION("Good password")
|
|
{
|
|
CreateWithAuth("basic-auth/wxtest/wxwidgets", "wxtest", "wxwidgets");
|
|
CHECK( Execute() );
|
|
CHECK( response.GetStatus() == 200 );
|
|
CHECK( state == wxWebRequest::State_Completed );
|
|
|
|
CHECK_THAT( response.AsString().utf8_string(),
|
|
Catch::Contains(AUTHORIZED_SUBSTRING) );
|
|
}
|
|
|
|
SECTION("Bad password")
|
|
{
|
|
CreateWithAuth("basic-auth/wxtest/wxwidgets", "wxtest", "foobar");
|
|
CHECK_FALSE( Execute() );
|
|
CHECK( response.GetStatus() == 401 );
|
|
CHECK( state == wxWebRequest::State_Unauthorized );
|
|
}
|
|
|
|
SECTION("Reserved characters")
|
|
{
|
|
if ( wxWebSession::GetDefault().GetLibraryVersionInfo().GetName()
|
|
== "URLSession" )
|
|
{
|
|
// NSURLSession doesn't decode percent-encoded characters in the
|
|
// password (as indirectly confirmed by the documentation of NSURL
|
|
// password property, which says that "Any percent-encoded
|
|
// characters are not unescaped.", resulting in sending wrong
|
|
// password to the server if we use any reserved characters in it.
|
|
CreateWithAuth("basic-auth/u%40d/1=2", "u@d", "1=2");
|
|
}
|
|
else
|
|
{
|
|
// With the other backends, using reserved characters in the
|
|
// password does work.
|
|
CreateWithAuth("basic-auth/u%40d/1%3d2%3f", "u@d", "1=2?");
|
|
}
|
|
|
|
REQUIRE( Execute() );
|
|
CHECK( response.GetStatus() == 200 );
|
|
CHECK( state == wxWebRequest::State_Completed );
|
|
|
|
CHECK_THAT( response.AsString().utf8_string(),
|
|
Catch::Contains(AUTHORIZED_SUBSTRING) );
|
|
}
|
|
}
|
|
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Auth::Digest", "[net][webrequest][sync][auth]")
|
|
{
|
|
if ( !InitBaseURL() )
|
|
return;
|
|
|
|
const auto& versionInfo = wxWebSession::GetDefault().GetLibraryVersionInfo();
|
|
if ( versionInfo.GetName() == "libcurl" && !versionInfo.AtLeast(7, 60) )
|
|
{
|
|
// This test fails under Ubuntu 18.04 which uses libcurl 7.58 with GnuTLS.
|
|
// It's not clear whether it does it because libcurl is too old or
|
|
// because of using GnuTLS instead of OpenSSL used elsewhere, but for
|
|
// now just skip it.
|
|
WARN("Skipping Digest auth test because it's known to fail with old libcurl");
|
|
return;
|
|
}
|
|
|
|
SECTION("No password")
|
|
{
|
|
Create("digest-auth/auth/wxtest/wxwidgets");
|
|
CHECK_FALSE( Execute() );
|
|
CHECK( state == wxWebRequest::State_Unauthorized );
|
|
CHECK( response.GetStatus() == 401 );
|
|
}
|
|
|
|
SECTION("Good password")
|
|
{
|
|
CreateWithAuth("digest-auth/auth/wxtest/wxwidgets", "wxtest", "wxwidgets");
|
|
CHECK( Execute() );
|
|
CHECK( response.GetStatus() == 200 );
|
|
CHECK( state == wxWebRequest::State_Completed );
|
|
|
|
CHECK_THAT( response.AsString().utf8_string(),
|
|
Catch::Contains(AUTHORIZED_SUBSTRING) );
|
|
}
|
|
|
|
SECTION("Bad password")
|
|
{
|
|
CreateWithAuth("digest-auth/auth/wxtest/wxwidgets", "foo", "bar");
|
|
CHECK_FALSE( Execute() );
|
|
CHECK( response.GetStatus() == 401 );
|
|
CHECK( state == wxWebRequest::State_Unauthorized );
|
|
}
|
|
}
|
|
|
|
|
|
static void DumpResponse(const wxWebResponse& response)
|
|
{
|
|
REQUIRE( response.IsOk() );
|
|
WARN("URL: " << response.GetURL() << "\n" <<
|
|
"Status: " << response.GetStatus()
|
|
<< " (" << response.GetStatusText() << ")\n" <<
|
|
"Body length: " << response.GetContentLength() << "\n" <<
|
|
"Body: " << response.AsString() << "\n");
|
|
|
|
// Also show the value of the given header if requested.
|
|
wxString header;
|
|
if ( wxGetEnv("WX_TEST_WEBREQUEST_HEADER", &header) )
|
|
{
|
|
WARN("Header " << header << ": " << response.GetHeader(header));
|
|
}
|
|
}
|
|
|
|
// This test is not run by default and has to be explicitly selected to run.
|
|
TEST_CASE_METHOD(RequestFixture,
|
|
"WebRequest::Manual", "[.]")
|
|
{
|
|
InitManualRequest();
|
|
|
|
request.Start();
|
|
RunLoopWithTimeout();
|
|
|
|
INFO("Error: \"" << errorDescription << "\"");
|
|
CHECK( request.GetState() == wxWebRequest::State_Completed );
|
|
|
|
DumpResponse(request.GetResponse());
|
|
}
|
|
|
|
// A sync version of the pseudo-test above.
|
|
TEST_CASE_METHOD(SyncRequestFixture,
|
|
"WebRequest::Sync::Manual", "[.]")
|
|
{
|
|
InitManualRequest();
|
|
|
|
if ( !Execute() )
|
|
{
|
|
FAIL_CHECK("Error: \"" << error << "\"");
|
|
}
|
|
|
|
DumpResponse(request.GetResponse());
|
|
}
|
|
|
|
using wxWebRequestHeaderMap = std::unordered_map<wxString, wxString>;
|
|
|
|
namespace wxPrivate
|
|
{
|
|
WXDLLIMPEXP_NET wxString
|
|
SplitParameters(const wxString& s, wxWebRequestHeaderMap& parameters);
|
|
}
|
|
|
|
TEST_CASE("WebRequestUtils", "[net][webrequest]")
|
|
{
|
|
wxString value;
|
|
wxWebRequestHeaderMap params;
|
|
|
|
wxString header = "multipart/mixed; boundary=\"MIME_boundary_01234567\"";
|
|
|
|
value = wxPrivate::SplitParameters(header, params);
|
|
CHECK( value == "multipart/mixed" );
|
|
CHECK( params.size() == 1 );
|
|
CHECK( params["boundary"] == "MIME_boundary_01234567" );
|
|
}
|
|
|
|
// This is not a real test, run it to see the version of the library used.
|
|
TEST_CASE("WebRequest::Version", "[.]")
|
|
{
|
|
const auto& info = wxWebSession::GetDefault().GetLibraryVersionInfo();
|
|
WARN("Using " << info.GetName() << " backend (" << info.ToString() << ")");
|
|
}
|
|
|
|
#endif // wxUSE_WEBREQUEST
|