wxWidgets/tests/net/webrequest.cpp
Vadim Zeitlin b3b5c8a670 Fix wxWebResponse::GetHeader() and GetURL() in wxMSW
These functions returned strings of wrong size, with some junk after the
end of the actual string, due to a confusion between the size of the
buffer in bytes used by the WinHTTP functions and the length of the
buffer in (wide) characters used by wxWCharBuffer.

Fix this and add a unit test checking that the expected header is
returned.

See #22549.

Closes #22181.
2022-06-20 00:41:13 +02:00

563 lines
16 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/wfstream.h"
// 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";
class RequestFixture : public wxTimer
{
public:
RequestFixture()
{
expectedFileSize = 0;
dataSize = 0;
stateFromEvent = wxWebRequest::State_Idle;
statusFromEvent = 0;
}
// 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) )
{
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;
}
return baseURL != "0";
}
void Create(const wxString& subURL)
{
CreateAbs(baseURL + subURL);
}
void CreateAbs(const wxString& url)
{
request = wxWebSession::GetDefault().CreateRequest(this, url);
Bind(wxEVT_WEBREQUEST_STATE, &RequestFixture::OnRequestState, this);
Bind(wxEVT_WEBREQUEST_DATA, &RequestFixture::OnData, this);
}
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() wxOVERRIDE
{
WARN("Exiting loop on timeout");
loop.Exit();
}
void OnData(wxWebRequestEvent& evt)
{
// Count all bytes recieved 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)));
}
wxString baseURL;
wxEventLoop loop;
wxWebRequest request;
wxWebRequest::State stateFromEvent;
int statusFromEvent;
wxString responseStringFromEvent;
wxInt64 expectedFileSize;
wxInt64 dataSize;
wxString errorDescription;
};
TEST_CASE_METHOD(RequestFixture,
"WebRequest::Get::Bytes", "[net][webrequest][get]")
{
if ( !InitBaseURL() )
return;
Create("/bytes/65536");
Run();
CHECK( request.GetResponse().GetContentLength() == 65536 );
CHECK( request.GetBytesExpectedToReceive() == 65536 );
CHECK( request.GetBytesReceived() == 65536 );
}
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::Param", "[net][webrequest][get]")
{
if ( !InitBaseURL() )
return;
Create("/get?pi=3.14159265358979323");
Run();
// 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.
const wxString& response = request.GetResponse().AsString();
INFO("Response: " << response);
const char* expectedKey = "\"pi\":";
size_t pos = response.find(expectedKey);
REQUIRE( pos != wxString::npos );
pos += strlen(expectedKey);
// There may, or not, be a space after it.
while ( wxIsspace(response[pos]) )
pos++;
const char* expectedValue = "\"3.14159265358979323\"";
REQUIRE( response.compare(pos, strlen(expectedValue), expectedValue) == 0 );
}
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;
CreateAbs("http://127.0.0.1:51234");
Run(wxWebRequest::State_Failed, 0);
}
TEST_CASE_METHOD(RequestFixture,
"WebRequest::SSL::Error", "[net][webrequest][error]")
{
wxString selfsignedURL;
if ( !wxGetEnv("WX_TEST_WEBREQUEST_URL_SELF_SIGNED", &selfsignedURL) )
{
WARN("Skipping because WX_TEST_WEBREQUEST_URL_SELF_SIGNED is not set.");
return;
}
if (!InitBaseURL())
return;
CreateAbs(selfsignedURL);
Run(wxWebRequest::State_Failed, 0);
}
TEST_CASE_METHOD(RequestFixture,
"WebRequest::SSL::Ignore", "[net][webrequest]")
{
wxString selfsignedURL;
if ( !wxGetEnv("WX_TEST_WEBREQUEST_URL_SELF_SIGNED", &selfsignedURL) )
{
WARN("Skipping because WX_TEST_WEBREQUEST_URL_SELF_SIGNED is not set.");
return;
}
if (!InitBaseURL())
return;
// For some reason this test sporadically fails under AppVeyor, so don't
// run it there.
#ifdef __WINDOWS__
if ( IsAutomaticTest() )
{
WARN("Skipping DisablePeerVerify() test known to sporadically fail.");
return;
}
#endif // __WINDOWS__
CreateAbs(selfsignedURL);
request.DisablePeerVerify();
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");
wxScopedPtr<wxInputStream> is(new wxFileInputStream("horse.png"));
REQUIRE( is->IsOk() );
request.SetData(is.release(), "image/png");
request.SetMethod("PUT");
Run();
}
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.GetResponse().GetStatus() == 200 );
CHECK( request.GetState() == wxWebRequest::State_Completed );
}
SECTION("Bad password")
{
UseCredentials("wxtest", "foobar");
RunLoopWithTimeout();
CHECK( request.GetResponse().GetStatus() == 401 );
CHECK( request.GetState() == wxWebRequest::State_Unauthorized );
}
}
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.GetResponse().GetStatus() == 200 );
CHECK( request.GetState() == wxWebRequest::State_Completed );
}
SECTION("Bad password")
{
UseCredentials("foo", "bar");
RunLoopWithTimeout();
CHECK( request.GetResponse().GetStatus() == 401 );
CHECK( request.GetState() == wxWebRequest::State_Unauthorized );
}
}
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!" );
}
// This test is not run by default and has to be explicitly selected to run.
TEST_CASE_METHOD(RequestFixture,
"WebRequest::Manual", "[.]")
{
// 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");
}
CreateAbs(url);
request.Start();
RunLoopWithTimeout();
CHECK( request.GetState() == wxWebRequest::State_Completed );
wxWebResponse response = request.GetResponse();
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));
}
}
WX_DECLARE_STRING_HASH_MAP(wxString, wxWebRequestHeaderMap);
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" );
}
#endif // wxUSE_WEBREQUEST