From 592f8ad2f8dd790b8eba53a1e38e903d70d0fdc7 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Mon, 15 Jun 2026 18:49:35 -0700 Subject: [PATCH 01/13] Migrate HttpOriginPolicyIntegrationTest into OriginPolicyHttpFilterTest --- .../OriginPolicyHttpFilterTest.cpp | 809 ++++++++++++++++++ 1 file changed, 809 insertions(+) diff --git a/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp b/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp index dc0f7ee2964..3e3bc649279 100644 --- a/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp +++ b/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp @@ -3,7 +3,11 @@ #include +#include +#include +#include #include +#include #include #include "WinRTNetworkingMocks.h" @@ -12,16 +16,44 @@ // Windows API #include +#include +#include +#include #include +// Standard Library +#include +#include +#include +#include + using namespace Microsoft::VisualStudio::CppUnitTestFramework; using namespace winrt::Windows::Web::Http; +using Microsoft::React::Networking::OriginPolicy; using Microsoft::React::Networking::OriginPolicyHttpFilter; +using Microsoft::React::Networking::RedirectHttpFilter; using Microsoft::React::Networking::RequestArgs; using Microsoft::React::Networking::ResponseOperation; +using std::string; using std::wstring; using winrt::Windows::Foundation::Uri; +using winrt::Windows::Web::Http::Filters::IHttpFilter; +using winrt::Windows::Web::Http::Headers::HttpMediaTypeHeaderValue; + +namespace { +constexpr char s_crossOriginUrl[]{"http://example.rnw"}; // Narrow form, for runtime options. +constexpr wchar_t s_crossOriginUrlW[]{L"http://example.rnw"}; // Wide form, for header values. + +// Common header field names (mirror the boost::beast::http::field values used by the original integration test). +constexpr wchar_t s_accessControlAllowCredentials[]{L"Access-Control-Allow-Credentials"}; +constexpr wchar_t s_accessControlAllowHeaders[]{L"Access-Control-Allow-Headers"}; +constexpr wchar_t s_accessControlAllowMethods[]{L"Access-Control-Allow-Methods"}; +constexpr wchar_t s_accessControlAllowOrigin[]{L"Access-Control-Allow-Origin"}; +constexpr wchar_t s_accessControlRequestHeaders[]{L"Access-Control-Request-Headers"}; +constexpr wchar_t s_location[]{L"Location"}; +constexpr wchar_t s_server[]{L"Server"}; +} // namespace namespace Microsoft::React::Test { @@ -364,6 +396,783 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { Assert::AreEqual(expected[i], OriginPolicyHttpFilter::GetOrigin(Uri{urls[i]}).c_str()); } } + +#pragma region Former Integration Tests + // clang-format off + + // + // The tests below were migrated from the HttpOriginPolicyIntegrationTest, which exercised the + // origin policy against an in-process HTTP server (Microsoft::React::Test::HttpServer). Here the + // network is replaced with a MockHttpBaseFilter terminating the same WinRT filter chain assembled + // by IHttpResource::Make() (OriginPolicyHttpFilter -> RedirectHttpFilter -> inner filter). + // + + static uint16_t s_port; + + /// + /// Describes a mocked HTTP endpoint. A single mock filter routes requests to the matching endpoint + /// by port and replies with the configured preflight (OPTIONS) or main response. + /// + struct ServerParams + { + uint16_t Port; + string Url; // Narrow form, used for runtime options and request URLs. + wstring WideUrl; // Wide form, used as Location / Access-Control-Allow-Origin header values. + + HttpStatusCode PreflightStatus{HttpStatusCode::Ok}; + std::map PreflightHeaders; + + HttpStatusCode ResponseStatus{HttpStatusCode::Ok}; + std::map ResponseHeaders; + wstring ResponseContent{L"RESPONSE_CONTENT"}; + + ServerParams(uint16_t port) + : Port{port} + , Url{"http://localhost:" + std::to_string(port)} + , WideUrl{L"http://localhost:" + std::to_wstring(port)} + { + PreflightHeaders[s_accessControlAllowMethods] = L"GET, POST, DELETE, PATCH"; + } + }; + + struct ClientParams + { + wstring Method; + std::vector> Headers; + bool WithCredentials{false}; + + ClientParams(wstring&& method, std::vector>&& headers) + : Method{std::move(method)} + , Headers{std::move(headers)} + { + } + }; + + struct RequestResult + { + winrt::hstring Error; + HttpStatusCode StatusCode{HttpStatusCode::None}; + winrt::hstring Content; + bool HasResponse{false}; + }; + + /// + /// Builds the same WinRT HTTP filter chain that IHttpResource::Make() assembles + /// (OriginPolicyHttpFilter -> RedirectHttpFilter -> inner filter), but terminates it with the + /// supplied mock filter instead of the live network stack. Reads the same runtime options. + /// + HttpClient MakeMockedClient(IHttpFilter const& innerMock) + { + auto redirFilter = winrt::make(IHttpFilter{innerMock}, IHttpFilter{innerMock}, winrt::hstring{}); + + if (static_cast(GetRuntimeOptionInt("Http.OriginPolicy")) == OriginPolicy::None) + { + return HttpClient{redirFilter}; + } + + auto globalOrigin = GetRuntimeOptionString("Http.GlobalOrigin"); + auto opFilter = winrt::make(std::move(globalOrigin), redirFilter); + redirFilter.as()->SetRedirectSource( + opFilter.as()); + + return HttpClient{opFilter}; + } + + /// + /// Builds an HTTP request the way WinRTHttpResource::CreateRequest does for the integration test's + /// inputs: empty string body, Content-Type attached to the request content, all other headers + /// attached to the request itself, and a RequestArgs property the OriginPolicyHttpFilter relies on. + /// + HttpRequestMessage BuildRequest(ClientParams const& client, string const& url) + { + HttpRequestMessage request{HttpMethod{winrt::hstring{client.Method}}, Uri{winrt::to_hstring(url)}}; + + auto reqArgs = winrt::make(); + reqArgs.as()->WithCredentials = client.WithCredentials; + request.Properties().Insert(L"RequestArgs", reqArgs); + + HttpStringContent content{winrt::hstring{L""}}; + for (auto const& header : client.Headers) + { + if (_wcsicmp(header.first.c_str(), L"Content-Type") == 0) + { + content.Headers().ContentType(HttpMediaTypeHeaderValue::Parse(header.second)); + } + else + { + // Use unvalidated append so that forbidden headers (e.g. Proxy-Authorization, Host) reach the + // OriginPolicyHttpFilter, which is the component under test responsible for rejecting them. + request.Headers().TryAppendWithoutValidation(header.first, header.second); + } + } + request.Content(content); + + return request; + } + + RequestResult RunRequest(std::vector const& servers, ClientParams const& client, string const& requestUrl) + { + auto mockFilter = winrt::make(); + mockFilter.as()->Mocks.SendRequestAsync = + [&servers](HttpRequestMessage const& request) -> ResponseOperation + { + HttpResponseMessage response{}; + + ServerParams const* match = nullptr; + for (auto* server : servers) + { + if (request.RequestUri().Port() == server->Port) + { + match = server; + break; + } + } + + if (!match) + { + response.StatusCode(HttpStatusCode::NotFound); + response.Content(HttpStringContent{winrt::hstring{L""}}); + co_return response; + } + + bool isPreflight = request.Method().ToString() == L"OPTIONS"; + response.StatusCode(isPreflight ? match->PreflightStatus : match->ResponseStatus); + + auto const& headers = isPreflight ? match->PreflightHeaders : match->ResponseHeaders; + for (auto const& header : headers) + { + if (_wcsicmp(header.first.c_str(), s_location) == 0) + { + response.Headers().Location(Uri{header.second}); + } + else + { + response.Headers().Insert(header.first, header.second); + } + } + + response.Content(HttpStringContent{isPreflight ? winrt::hstring{L""} : winrt::hstring{match->ResponseContent}}); + + co_return response; + }; + + auto client_ = MakeMockedClient(mockFilter); + + RequestResult result; + try + { + auto request = BuildRequest(client, requestUrl); + auto sendOp = client_.SendRequestAsync(request); + sendOp.get(); + + auto response = sendOp.GetResults(); + result.HasResponse = static_cast(response); + if (response) + { + result.StatusCode = response.StatusCode(); + if (response.Content()) + { + auto contentOp = response.Content().ReadAsStringAsync(); + contentOp.get(); + result.Content = contentOp.GetResults(); + } + } + } + catch (winrt::hresult_error const& e) + { + result.Error = e.message(); + } + + return result; + } + + void AssertSucceeds(RequestResult const& result, HttpStatusCode expectedStatus) + { + Assert::AreEqual(L"", result.Error.c_str()); + Assert::AreEqual(static_cast(expectedStatus), static_cast(result.StatusCode)); + Assert::AreEqual(L"RESPONSE_CONTENT", result.Content.c_str()); + } + + void AssertFails(RequestResult const& result) + { + Assert::AreNotEqual(L"", result.Error.c_str()); + } + + void TestOriginPolicy(ServerParams& serverArgs, ClientParams& clientArgs, bool shouldSucceed) + { + auto result = RunRequest({&serverArgs}, clientArgs, serverArgs.Url); + + if (shouldSucceed) + AssertSucceeds(result, serverArgs.ResponseStatus); + else + AssertFails(result); + } + + void TestOriginPolicyWithRedirect( + ServerParams& server1Args, + ServerParams& server2Args, + ClientParams& clientArgs, + bool shouldSucceed) + { + // We assume 2-server tests will always redirect so the final response will come from server 2. + auto result = RunRequest({&server1Args, &server2Args}, clientArgs, server1Args.Url); + + if (shouldSucceed) + AssertSucceeds(result, server2Args.ResponseStatus); + else + AssertFails(result); + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + // Clear any runtime options that may be used by tests in this class. + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); + SetRuntimeOptionString("Http.GlobalOrigin", {}); + SetRuntimeOptionBool("Http.OmitCredentials", false); + + // Keep parity with the original integration test by using a different origin per test. + s_port++; + } + + BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsForbiddenMethodSucceeds) + // CONNECT, TRACE, and TRACK methods not supported by Windows.Web.Http + // https://docs.microsoft.com/en-us/uwp/api/windows.web.http.httpmethod?view=winrt-22621#properties + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(NoCorsForbiddenMethodSucceeds) + { + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); + + ServerParams serverArgs(s_port); + serverArgs.ResponseStatus = HttpStatusCode::Ok; + serverArgs.ResponseContent = L"GET_CONTENT"; + + ClientParams clientArgs(L"TRACE", {{L"ValidHeader", L"AnyValue"}}); + + auto result = RunRequest({&serverArgs}, clientArgs, serverArgs.Url); + + Assert::AreEqual(L"", result.Error.c_str()); + Assert::AreEqual(static_cast(HttpStatusCode::Ok), static_cast(result.StatusCode)); + Assert::AreEqual(L"GET_CONTENT", result.Content.c_str()); + } // NoCorsForbiddenMethodSucceeds + + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsForbiddenMethodFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsForbiddenMethodFails) + { + ServerParams serverArgs(s_port); + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = serverArgs.WideUrl; + + ClientParams clientArgs(L"CONNECT", {{L"Content-Type", L"text/plain"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, false /*shouldSucceed*/); + } // SimpleCorsForbiddenMethodFails + + BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginFetchRequestSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(NoCorsCrossOriginFetchRequestSucceeds) + { + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); + + ServerParams serverArgs(s_port); + serverArgs.ResponseStatus = HttpStatusCode::Ok; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"text/plain"}}); + + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + } // NoCorsCrossOriginFetchRequestSucceeds + + BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginPatchSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(NoCorsCrossOriginPatchSucceeds) + { + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); + + ServerParams serverArgs(s_port); + serverArgs.ResponseStatus = HttpStatusCode::Ok; + + ClientParams clientArgs(L"PATCH", {{L"Content-Type", L"text/plain"}}); + + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + } // NoCorsCrossOriginPatchSucceeds + + // Simple-Cors - Prevents the method from being anything other than HEAD, GET or POST, + // and the headers from being anything other than simple headers (CORS safe listed headers). + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsSameOriginSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsSameOriginSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.ResponseStatus = HttpStatusCode::Ok; + + ClientParams clientArgs(L"PATCH", {{L"Content-Type", L"text/plain"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + } // SimpleCorsSameOriginSucceeds + + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsCrossOriginFetchFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsCrossOriginFetchFails) + { + ServerParams serverArgs(s_port); + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"text/html"}}); // text/html is a non-simple value + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, false /*shouldSucceed*/); + } // SimpleCorsCrossOriginFetchFails + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginRequestSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginRequestSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.ResponseStatus = HttpStatusCode::Ok; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"text/plain"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + } // FullCorsSameOriginRequestSucceeds + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginAllowOriginWildcardSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginAllowOriginWildcardSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = L"*"; + serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; + serverArgs.ResponseStatus = HttpStatusCode::Accepted; + serverArgs.ResponseHeaders[s_accessControlAllowOrigin] = L"*"; + serverArgs.ResponseHeaders[s_accessControlAllowCredentials] = L"true"; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"text/plain"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + } // FullCorsCrossOriginAllowOriginWildcardSucceeds + + // With CORS, the server can decide what origins are permitted to read information from the client. + // Additionally, for non-simple requests, the client should preflight the request through HTTP OPTIONS. + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMatchingOriginSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMatchingOriginSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; + serverArgs.ResponseStatus = HttpStatusCode::Accepted; + serverArgs.ResponseHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + serverArgs.ResponseHeaders[s_accessControlAllowCredentials] = L"true"; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"text/plain"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + } // FullCorsCrossOriginMatchingOriginSucceeds + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginWithCredentialsFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginWithCredentialsFails) + { + ServerParams serverArgs(s_port); + serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + serverArgs.PreflightHeaders[s_accessControlAllowCredentials] = L"true"; + serverArgs.ResponseHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"application/text"}}); // application/text is a non-simple header + clientArgs.WithCredentials = true; + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionBool("Http.OmitCredentials", true); + + TestOriginPolicy(serverArgs, clientArgs, false /*shouldSucceed*/); + } // FullCorsCrossOriginWithCredentialsFails + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMissingCorsHeadersFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMissingCorsHeadersFails) + { + ServerParams serverArgs(s_port); + serverArgs.PreflightHeaders.erase(s_accessControlAllowMethods); + serverArgs.PreflightStatus = HttpStatusCode::NotImplemented; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"application/text"}}); // application/text is a non-simple header + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, false /*shouldSucceed*/); + } // FullCorsCrossOriginMissingCorsHeadersFails + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMismatchedCorsHeaderFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMismatchedCorsHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + serverArgs.ResponseStatus = HttpStatusCode::Accepted; + serverArgs.ResponseHeaders[s_accessControlAllowOrigin] = L"http://other.example.rnw"; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"application/text"}}); // application/text is a non-simple header + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, false /*shouldSucceed*/); + } // FullCorsCrossOriginMismatchedCorsHeaderFails + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) + { + ServerParams serverArgs(s_port); + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + serverArgs.PreflightHeaders[s_location] = L"http://any-host.extension"; + serverArgs.PreflightStatus = HttpStatusCode::MovedPermanently; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"application/text"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, false /*shouldSucceed*/); + } // FullCorsCrossOriginCheckFailsOnPreflightRedirectFails + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCorsCheckFailsOnResponseRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCorsCheckFailsOnResponseRedirectFails) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); + + // server1 allowed origin header includes the cross origin + serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + + // This is a CORS request to server1, but server1 redirects the request to server2 + serverArgs.ResponseStatus = HttpStatusCode::MovedPermanently; + serverArgs.ResponseHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + serverArgs.ResponseHeaders[s_location] = redirServerArgs.WideUrl; + serverArgs.ResponseHeaders[s_server] = L"BaseServer"; + + // Server2 does not set Access-Control-Allow-Origin for GET requests + redirServerArgs.ResponseStatus = HttpStatusCode::Accepted; + redirServerArgs.ResponseHeaders[s_server] = L"RedirectServer"; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"application/text"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, false /*shouldSucceed*/); + } // FullCorsCorsCheckFailsOnResponseRedirectFails + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToSameOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginToSameOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.PreflightHeaders[s_location] = serverArgs.WideUrl; + serverArgs.ResponseStatus = HttpStatusCode::Accepted; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"application/text"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + } // FullCorsSameOriginToSameOriginRedirectSucceeds + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToCrossOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginToCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); + + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = serverArgs.WideUrl; + serverArgs.ResponseStatus = HttpStatusCode::MovedPermanently; + serverArgs.ResponseHeaders[s_location] = redirServerArgs.WideUrl; + + redirServerArgs.ResponseStatus = HttpStatusCode::Accepted; + redirServerArgs.ResponseHeaders[s_accessControlAllowOrigin] = serverArgs.WideUrl; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"application/text"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, true /*shouldSucceed*/); + } // FullCorsSameOriginToCrossOriginRedirectSucceeds + + // Redirects a cross origin request to a cross origin request on the same server. + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToCrossOriginRedirectSucceeds) + // Seems to redirect to exact same resource. Implement second resource in same server. + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = serverArgs.WideUrl; + serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + serverArgs.ResponseStatus = HttpStatusCode::MovedPermanently; + serverArgs.ResponseHeaders[s_location] = serverArgs.WideUrl; + serverArgs.ResponseHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"application/text"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + } // FullCorsCrossOriginToCrossOriginRedirectSucceeds + + // The initial request gets redirected back to the original origin, + // but it will lack the Access-Control-Allow-Origin header. + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToOriginalOriginRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToOriginalOriginRedirectFails) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); + + serverArgs.ResponseStatus = HttpStatusCode::MovedPermanently; + serverArgs.ResponseHeaders[s_location] = redirServerArgs.WideUrl; + serverArgs.ResponseHeaders[s_accessControlAllowOrigin] = redirServerArgs.WideUrl; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"text/plain"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", redirServerArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, false /*shouldSucceed*/); + } // FullCorsCrossOriginToOriginalOriginRedirectFails + + // Redirects a cross origin request to server1 to a cross origin request to server2. + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); + + serverArgs.ResponseStatus = HttpStatusCode::MovedPermanently; + serverArgs.ResponseHeaders[s_location] = redirServerArgs.WideUrl; + serverArgs.ResponseHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + + redirServerArgs.ResponseStatus = HttpStatusCode::Accepted; + redirServerArgs.ResponseHeaders[s_accessControlAllowOrigin] = L"*"; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"text/plain"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, true /*shouldSucceed*/); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); + + serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + // server1 redirects the request to server2 + serverArgs.ResponseStatus = HttpStatusCode::MovedPermanently; + serverArgs.ResponseHeaders[s_location] = redirServerArgs.WideUrl; + serverArgs.ResponseHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + + // Since redirect tainted the origin, the server has to allow all origins for CORS to succeed + redirServerArgs.ResponseStatus = HttpStatusCode::Accepted; + redirServerArgs.ResponseHeaders[s_accessControlAllowOrigin] = L"*"; + + // PATCH is not a simple method, so preflight is required for server1 + ClientParams clientArgs(L"PATCH", {{L"Content-Type", L"text/plain"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, true /*shouldSucceed*/); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); + + serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + // server1 redirects the request to server2 + serverArgs.ResponseStatus = HttpStatusCode::MovedPermanently; + serverArgs.ResponseHeaders[s_location] = redirServerArgs.WideUrl; + serverArgs.ResponseHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + + // Since redirect tainted the origin, the server does not know what origin to allow through a single value. + // Even if the server successfully guessed the single value, it will still fail on the client side. + redirServerArgs.ResponseStatus = HttpStatusCode::Accepted; + redirServerArgs.ResponseHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + + // PATCH is not a simple method, so preflight is required for server1 + ClientParams clientArgs(L"PATCH", {{L"Content-Type", L"text/plain"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, false /*shouldSucceed*/); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCors304ForSimpleGetFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCors304ForSimpleGetFails) + { + ServerParams serverArgs(s_port); + serverArgs.ResponseStatus = HttpStatusCode::NotModified; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"text/plain"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, false /*shouldSucceed*/); + } // FullCors304ForSimpleGetFails + + TEST_METHOD(OfficeDev_OfficeJS_4144) + { + SetRuntimeOptionString("Http.GlobalOrigin", "http://orig.in"); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + // Regression test for https://github.com/OfficeDev/office-js/issues/4144. + // A header-less GET should be handled without raising an application error. + ServerParams serverArgs(s_port); + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = L"*"; + serverArgs.ResponseHeaders[s_accessControlAllowOrigin] = L"*"; + + ClientParams clientArgs(L"GET", {} /*headers*/); + + auto result = RunRequest({&serverArgs}, clientArgs, serverArgs.Url + "/officedev/office-js/issues/4144"); + + Assert::AreEqual(L"", result.Error.c_str()); + } // OfficeDev_OfficeJS_4144 + + TEST_METHOD(FullCorsPreflightSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"ArbitraryHeader"; + serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"ArbitraryHeader"; + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + serverArgs.ResponseStatus = HttpStatusCode::Ok; + serverArgs.ResponseHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"text/plain"}, {L"ArbitraryHeader", L"AnyValue"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + } // FullCorsPreflightSucceeds + + // The current implementation omits the withCredentials flag from the request and always sets it to false. + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginWithCredentialsSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginWithCredentialsSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + serverArgs.PreflightHeaders[s_accessControlAllowCredentials] = L"true"; + serverArgs.ResponseStatus = HttpStatusCode::Accepted; + serverArgs.ResponseHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"application/text"}}); // application/text is a non-simple header + clientArgs.WithCredentials = true; + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + } // FullCorsCrossOriginWithCredentialsSucceeds + + // "Host" is one of the forbidden headers for fetch + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsRequestWithHostHeaderFails) + // "Host" is not an accepted request header in WinRT. + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsRequestWithHostHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; + serverArgs.ResponseStatus = HttpStatusCode::Accepted; + + ClientParams clientArgs(L"GET", {{L"Content-Type", L"application/text"}, {L"Host", L"http://sub.example.rnw"}}); + + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, false /*shouldSucceed*/); + + Assert::Fail(L"FIX!!! Passes for the wrong reason. Error: 0x80070057 : 'Invalid HTTP headers.'"); + } // FullCorsRequestWithHostHeaderFails + + BEGIN_TEST_METHOD_ATTRIBUTE(RequestWithProxyAuthorizationHeaderFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(RequestWithProxyAuthorizationHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; + serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; + serverArgs.ResponseStatus = HttpStatusCode::Accepted; + + ClientParams clientArgs( + L"GET", {{L"Content-Type", L"application/text"}, {L"Proxy-Authorization", L"Basic Zm9vOmJhcg=="}}); + + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, false /*shouldSucceed*/); + } // RequestWithProxyAuthorizationHeaderFails + + // clang-format on }; +uint16_t OriginPolicyHttpFilterTest::s_port = 7777; +#pragma endregion Former Integration Tests + } // namespace Microsoft::React::Test From 0b3e5bdb34a67a802ecb53889afe9c063dafd2f6 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Mon, 15 Jun 2026 19:29:18 -0700 Subject: [PATCH 02/13] Drop port numbers --- .../OriginPolicyHttpFilterTest.cpp | 98 +++++++++---------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp b/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp index 3e3bc649279..23f8094f4ea 100644 --- a/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp +++ b/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp @@ -42,6 +42,11 @@ using winrt::Windows::Web::Http::Filters::IHttpFilter; using winrt::Windows::Web::Http::Headers::HttpMediaTypeHeaderValue; namespace { +// Clearly fake endpoint host names. These never reach the network (all requests are served by a +// mock filter), so they intentionally avoid "localhost" to prevent confusion with a real service. +constexpr wchar_t s_mockServerHost[]{L"mockserver.rnw"}; // Primary endpoint. +constexpr wchar_t s_redirectServerHost[]{L"redirserver.rnw"}; // Secondary endpoint (redirect target). + constexpr char s_crossOriginUrl[]{"http://example.rnw"}; // Narrow form, for runtime options. constexpr wchar_t s_crossOriginUrlW[]{L"http://example.rnw"}; // Wide form, for header values. @@ -407,17 +412,15 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { // by IHttpResource::Make() (OriginPolicyHttpFilter -> RedirectHttpFilter -> inner filter). // - static uint16_t s_port; - /// /// Describes a mocked HTTP endpoint. A single mock filter routes requests to the matching endpoint - /// by port and replies with the configured preflight (OPTIONS) or main response. + /// by host and replies with the configured preflight (OPTIONS) or main response. /// struct ServerParams { - uint16_t Port; - string Url; // Narrow form, used for runtime options and request URLs. - wstring WideUrl; // Wide form, used as Location / Access-Control-Allow-Origin header values. + wstring Host; // Host name used to route requests to this endpoint (e.g. L"mockserver.rnw"). + wstring WideUrl; // Wide URL form, used as Location / Access-Control-Allow-Origin header values. + string Url; // Narrow URL form, used for runtime options and request URLs. HttpStatusCode PreflightStatus{HttpStatusCode::Ok}; std::map PreflightHeaders; @@ -426,10 +429,10 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { std::map ResponseHeaders; wstring ResponseContent{L"RESPONSE_CONTENT"}; - ServerParams(uint16_t port) - : Port{port} - , Url{"http://localhost:" + std::to_string(port)} - , WideUrl{L"http://localhost:" + std::to_wstring(port)} + ServerParams(wstring host) + : Host{std::move(host)} + , WideUrl{L"http://" + Host} + , Url{winrt::to_string(WideUrl)} { PreflightHeaders[s_accessControlAllowMethods] = L"GET, POST, DELETE, PATCH"; } @@ -521,7 +524,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { ServerParams const* match = nullptr; for (auto* server : servers) { - if (request.RequestUri().Port() == server->Port) + if (request.RequestUri().Host() == server->Host) { match = server; break; @@ -629,9 +632,6 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); SetRuntimeOptionString("Http.GlobalOrigin", {}); SetRuntimeOptionBool("Http.OmitCredentials", false); - - // Keep parity with the original integration test by using a different origin per test. - s_port++; } BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsForbiddenMethodSucceeds) @@ -643,7 +643,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { { SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.ResponseStatus = HttpStatusCode::Ok; serverArgs.ResponseContent = L"GET_CONTENT"; @@ -660,7 +660,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(SimpleCorsForbiddenMethodFails) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = serverArgs.WideUrl; ClientParams clientArgs(L"CONNECT", {{L"Content-Type", L"text/plain"}}); @@ -678,7 +678,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.ResponseStatus = HttpStatusCode::Ok; ClientParams clientArgs(L"GET", {{L"Content-Type", L"text/plain"}}); @@ -693,7 +693,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.ResponseStatus = HttpStatusCode::Ok; ClientParams clientArgs(L"PATCH", {{L"Content-Type", L"text/plain"}}); @@ -707,7 +707,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(SimpleCorsSameOriginSucceeds) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.ResponseStatus = HttpStatusCode::Ok; ClientParams clientArgs(L"PATCH", {{L"Content-Type", L"text/plain"}}); @@ -722,7 +722,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(SimpleCorsCrossOriginFetchFails) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); ClientParams clientArgs(L"GET", {{L"Content-Type", L"text/html"}}); // text/html is a non-simple value @@ -736,7 +736,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsSameOriginRequestSucceeds) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.ResponseStatus = HttpStatusCode::Ok; ClientParams clientArgs(L"GET", {{L"Content-Type", L"text/plain"}}); @@ -751,7 +751,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsCrossOriginAllowOriginWildcardSucceeds) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = L"*"; serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; @@ -773,7 +773,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsCrossOriginMatchingOriginSucceeds) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; @@ -793,7 +793,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsCrossOriginWithCredentialsFails) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; @@ -814,7 +814,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsCrossOriginMissingCorsHeadersFails) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.PreflightHeaders.erase(s_accessControlAllowMethods); serverArgs.PreflightStatus = HttpStatusCode::NotImplemented; @@ -830,7 +830,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsCrossOriginMismatchedCorsHeaderFails) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; @@ -850,7 +850,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; serverArgs.PreflightHeaders[s_location] = L"http://any-host.extension"; serverArgs.PreflightStatus = HttpStatusCode::MovedPermanently; @@ -867,8 +867,8 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsCorsCheckFailsOnResponseRedirectFails) { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + ServerParams serverArgs(s_mockServerHost); + ServerParams redirServerArgs(s_redirectServerHost); // server1 allowed origin header includes the cross origin serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; @@ -897,7 +897,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsSameOriginToSameOriginRedirectSucceeds) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.PreflightHeaders[s_location] = serverArgs.WideUrl; serverArgs.ResponseStatus = HttpStatusCode::Accepted; @@ -913,8 +913,8 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsSameOriginToCrossOriginRedirectSucceeds) { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + ServerParams serverArgs(s_mockServerHost); + ServerParams redirServerArgs(s_redirectServerHost); serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = serverArgs.WideUrl; serverArgs.ResponseStatus = HttpStatusCode::MovedPermanently; @@ -938,7 +938,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsCrossOriginToCrossOriginRedirectSucceeds) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = serverArgs.WideUrl; serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; @@ -962,8 +962,8 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsCrossOriginToOriginalOriginRedirectFails) { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + ServerParams serverArgs(s_mockServerHost); + ServerParams redirServerArgs(s_redirectServerHost); serverArgs.ResponseStatus = HttpStatusCode::MovedPermanently; serverArgs.ResponseHeaders[s_location] = redirServerArgs.WideUrl; @@ -982,8 +982,8 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + ServerParams serverArgs(s_mockServerHost); + ServerParams redirServerArgs(s_redirectServerHost); serverArgs.ResponseStatus = HttpStatusCode::MovedPermanently; serverArgs.ResponseHeaders[s_location] = redirServerArgs.WideUrl; @@ -1004,8 +1004,8 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + ServerParams serverArgs(s_mockServerHost); + ServerParams redirServerArgs(s_redirectServerHost); serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; @@ -1032,8 +1032,8 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + ServerParams serverArgs(s_mockServerHost); + ServerParams redirServerArgs(s_redirectServerHost); serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; @@ -1061,7 +1061,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCors304ForSimpleGetFails) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.ResponseStatus = HttpStatusCode::NotModified; ClientParams clientArgs(L"GET", {{L"Content-Type", L"text/plain"}}); @@ -1074,12 +1074,12 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { TEST_METHOD(OfficeDev_OfficeJS_4144) { - SetRuntimeOptionString("Http.GlobalOrigin", "http://orig.in"); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); // Regression test for https://github.com/OfficeDev/office-js/issues/4144. // A header-less GET should be handled without raising an application error. - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = L"*"; serverArgs.ResponseHeaders[s_accessControlAllowOrigin] = L"*"; @@ -1092,7 +1092,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { TEST_METHOD(FullCorsPreflightSucceeds) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"ArbitraryHeader"; serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"ArbitraryHeader"; serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; @@ -1112,7 +1112,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsCrossOriginWithCredentialsSucceeds) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; serverArgs.PreflightHeaders[s_accessControlAllowOrigin] = s_crossOriginUrlW; @@ -1136,7 +1136,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(FullCorsRequestWithHostHeaderFails) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; serverArgs.ResponseStatus = HttpStatusCode::Accepted; @@ -1155,7 +1155,7 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(RequestWithProxyAuthorizationHeaderFails) { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_mockServerHost); serverArgs.PreflightHeaders[s_accessControlRequestHeaders] = L"Content-Type"; serverArgs.PreflightHeaders[s_accessControlAllowHeaders] = L"Content-Type"; serverArgs.ResponseStatus = HttpStatusCode::Accepted; @@ -1171,8 +1171,6 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { // clang-format on }; - -uint16_t OriginPolicyHttpFilterTest::s_port = 7777; #pragma endregion Former Integration Tests } // namespace Microsoft::React::Test From 5318943247e6a31eb8868ebe96f36cfd5dd8024d Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Mon, 15 Jun 2026 23:35:36 -0700 Subject: [PATCH 03/13] Remove HttpOriginPolicyIntegrationTest --- .../HttpOriginPolicyIntegrationTest.cpp | 869 ------------------ ...t.Windows.Desktop.IntegrationTests.vcxproj | 2 - ...s.Desktop.IntegrationTests.vcxproj.filters | 6 +- 3 files changed, 3 insertions(+), 874 deletions(-) delete mode 100644 vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp diff --git a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp deleted file mode 100644 index c6c16b2644b..00000000000 --- a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp +++ /dev/null @@ -1,869 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -#include - -#include -#include -#include - -// Standard Library -#include - -#include "HttpServer.h" - -using namespace Microsoft::VisualStudio::CppUnitTestFramework; - -namespace http = boost::beast::http; - -using Microsoft::React::Networking::IHttpResource; -using Microsoft::React::Networking::OriginPolicy; -using std::make_shared; -using std::promise; -using std::string; - -namespace { -constexpr char s_serverHost[]{"http://localhost"}; -constexpr char s_crossOriginUrl[]{"http://example.rnw"}; -} // namespace - -// clang-format off -namespace Microsoft::React::Test { - -TEST_CLASS(HttpOriginPolicyIntegrationTest) -{ - static constexpr bool s_shouldSucceed{true}; - static constexpr bool s_shouldFail{false}; - - static uint16_t s_port; - - struct ServerParams - { - uint16_t Port; - string Url; - EmptyResponse Preflight; - StringResponse Response; - - ServerParams( - uint16_t port) noexcept - : Port{port} - , Url{s_serverHost + string{":"} + std::to_string(port)} - { - Preflight.set(http::field::access_control_allow_methods, "GET, POST, DELETE, PATCH"); - - Response.result(http::status::unknown); - Response.body() = "RESPONSE_CONTENT"; - } - }; - - struct ClientParams - { - promise ContentPromise; - string ErrorMessage; - IHttpResource::Response Response; - string ResponseContent; - http::verb Method; - IHttpResource::Headers RequestHeaders; - bool WithCredentials{false}; - - ClientParams(http::verb method, IHttpResource::Headers&& headers) - : Method{ method } - , RequestHeaders{ std::move(headers) } - { - } - }; - - std::shared_ptr CreateServer(ServerParams& serverArgs, ClientParams& clientArgs) noexcept - { - auto server = make_shared(serverArgs.Port); - server->Callbacks().OnOptions = [&serverArgs](const DynamicRequest& request) -> ResponseWrapper - { - return { std::move(serverArgs.Preflight) }; - }; - - auto reqHandler = [&serverArgs](const DynamicRequest& request) -> ResponseWrapper - { - // Don't use move constructor in case of multiple requests - return { serverArgs.Response }; - }; - - switch (clientArgs.Method) - { - case http::verb::get: - server->Callbacks().OnGet = reqHandler; - break; - - case http::verb::post: - server->Callbacks().OnPost = reqHandler; - break; - - case http::verb::patch: - server->Callbacks().OnPatch = reqHandler; - break; - - case http::verb::trace: - server->Callbacks().OnTrace = reqHandler; - break; - - case http::verb::connect: - server->Callbacks().OnConnect = reqHandler; - break; - - case http::verb::options: - default: - Assert::Fail(L"Unsupported request method"); - } - - return server; - } - - void TestOriginPolicyWithRedirect(ServerParams& server1Args, ServerParams& server2Args, ClientParams& clientArgs, bool shouldSucceed) - { - auto server1 = CreateServer(server1Args, clientArgs); - auto server2 = CreateServer(server2Args, clientArgs); - - server1->Start(); - server2->Start(); - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& response) - { - clientArgs.Response = std::move(response); - }); - resource->SetOnData([&clientArgs](int64_t, string&& content) - { - clientArgs.ResponseContent = std::move(content); - clientArgs.ContentPromise.set_value(); - }); - resource->SetOnError([&clientArgs](int64_t, string&& message, bool) - { - clientArgs.ErrorMessage = std::move(message); - clientArgs.ContentPromise.set_value(); - }); - - resource->SendRequest( - string{http::to_string(clientArgs.Method).data()}, - string{server1Args.Url}, - 0, /*requestId*/ - std::move(clientArgs.RequestHeaders), - { { "string", "" } }, /*data*/ - "text", - false, /*useIncrementalUpdates*/ - 0, /*timeout*/ - clientArgs.WithCredentials, /*withCredentials*/ - [](int64_t){} /*reactCallback*/ - ); - - clientArgs.ContentPromise.get_future().wait(); - - server2->Stop(); - server1->Stop(); - - if (shouldSucceed) - { - Assert::AreEqual({}, clientArgs.ErrorMessage); - //TODO: chose server? - // We assume 2-server tests will always redirect so the final response will come from server 2. - Assert::AreEqual(server2Args.Response.result_int(), static_cast(clientArgs.Response.StatusCode)); - Assert::AreEqual({"RESPONSE_CONTENT"}, clientArgs.ResponseContent); - } - else - { - Assert::AreNotEqual({}, clientArgs.ErrorMessage); - } - } - - void TestOriginPolicy(ServerParams& serverArgs, ClientParams& clientArgs, bool shouldSucceed) - { - auto server = CreateServer(serverArgs, clientArgs); - - server->Start(); - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& res) - { - clientArgs.Response = std::move(res); - }); - resource->SetOnData([&clientArgs](int64_t, string&& content) - { - clientArgs.ResponseContent = std::move(content); - clientArgs.ContentPromise.set_value(); - }); - resource->SetOnError([&clientArgs](int64_t, string&& message, bool) - { - clientArgs.ErrorMessage = std::move(message); - clientArgs.ContentPromise.set_value(); - }); - - resource->SendRequest( - string{http::to_string(clientArgs.Method).data()}, - string{serverArgs.Url}, - 0, /*requestId*/ - std::move(clientArgs.RequestHeaders), - { { "string", "" } }, /*data*/ - "text", - false, /*useIncrementalUpdates*/ - 0, /*timeout*/ - clientArgs.WithCredentials, /*withCredentials*/ - [](int64_t) {} /*reactCallback*/ - ); - - clientArgs.ContentPromise.get_future().wait(); - server->Stop(); - - if (shouldSucceed) - { - Assert::AreEqual({}, clientArgs.ErrorMessage); - Assert::AreEqual(serverArgs.Response.result_int(), static_cast(clientArgs.Response.StatusCode)); - Assert::AreEqual({"RESPONSE_CONTENT"}, clientArgs.ResponseContent); - } - else - { - Assert::AreNotEqual({}, clientArgs.ErrorMessage); - } - } - - TEST_METHOD_CLEANUP(MethodCleanup) - { - // Clear any runtime options that may be used by tests in this class. - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - SetRuntimeOptionString("Http.GlobalOrigin", {}); - SetRuntimeOptionBool("Http.OmitCredentials", false); - - // Bug in HttpServer does not correctly release TCP port between test methods. - // Using a different por per test for now. - s_port++; - } - - BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsForbiddenMethodSucceeds) - // CONNECT, TRACE, and TRACK methods not supported by Windows.Web.Http - // https://docs.microsoft.com/en-us/uwp/api/windows.web.http.httpmethod?view=winrt-22621#properties - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(NoCorsForbiddenMethodSucceeds) - { - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - - constexpr uint16_t port{ 5556 }; - constexpr char url[]{ "http://localhost:5556" }; - - string error; - string getContent; - IHttpResource::Response getResponse; - promise getDataPromise; - - auto server = make_shared(port); - server->Callbacks().OnOptions = [&url](const DynamicRequest& request) -> ResponseWrapper - { - EmptyResponse response; - response.result(http::status::accepted); - - response.set(http::field::access_control_allow_credentials, "false"); - response.set(http::field::access_control_allow_headers, "ValidHeader"); - response.set(http::field::access_control_allow_methods, "GET, POST, DELETE, PATCH"); - response.set(http::field::access_control_allow_origin, url); - - return { std::move(response) }; - }; - server->Callbacks().OnTrace = [](const DynamicRequest& request) -> ResponseWrapper - { - StringResponse response; - response.result(http::status::ok); - response.body() = "GET_CONTENT"; - - return { std::move(response) }; - }; - server->Start(); - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&getResponse](int64_t, IHttpResource::Response&& res) - { - getResponse = std::move(res); - }); - resource->SetOnData([&getDataPromise, &getContent](int64_t, string&& content) - { - getContent = std::move(content); - getDataPromise.set_value(); - }); - resource->SetOnError([&server, &error, &getDataPromise](int64_t, string&& message, bool) - { - error = std::move(message); - getDataPromise.set_value(); - }); - - resource->SendRequest( - "TRACE", - url, - 0, /*requestId*/ - { - {"ValidHeader", "AnyValue"} - }, - { { "string", "" } }, /*data*/ - "text", - false /*useIncrementalUpdates*/, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {} /*callback*/ - ); - - getDataPromise.get_future().wait(); - server->Stop(); - - Assert::AreEqual({}, error); - Assert::AreEqual(200, static_cast(getResponse.StatusCode)); - Assert::AreEqual({ "GET_CONTENT" }, getContent); - }// NoCorsForbiddenMethodSucceeds - - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsForbiddenMethodFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsForbiddenMethodFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - - ClientParams clientArgs(http::verb::connect, {{"Content-Type", "text/plain"}}); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// SimpleCorsForbiddenMethodFails - - //NoCors_ForbiddenMethodConnect_Failed - - BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginFetchRequestSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(NoCorsCrossOriginFetchRequestSucceeds) - { - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); - - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// NoCorsCrossOriginFetchRequestSucceeds - - BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginPatchSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(NoCorsCrossOriginPatchSucceeds) - { - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); - - ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); - - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// NoCorsCrossOriginPatchSucceeds - - // Simple-Cors — Prevents the method from being anything other than HEAD, GET or POST, - // and the headers from being anything other than simple headers (CORS safe listed headers). - // If any ServiceWorkers intercept these requests, they may not add or override any headers except for those that are simple headers. - // In addition, JavaScript may not access any properties of the resulting Response. - // This ensures that ServiceWorkers do not affect the semantics of the Web and prevents security and privacy issues arising from leaking data across domains. - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsSameOriginSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsSameOriginSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); - - ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// SimpleCorsSameOriginSucceeds - - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsCrossOriginFetchFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsCrossOriginFetchFails) - { - ServerParams serverArgs(s_port); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/html" }}); // text/html is a non-simple value - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// SimpleCorsCrossOriginFetchFails - - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginRequestSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginRequestSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header - - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsSameOriginRequestSucceeds - - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginAllowOriginWildcardSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginAllowOriginWildcardSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, "*"); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, "*"); - serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsCrossOriginAllowOriginWildcardSucceeds - - // With CORS, Cross-Origin Resource Sharing, the server can decide what origins are permitted to read information from the client. - // Additionally, for non-simple requests, client should preflight the request through the HTTP Options request, and only send the - // actual request after the server has responded that the desired headers are supported. - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMatchingOriginSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMatchingOriginSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsCrossOriginMatchingOriginSucceeds - - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginWithCredentialsFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginWithCredentialsFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header - clientArgs.WithCredentials = true; - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - SetRuntimeOptionBool("Http.OmitCredentials", true); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginWithCredentialsFails - - - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMissingCorsHeadersFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMissingCorsHeadersFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.erase(http::field::access_control_allow_methods); - serverArgs.Preflight.result(http::status::not_implemented); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginMissingCorsHeadersFails - - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMismatchedCorsHeaderFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMismatchedCorsHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, "http://other.example.rnw"); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginMismatchedCorsHeaderFails - - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::location, "http://any-host.extension"); - serverArgs.Preflight.result(http::status::moved_permanently); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginCheckFailsOnPreflightRedirectFails - - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCorsCheckFailsOnResponseRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCorsCheckFailsOnResponseRedirectFails) - { - ServerParams serverArgs(s_port); - - // server1 allowed origin header includes http://example.com - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - // This is a CORS request to server1, but server1 redirects the request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // Redir server's URL - serverArgs.Response.set(http::field::location, "http://localhost:6666"); - serverArgs.Response.set(http::field::server, "BaseServer"); - - // Server2 does not set Access-Control-Allow-Origin for GET requests - ServerParams redirServerArgs(++s_port); - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::server, "RedirectServer"); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - }// FullCorsCorsCheckFailsOnResponseRedirectFails - - - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToSameOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginToSameOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::location, serverArgs.Url); - serverArgs.Response.result(http::status::accepted); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - } // FullCorsSameOriginToSameOriginRedirectSucceeds - - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToCrossOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginToCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); - - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsSameOriginToCrossOriginRedirectSucceeds - - //TODO: Seems to redirect to exact same resource. Implement second resource in same server. - // Redirects a cross origin request to cross origin request on the same server - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToCrossOriginRedirectSucceeds) - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - //ServerParams redirServerArgs(++s_port); - - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, serverArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - //redirServerArgs.Response.result(http::status::accepted); - //redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, /*redirServerArgs, */clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToCrossOriginRedirectSucceeds - - // The initial request gets redirected back to the original origin, - // but it will lack the Access-Control-Allow-Origin header. - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToOriginalOriginRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToOriginalOriginRedirectFails) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); - - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, redirServerArgs.Url); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", redirServerArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - } // FullCorsCrossOriginToOriginalOriginRedirectFails - - // Redirects cross origin request to server1 to cross origin request to server2 - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); - - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds - - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); - - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // server1 redirects the GET request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - // Since redirect tainted the origin, the server has to allow all origins for CORS to succeed - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); - - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds - - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); - - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // server1 redirects the GET request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - // Since redirect tainted the origin, the server does not know what origin to allow through a single value. - // Even if server successfully guessed the single value, it will still fail on the client side. - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails - - BEGIN_TEST_METHOD_ATTRIBUTE(FullCors304ForSimpleGetFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCors304ForSimpleGetFails) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::not_modified); - - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - } // FullCors304ForSimpleGetFails - - TEST_METHOD(OfficeDev_OfficeJS_4144) - { - SetRuntimeOptionString("Http.GlobalOrigin", "http://orig.in"); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - ClientParams clientArgs(http::verb::get, {} /*headers*/); - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& res) - { - clientArgs.Response = std::move(res); - }); - resource->SetOnData([&clientArgs](int64_t, string&& content) - { - clientArgs.ResponseContent = std::move(content); - clientArgs.ContentPromise.set_value(); - }); - resource->SetOnError([&clientArgs](int64_t, string&& message, bool) - { - clientArgs.ErrorMessage = std::move(message); - clientArgs.ContentPromise.set_value(); - }); - - resource->SendRequest( - string { http::to_string(clientArgs.Method).data() }, - string { "http://localhost:5555/officedev/office-js/issues/4144"}, - 0, /*requestId*/ - std::move(clientArgs.RequestHeaders), - { { "string", "" } }, /*data*/ - "text", - false, /*useIncrementalUpdates*/ - 0, /*timeout*/ - clientArgs.WithCredentials, /*withCredentials*/ - [](int64_t) {} /*reactCallback*/ - ); - - clientArgs.ContentPromise.get_future().wait(); - - Assert::AreEqual({}, clientArgs.ErrorMessage); - } - - TEST_METHOD(FullCorsPreflightSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "ArbitraryHeader"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "ArbitraryHeader"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::ok); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - ClientParams clientArgs(http::verb::get, { {"Content-Type", "text/plain"}, {"ArbitraryHeader", "AnyValue"} }); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - }// FullCorsPreflightSucceeds - - // The current implementation omits withCredentials flag from request and always sets it to false - // Configure the responses for CORS request - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginWithCredentialsSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginWithCredentialsSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header - clientArgs.WithCredentials = true; - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - }// FullCorsCrossOriginWithCredentialsSucceeds - - // "Host" is one of the forbidden headers for fetch - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsRequestWithHostHeaderFails) - // "Host" is not an accepted request header in WinRT. - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsRequestWithHostHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }, { "Host", "http://sub.example.rnw" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - - Assert::Fail(L"FIX!!! Passes for the wrong reason. Error: 0x80070057 : 'Invalid HTTP headers.'"); - }// FullCorsRequestWithHostHeaderFails - - BEGIN_TEST_METHOD_ATTRIBUTE(RequestWithProxyAuthorizationHeaderFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(RequestWithProxyAuthorizationHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }, { "Proxy-Authorization", "Basic Zm9vOmJhcg==" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// RequestWithProxyAuthorizationHeaderFails -}; - -uint16_t HttpOriginPolicyIntegrationTest::s_port = 7777; - -}//namespace Microsoft::React::Test -// clang-format on diff --git a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj index 2395ccaf386..36aa5da6d54 100644 --- a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj +++ b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj @@ -88,7 +88,6 @@ %(AdditionalIncludeDirectories) true - $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) @@ -108,7 +107,6 @@ - diff --git a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters index 45c06fc788b..f7f55cfb3da 100644 --- a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters +++ b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters @@ -36,9 +36,6 @@ Source Files\Modules - - Integration Tests - Integration Tests @@ -96,4 +93,7 @@ Header Files\Modules + + + \ No newline at end of file From 555c666087fd3f410e7b1c00cd778f64500de91a Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Mon, 15 Jun 2026 23:37:03 -0700 Subject: [PATCH 04/13] Remove HttpOriginPolicyIntegrationTest --- .ado/build-template.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.ado/build-template.yml b/.ado/build-template.yml index f328ee6760c..f78b0d85af8 100644 --- a/.ado/build-template.yml +++ b/.ado/build-template.yml @@ -287,8 +287,7 @@ extends: (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::SendReceiveClose)& (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::SendConsecutive)& (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::SendReceiveLargeMessage)& - (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::SendReceiveSsl)& - (FullyQualifiedName!=Microsoft::React::Test::HttpOriginPolicyIntegrationTest) + (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::SendReceiveSsl) #6799 - HostFunctionTest, HostObjectProtoTest crash under JSI/V8; # PreparedJavaScriptSourceTest asserts/fails under JSI/ChakraCore - name: Desktop.UnitTests.Filter From 83454dae55d41ec803ecad1b7b10a7091250c02e Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 16 Jun 2026 01:05:58 -0700 Subject: [PATCH 05/13] Make HTTP integrarion tests website-based --- .../HttpResourceIntegrationTests.cpp | 251 ++++-------------- .../TestWebSite/Microsoft/React/HttpTests.cs | 89 +++++++ vnext/TestWebSite/Program.cs | 47 ++++ 3 files changed, 189 insertions(+), 198 deletions(-) create mode 100644 vnext/TestWebSite/Microsoft/React/HttpTests.cs diff --git a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp index c20c173e86e..2474bf53c65 100644 --- a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// C4996: 'gethostbyaddr': Use getnameinfo() or GetNameInfoW() instead -#define _WINSOCK_DEPRECATED_NO_WARNINGS - #include #include @@ -11,71 +8,59 @@ #include #include -// Boost Library -#include - // Standard Library +#include #include -#include "HttpServer.h" - using namespace Microsoft::React; using namespace Microsoft::VisualStudio::CppUnitTestFramework; -namespace http = boost::beast::http; - using Networking::IHttpResource; using Networking::OriginPolicy; -using std::make_shared; using std::promise; using std::string; using std::vector; -using Test::DynamicRequest; -using Test::DynamicResponse; -using Test::EmptyResponse; -using Test::HttpServer; -using Test::ResponseWrapper; namespace Microsoft::React::Test { +namespace { + +const string kHttpResourceBaseUrl{"http://localhost:5555/rnw/http"}; + +string MakeHttpResourceUrl(const char *path) { + return kHttpResourceBaseUrl + path; +} + +string MakeCacheUrl(const string &requestId) { + return kHttpResourceBaseUrl + "/cache/" + requestId; +} + +} // namespace + TEST_CLASS (HttpResourceIntegrationTest) { - static uint16_t s_port; + static uint16_t s_requestId; TEST_METHOD_CLEANUP(MethodCleanup) { // Clear any runtime options that may be used by tests in this class. MicrosoftReactSetRuntimeOptionString("Http.UserAgent", nullptr); - // Bug in test HTTP server does not correctly release TCP port between test methods. - // Using a different por per test for now. - s_port++; + s_requestId++; } TEST_METHOD(RequestGetSucceeds) { - string url = "http://localhost:" + std::to_string(s_port); + string url = MakeHttpResourceUrl("/get"); promise resPromise; string error; int statusCode = 0; - auto server = make_shared(s_port); - server->Callbacks().OnGet = [&resPromise](const DynamicRequest &request) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::ok); - response.body() = Test::CreateStringResponseBody("some response content"); - - return {std::move(response)}; - }; - server->Start(); - auto resource = IHttpResource::Make(); resource->SetOnResponse([&resPromise, &statusCode](int64_t, IHttpResource::Response response) { statusCode = static_cast(response.StatusCode); }); resource->SetOnData([&resPromise](int64_t, string &&content) { resPromise.set_value(); }); - resource->SetOnError([&resPromise, &error, &server](int64_t, string &&message, bool) { + resource->SetOnError([&resPromise, &error](int64_t, string &&message, bool) { error = std::move(message); resPromise.set_value(); - - server->Stop(); }); resource->SendRequest( "GET", @@ -91,45 +76,26 @@ TEST_CLASS (HttpResourceIntegrationTest) { // Synchronize response. resPromise.get_future().wait(); - server->Stop(); Assert::AreEqual({}, error); Assert::AreEqual(200, statusCode); } TEST_METHOD(RequestGetHeadersSucceeds) { - string url = "http://localhost:" + std::to_string(s_port); + string url = MakeHttpResourceUrl("/headers"); promise rcPromise; string error; IHttpResource::Response response; - auto server = make_shared(s_port); - server->Callbacks().OnGet = [](const DynamicRequest &request) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::ok); - - // Response header - response.set(http::field::server, "Microsoft::React::Test::HttpServer"); - // Response content header - response.set(http::field::content_length, "0"); - // Response arbitrary header - response.set("ResponseHeaderName1", "ResponseHeaderValue1"); - - return {std::move(response)}; - }; - server->Start(); - auto resource = IHttpResource::Make(); resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { response = callbackResponse; rcPromise.set_value(); }); - resource->SetOnError([&rcPromise, &error, &server](int64_t, string &&message, bool) { + resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { error = std::move(message); rcPromise.set_value(); - - server->Abort(); }); //clang-format off @@ -152,25 +118,30 @@ TEST_CLASS (HttpResourceIntegrationTest) { //clang-format on rcPromise.get_future().wait(); - server->Stop(); Assert::AreEqual({}, error, L"Error encountered"); + bool foundServerHeader = false; + bool foundContentLengthHeader = false; + bool foundResponseHeader = false; for (auto header : response.Headers) { if (header.first == "Server") { - Assert::AreEqual({"Microsoft::React::Test::HttpServer"}, header.second, L"Wrong header"); + Assert::IsTrue(header.second.find("Microsoft.React.Test.Website") != string::npos, L"Wrong header"); + foundServerHeader = true; } else if (header.first == "Content-Length") { Assert::AreEqual({"0"}, header.second, L"Wrong header"); + foundContentLengthHeader = true; } else if (header.first == "ResponseHeaderName1") { Assert::AreEqual({"ResponseHeaderValue1"}, header.second, L"Wrong header"); - } else { - string message = "Unexpected header: [" + header.first + "]=[" + header.second + "]"; - Assert::Fail(Microsoft::Common::Unicode::Utf8ToUtf16(message).c_str()); + foundResponseHeader = true; } } + Assert::IsTrue(foundServerHeader, L"Missing Server header"); + Assert::IsTrue(foundContentLengthHeader, L"Missing Content-Length header"); + Assert::IsTrue(foundResponseHeader, L"Missing custom response header"); } TEST_METHOD(RequestGetExplicitUserAgentSucceeds) { - string url = "https://api.github.com/rate_limit"; + string url = MakeHttpResourceUrl("/useragent"); promise rcPromise; string error; @@ -207,7 +178,7 @@ TEST_CLASS (HttpResourceIntegrationTest) { } TEST_METHOD(RequestGetImplicitUserAgentSucceeds) { - string url = "https://api.github.com/rate_limit"; + string url = MakeHttpResourceUrl("/useragent"); promise rcPromise; string error; @@ -246,8 +217,7 @@ TEST_CLASS (HttpResourceIntegrationTest) { } TEST_METHOD(RequestGetMissingUserAgentFails) { - // string url = "http://localhost:" + std::to_string(s_port); - string url = "https://api.github.com/rate_limit"; + string url = MakeHttpResourceUrl("/useragent"); promise rcPromise; string error; @@ -302,7 +272,7 @@ TEST_CLASS (HttpResourceIntegrationTest) { } TEST_METHOD(RequestOptionsSucceeds) { - string url = "http://localhost:" + std::to_string(s_port); + string url = MakeHttpResourceUrl("/options"); promise getResponsePromise; promise getDataPromise; @@ -312,30 +282,13 @@ TEST_CLASS (HttpResourceIntegrationTest) { IHttpResource::Response optionsResponse; string content; - auto server = make_shared(s_port); - server->Callbacks().OnOptions = [](const DynamicRequest &request) -> ResponseWrapper { - EmptyResponse response; - response.result(http::status::partial_content); - response.set("PreflightName", "PreflightValue"); - - return {std::move(response)}; - }; - server->Callbacks().OnGet = [](const DynamicRequest &request) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::ok); - response.body() = Test::CreateStringResponseBody("Response Body"); - - return {std::move(response)}; - }; - server->Start(); - auto resource = IHttpResource::Make(); resource->SetOnResponse([&getResponse, &getResponsePromise, &optionsResponse, &optionsPromise]( int64_t, IHttpResource::Response callbackResponse) { - if (callbackResponse.StatusCode == static_cast(http::status::ok)) { + if (callbackResponse.StatusCode == 200) { getResponse = callbackResponse; getResponsePromise.set_value(); - } else if (callbackResponse.StatusCode == static_cast(http::status::partial_content)) { + } else if (callbackResponse.StatusCode == 206) { optionsResponse = callbackResponse; optionsPromise.set_value(); } @@ -347,14 +300,12 @@ TEST_CLASS (HttpResourceIntegrationTest) { getDataPromise.set_value(); }); resource->SetOnError( - [&optionsPromise, &getResponsePromise, &getDataPromise, &error, &server](int64_t, string &&message, bool) { + [&optionsPromise, &getResponsePromise, &getDataPromise, &error](int64_t, string &&message, bool) { error = std::move(message); optionsPromise.set_value(); getResponsePromise.set_value(); getDataPromise.set_value(); - - server->Stop(); }); //clang-format off @@ -385,25 +336,21 @@ TEST_CLASS (HttpResourceIntegrationTest) { optionsPromise.get_future().wait(); getResponsePromise.get_future().wait(); getDataPromise.get_future().wait(); - server->Stop(); Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(1), optionsResponse.Headers.size()); + bool foundPreflightHeader = false; for (auto header : optionsResponse.Headers) { if (header.first == "PreflightName") { Assert::AreEqual({"PreflightValue"}, header.second, L"Wrong header"); - } else { - string message = "Unexpected header: [" + header.first + "]=[" + header.second + "]"; - Assert::Fail(Microsoft::Common::Unicode::Utf8ToUtf16(message).c_str()); + foundPreflightHeader = true; } } + Assert::IsTrue(foundPreflightHeader, L"Missing PreflightName header"); Assert::AreEqual({"Response Body"}, content); } TEST_METHOD(SimpleRedirectGetSucceeds) { - auto port1 = s_port; - auto port2 = ++s_port; - string url = "http://localhost:" + std::to_string(port1); + string url = MakeHttpResourceUrl("/redirect/get/start"); promise responsePromise; promise contentPromise; @@ -411,29 +358,9 @@ TEST_CLASS (HttpResourceIntegrationTest) { string content; string error; - auto server1 = make_shared(port1); - server1->Callbacks().OnGet = [port2](const DynamicRequest &request) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::moved_permanently); - response.set(http::field::location, {"http://localhost:" + std::to_string(port2)}); - - return {std::move(response)}; - }; - auto server2 = make_shared(port2); - server2->Callbacks().OnGet = [](const DynamicRequest &request) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::ok); - response.body() = Test::CreateStringResponseBody("Redirect Content"); - - return {std::move(response)}; - }; - - server1->Start(); - server2->Start(); - auto resource = IHttpResource::Make(); resource->SetOnResponse([&responseResult, &responsePromise](int64_t, IHttpResource::Response response) { - if (response.StatusCode == static_cast(http::status::ok)) { + if (response.StatusCode == 200) { responseResult = response; responsePromise.set_value(); } @@ -444,7 +371,7 @@ TEST_CLASS (HttpResourceIntegrationTest) { if (!content.empty()) contentPromise.set_value(); }); - resource->SetOnError([&responsePromise, &contentPromise, &error, &server1](int64_t, string &&message, bool) { + resource->SetOnError([&responsePromise, &contentPromise, &error](int64_t, string &&message, bool) { error = std::move(message); responsePromise.set_value(); @@ -468,18 +395,13 @@ TEST_CLASS (HttpResourceIntegrationTest) { responsePromise.get_future().wait(); contentPromise.get_future().wait(); - server2->Stop(); - server1->Stop(); - Assert::AreEqual({}, error, L"Error encountered"); Assert::AreEqual(static_cast(200), responseResult.StatusCode); Assert::AreEqual({"Redirect Content"}, content); } TEST_METHOD(SimpleRedirectPatchSucceeds) { - auto port1 = s_port; - auto port2 = ++s_port; - string url = "http://localhost:" + std::to_string(port1); + string url = MakeHttpResourceUrl("/redirect/patch/start"); promise responsePromise; promise contentPromise; @@ -487,29 +409,9 @@ TEST_CLASS (HttpResourceIntegrationTest) { string content; string error; - auto server1 = make_shared(port1); - server1->Callbacks().OnPatch = [port2](const DynamicRequest &request) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::moved_permanently); - response.set(http::field::location, {"http://localhost:" + std::to_string(port2)}); - - return {std::move(response)}; - }; - auto server2 = make_shared(port2); - server2->Callbacks().OnPatch = [](const DynamicRequest &request) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::ok); - response.body() = Test::CreateStringResponseBody("Redirect Content"); - - return {std::move(response)}; - }; - - server1->Start(); - server2->Start(); - auto resource = IHttpResource::Make(); resource->SetOnResponse([&responseResult, &responsePromise](int64_t, IHttpResource::Response response) { - if (response.StatusCode == static_cast(http::status::ok)) { + if (response.StatusCode == 200) { responseResult = response; responsePromise.set_value(); } @@ -520,7 +422,7 @@ TEST_CLASS (HttpResourceIntegrationTest) { if (!content.empty()) contentPromise.set_value(); }); - resource->SetOnError([&responsePromise, &contentPromise, &error, &server1](int64_t, string &&message, bool) { + resource->SetOnError([&responsePromise, &contentPromise, &error](int64_t, string &&message, bool) { error = std::move(message); responsePromise.set_value(); @@ -544,36 +446,19 @@ TEST_CLASS (HttpResourceIntegrationTest) { responsePromise.get_future().wait(); contentPromise.get_future().wait(); - server2->Stop(); - server1->Stop(); - Assert::AreEqual({}, error, L"Error encountered"); Assert::AreEqual(static_cast(200), responseResult.StatusCode); Assert::AreEqual({"Redirect Content"}, content); } TEST_METHOD(TimeoutSucceeds) { - auto port = s_port; - string url = "http://localhost:" + std::to_string(port); + string url = MakeHttpResourceUrl("/delay/2000"); promise getPromise; string error; int statusCode = 0; bool timeoutError = false; - auto server = std::make_shared(s_port); - server->Callbacks().OnGet = [](const DynamicRequest &) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::ok); - - // Hold response to test client timeout - promise timer; - timer.get_future().wait_for(std::chrono::milliseconds(2000)); - - return {std::move(response)}; - }; - server->Start(); - auto resource = IHttpResource::Make(); resource->SetOnResponse([&getPromise, &statusCode](int64_t, IHttpResource::Response response) { statusCode = static_cast(response.StatusCode); @@ -597,7 +482,6 @@ TEST_CLASS (HttpResourceIntegrationTest) { [](int64_t) {} /*callback*/); getPromise.get_future().wait(); - server->Stop(); Assert::AreEqual({}, error); Assert::IsFalse(timeoutError); @@ -605,27 +489,13 @@ TEST_CLASS (HttpResourceIntegrationTest) { } TEST_METHOD(TimeoutFails) { - auto port = s_port; - string url = "http://localhost:" + std::to_string(port); + string url = MakeHttpResourceUrl("/delay/4000"); promise getPromise; string error; int statusCode = 0; bool timeoutError = false; - auto server = std::make_shared(s_port); - server->Callbacks().OnGet = [](const DynamicRequest &) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::ok); - - // Hold response to test client timeout - promise timer; - timer.get_future().wait_for(std::chrono::milliseconds(4000)); - - return {std::move(response)}; - }; - server->Start(); - auto resource = IHttpResource::Make(); resource->SetOnResponse([&getPromise, &statusCode](int64_t, IHttpResource::Response response) { statusCode = static_cast(response.StatusCode); @@ -649,7 +519,6 @@ TEST_CLASS (HttpResourceIntegrationTest) { [](int64_t) {} /*callback*/); getPromise.get_future().wait(); - server->Stop(); Assert::IsTrue(timeoutError); Assert::AreEqual({"[0x800705b4] This operation returned because the timeout period expired."}, error); @@ -657,7 +526,9 @@ TEST_CLASS (HttpResourceIntegrationTest) { } TEST_METHOD(GetDisableThenReenableCacheSucceeds) { - string url = "http://localhost:" + std::to_string(s_port); + const auto uniqueId = + std::to_string(s_requestId) + "-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + string url = MakeCacheUrl(uniqueId); std::vector> promises; promises.push_back(promise()); @@ -670,23 +541,8 @@ TEST_CLASS (HttpResourceIntegrationTest) { string error; int statusCode = 0; int requestCount = 0; - int body = 1; string result; - auto server = make_shared(s_port); - server->Callbacks().OnGet = [&requestCount, &body](const DynamicRequest &request) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::ok); - // Re-enable cache after the third request. - // Subsequent responses should be fetched from the client cache. - if (requestCount < 3) - response.insert(http::field::cache_control, "no-cache"); - response.body() = Test::CreateStringResponseBody(std::to_string(body++)); - - return {std::move(response)}; - }; - server->Start(); - auto resource = IHttpResource::Make(); resource->SetOnResponse([&statusCode, &promises](int64_t, IHttpResource::Response response) { statusCode = static_cast(response.StatusCode); @@ -718,7 +574,6 @@ TEST_CLASS (HttpResourceIntegrationTest) { // Synchronize response. p.get_future().wait(); } - server->Stop(); Assert::AreEqual({}, error); Assert::AreEqual(200, statusCode); @@ -726,6 +581,6 @@ TEST_CLASS (HttpResourceIntegrationTest) { } }; -/*static*/ uint16_t HttpResourceIntegrationTest::s_port = 4444; +/*static*/ uint16_t HttpResourceIntegrationTest::s_requestId = 1; } // namespace Microsoft::React::Test diff --git a/vnext/TestWebSite/Microsoft/React/HttpTests.cs b/vnext/TestWebSite/Microsoft/React/HttpTests.cs new file mode 100644 index 00000000000..b035a257554 --- /dev/null +++ b/vnext/TestWebSite/Microsoft/React/HttpTests.cs @@ -0,0 +1,89 @@ +namespace Microsoft.React.Test; + +using System.Collections.Concurrent; + +internal static class HttpTests +{ + private static readonly ConcurrentDictionary s_cacheRequestCounts = new(); + + public static IResult BasicGet() + { + return Results.Text("some response content", "text/plain"); + } + + public static IResult Headers(HttpContext context) + { + context.Response.Headers.Server = "Microsoft.React.Test.Website"; + context.Response.Headers.Append("ResponseHeaderName1", "ResponseHeaderValue1"); + context.Response.ContentLength = 0; + + return Results.Empty; + } + + public static IResult UserAgent(HttpContext context) + { + context.Response.Headers.CacheControl = "no-store, no-cache, max-age=0"; + context.Response.Headers.Pragma = "no-cache"; + + var userAgent = context.Request.Headers.UserAgent.ToString(); + + return string.IsNullOrWhiteSpace(userAgent) + ? Results.StatusCode(StatusCodes.Status403Forbidden) + : Results.Ok(); + } + + public static IResult Options(HttpContext context) + { + context.Response.Headers.Append("PreflightName", "PreflightValue"); + + return Results.Text(string.Empty, contentType: null, statusCode: StatusCodes.Status206PartialContent); + } + + public static IResult OptionsGet() + { + return Results.Text("Response Body", "text/plain"); + } + + public static IResult RedirectGetStart(HttpContext context) + { + context.Response.StatusCode = StatusCodes.Status301MovedPermanently; + context.Response.Headers.Location = "/rnw/http/redirect/get/final"; + + return Results.Empty; + } + + public static IResult RedirectGetFinal() + { + return Results.Text("Redirect Content", "text/plain"); + } + + public static IResult RedirectPatchStart(HttpContext context) + { + context.Response.StatusCode = StatusCodes.Status301MovedPermanently; + context.Response.Headers.Location = "/rnw/http/redirect/patch/final"; + + return Results.Empty; + } + + public static IResult RedirectPatchFinal() + { + return Results.Text("Redirect Content", "text/plain"); + } + + public static async Task Delay(int milliseconds) + { + await Task.Delay(milliseconds); + return Results.Ok(); + } + + public static IResult Cache(string id, HttpContext context) + { + var requestCount = s_cacheRequestCounts.AddOrUpdate(id, 1, static (_, current) => current + 1); + if (requestCount <= 3) + { + context.Response.Headers.CacheControl = "no-cache"; + } + + return Results.Text(Math.Min(requestCount, 4).ToString(), "text/plain"); + } +} diff --git a/vnext/TestWebSite/Program.cs b/vnext/TestWebSite/Program.cs index bf70c3cac2e..bf97383c83c 100644 --- a/vnext/TestWebSite/Program.cs +++ b/vnext/TestWebSite/Program.cs @@ -94,6 +94,53 @@ async Task DefaultRequestDelegate(HttpContext context) "/officedev/office-js/issues/5869", Microsoft.Office.Test.OfficeJsTests.Issue5869); +app.MapGet( + "/rnw/http/get", + Microsoft.React.Test.HttpTests.BasicGet); + +app.MapGet( + "/rnw/http/headers", + Microsoft.React.Test.HttpTests.Headers); + +app.MapGet( + "/rnw/http/useragent", + Microsoft.React.Test.HttpTests.UserAgent); + +app.MapMethods( + "/rnw/http/options", + ["OPTIONS"], + Microsoft.React.Test.HttpTests.Options); + +app.MapGet( + "/rnw/http/options", + Microsoft.React.Test.HttpTests.OptionsGet); + +app.MapGet( + "/rnw/http/redirect/get/start", + Microsoft.React.Test.HttpTests.RedirectGetStart); + +app.MapGet( + "/rnw/http/redirect/get/final", + Microsoft.React.Test.HttpTests.RedirectGetFinal); + +app.MapMethods( + "/rnw/http/redirect/patch/start", + ["PATCH"], + Microsoft.React.Test.HttpTests.RedirectPatchStart); + +app.MapMethods( + "/rnw/http/redirect/patch/final", + ["PATCH"], + Microsoft.React.Test.HttpTests.RedirectPatchFinal); + +app.MapGet( + "/rnw/http/delay/{milliseconds:int}", + Microsoft.React.Test.HttpTests.Delay); + +app.MapGet( + "/rnw/http/cache/{id}", + Microsoft.React.Test.HttpTests.Cache); + #endregion Request Mappings await app.RunAsync(); From 870a0e400849a47c64165a24bc504acdc91a12f0 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 16 Jun 2026 01:10:18 -0700 Subject: [PATCH 06/13] Remove s_requestId --- .../HttpResourceIntegrationTests.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp index 2474bf53c65..546c7c31606 100644 --- a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp @@ -38,13 +38,9 @@ string MakeCacheUrl(const string &requestId) { } // namespace TEST_CLASS (HttpResourceIntegrationTest) { - static uint16_t s_requestId; - TEST_METHOD_CLEANUP(MethodCleanup) { // Clear any runtime options that may be used by tests in this class. MicrosoftReactSetRuntimeOptionString("Http.UserAgent", nullptr); - - s_requestId++; } TEST_METHOD(RequestGetSucceeds) { string url = MakeHttpResourceUrl("/get"); @@ -526,8 +522,7 @@ TEST_CLASS (HttpResourceIntegrationTest) { } TEST_METHOD(GetDisableThenReenableCacheSucceeds) { - const auto uniqueId = - std::to_string(s_requestId) + "-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const auto uniqueId = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); string url = MakeCacheUrl(uniqueId); std::vector> promises; @@ -581,6 +576,4 @@ TEST_CLASS (HttpResourceIntegrationTest) { } }; -/*static*/ uint16_t HttpResourceIntegrationTest::s_requestId = 1; - } // namespace Microsoft::React::Test From 98929224e65da85ad1c587afcbd68eb62bd790a6 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 16 Jun 2026 01:12:16 -0700 Subject: [PATCH 07/13] Change files --- ...ative-windows-7d9a085f-d222-4893-964b-4b39501b550f.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-7d9a085f-d222-4893-964b-4b39501b550f.json diff --git a/change/react-native-windows-7d9a085f-d222-4893-964b-4b39501b550f.json b/change/react-native-windows-7d9a085f-d222-4893-964b-4b39501b550f.json new file mode 100644 index 00000000000..a3d6d2da594 --- /dev/null +++ b/change/react-native-windows-7d9a085f-d222-4893-964b-4b39501b550f.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Drop in-process HTTP test server", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} From 36748adfe4c78e8e0bdae89da18c5f3b0185f2c4 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 16 Jun 2026 01:29:24 -0700 Subject: [PATCH 08/13] Delete in-process HTTP server --- vnext/Desktop.IntegrationTests/HttpServer.cpp | 404 ------------------ vnext/Desktop.IntegrationTests/HttpServer.h | 142 ------ ...t.Windows.Desktop.IntegrationTests.vcxproj | 2 - ...s.Desktop.IntegrationTests.vcxproj.filters | 6 - 4 files changed, 554 deletions(-) delete mode 100644 vnext/Desktop.IntegrationTests/HttpServer.cpp delete mode 100644 vnext/Desktop.IntegrationTests/HttpServer.h diff --git a/vnext/Desktop.IntegrationTests/HttpServer.cpp b/vnext/Desktop.IntegrationTests/HttpServer.cpp deleted file mode 100644 index 32457be591a..00000000000 --- a/vnext/Desktop.IntegrationTests/HttpServer.cpp +++ /dev/null @@ -1,404 +0,0 @@ -// clang-format off - -// C4996: 'gethostbyaddr': Use getnameinfo() or GetNameInfoW() instead -#define _WINSOCK_DEPRECATED_NO_WARNINGS - -#include "HttpServer.h" - -#include -#include -#include - -// Include to prevent 'incomplete type' errors. -#include -#include - -using namespace boost::asio::ip; - -namespace http = boost::beast::http; - -using boost::asio::io_context; -using boost::asio::make_strand; -using boost::beast::bind_front_handler; -using boost::system::error_code; -using std::function; -using std::make_shared; -using std::shared_ptr; -using std::static_pointer_cast; -using std::string; - -namespace Microsoft::React::Test -{ - -#pragma region Utility functions - -boost::beast::multi_buffer CreateStringResponseBody(string&& content) -{ - auto result = boost::beast::multi_buffer(); - auto n = boost::asio::buffer_copy(result.prepare(content.size()), boost::asio::buffer(content)); - result.commit(n); - - return result; -} - -#pragma endregion // Utility functions - -#pragma region ResponseWrapper - -ResponseWrapper::ResponseWrapper(DynamicResponse&& response) - : m_response{ make_shared(std::move(response)) } - , m_type{ ResponseType::Dynamic } -{ -} - -ResponseWrapper::ResponseWrapper(EmptyResponse&& response) - : m_response{ make_shared(std::move(response)) } - , m_type{ ResponseType::Empty } -{ -} - -ResponseWrapper::ResponseWrapper(FileResponse&& response) - : m_response{ make_shared(std::move(response)) } - , m_type{ ResponseType::File } -{ -} - -ResponseWrapper::ResponseWrapper(StringResponse&& response) - : m_response{ make_shared(std::move(response)) } - , m_type{ ResponseType::String } -{ -} - -ResponseWrapper::ResponseWrapper(DynamicResponse const& response) - : m_response{ make_shared(response) } - , m_type{ ResponseType::Dynamic } -{ -} - -ResponseWrapper::ResponseWrapper(EmptyResponse const& response) - : m_response{ make_shared(response) } - , m_type{ ResponseType::Empty } -{ -} - -ResponseWrapper::ResponseWrapper(StringResponse const& response) - : m_response{ make_shared(response) } - , m_type{ ResponseType::String } -{ -} - -shared_ptr ResponseWrapper::Response() -{ - return m_response; -} - -ResponseWrapper::ResponseType ResponseWrapper::Type() -{ - return m_type; -} - -#pragma endregion ResponseWrapper - -#pragma region HttpSession - -HttpSession::HttpSession(tcp::socket &&socket, HttpCallbacks &callbacks, io_context& context) - : m_stream{ std::move(socket) } - , m_callbacks{ callbacks } -{ -} - -HttpSession::~HttpSession() {} - -void HttpSession::Start() -{ - m_sendLambda = [self = shared_from_this()](ResponseWrapper&& wrapper) - { - auto dr = wrapper.Response(); - auto type = wrapper.Type(); - self->m_response = make_shared(std::move(wrapper)); - - // Ugh! - switch (type) - { - case ResponseWrapper::ResponseType::Empty: - http::async_write( - self->m_stream, - *static_pointer_cast(dr), - bind_front_handler( - &HttpSession::OnWrite, - self->shared_from_this(), - static_pointer_cast(dr)->need_eof() - ) - ); - break; - - case ResponseWrapper::ResponseType::Dynamic: - http::async_write( - self->m_stream, - *static_pointer_cast(dr), - bind_front_handler( - &HttpSession::OnWrite, - self->shared_from_this(), - static_pointer_cast(dr)->need_eof() - ) - ); - break; - - case ResponseWrapper::ResponseType::File: - http::async_write( - self->m_stream, - *static_pointer_cast(dr), - bind_front_handler( - &HttpSession::OnWrite, - self->shared_from_this(), - static_pointer_cast(dr)->need_eof() - ) - ); - break; - - case ResponseWrapper::ResponseType::String: - http::async_write( - self->m_stream, - *static_pointer_cast(dr), - bind_front_handler( - &HttpSession::OnWrite, - self->shared_from_this(), - static_pointer_cast(dr)->need_eof() - ) - ); - break; - - default: - throw; - } - }; - - // Ensure thread-safety. - boost::asio::dispatch(m_stream.get_executor(), bind_front_handler(&HttpSession::Read, shared_from_this())); -} - -void HttpSession::Read() -{ - // Clear request. - m_request = {}; - - http::async_read( - m_stream, - m_buffer, - m_request, - bind_front_handler(&HttpSession::OnRead, shared_from_this()) - ); -} - -void HttpSession::OnRead(error_code ec, size_t /*transferred*/) -{ - if (http::error::end_of_stream == ec) - return Close(); - - if (ec) - { - // ISS:2735328 - Implement failure propagation mechanism. - return; - } - - Respond(); -} - -void HttpSession::Respond() -{ - switch (m_request.method()) - { - case http::verb::get: - m_sendLambda(m_callbacks.OnGet(m_request)); - break; - - case http::verb::options: - if (!m_callbacks.OnOptions) - { - // Default OPTIONS handler - m_callbacks.OnOptions = [](const DynamicRequest& request) -> DynamicResponse { - DynamicResponse response{http::status::accepted, request.version()}; - response.set( - http::field::access_control_request_headers, - "Access-Control-Allow-Headers, Content-type, Custom-Header, Header-expose-allowed"); - response.set(http::field::access_control_allow_methods, "GET, POST, DELETE"); - response.set(http::field::access_control_expose_headers, "Header-expose-allowed"); - response.result(http::status::ok); - - return { std::move(response) }; - }; - } - m_sendLambda(m_callbacks.OnOptions(m_request)); - break; - - case http::verb::post: - break; - case http::verb::put: - break; - case http::verb::delete_: - this->Close(); - break; - case http::verb::patch: - m_sendLambda(m_callbacks.OnPatch(m_request)); - break; - - case http::verb::connect: - m_sendLambda(m_callbacks.OnConnect(m_request)); - break; - - case http::verb::trace: - m_sendLambda(m_callbacks.OnTrace(m_request)); - break; - - default: - // ISS:2735328 - Implement failure propagation mechanism - throw; - } -} - -void HttpSession::OnWrite(bool close, error_code ec, size_t /*transferred*/) -{ - if (ec) - { - return; - } - - if (m_callbacks.OnResponseSent) - { - m_callbacks.OnResponseSent(); - } - - if (close) - return Close(); - - // Clear response - m_response = nullptr; - - Read(); -} - -void HttpSession::Close() -{ - error_code ec; - m_stream.socket().shutdown(tcp::socket::shutdown_send, ec); -} - -#pragma endregion // HttpSession - -#pragma region HttpServer - -HttpServer::HttpServer(string &&address, uint16_t port, int concurrency) - : m_ioThreadCount{ static_cast(concurrency) } - , m_ioContext{concurrency} - , m_acceptor{make_strand(m_ioContext)} -{ - auto endpoint = tcp::endpoint{make_address(std::move(address)), port}; - error_code ec; - m_acceptor.open(endpoint.protocol(), ec); - if (ec) - { - // ISS:2735328 - Implement failure propagation mechanism - return; - } - - m_acceptor.set_option(boost::asio::socket_base::reuse_address(true), ec); - if (ec) - { - // ISS:2735328 - Implement failure propagation mechanism - return; - } - - m_acceptor.bind(endpoint, ec); - if (ec) - { - // ISS:2735328 - Implement failure propagation mechanism - return; - } - - m_acceptor.listen(boost::asio::socket_base::max_listen_connections, ec); - if (ec) - { - // ISS:2735328 - Implement failure propagation mechanism - return; - } -} - -HttpServer::HttpServer(uint16_t port, int concurrency) - : HttpServer("0.0.0.0", port, concurrency) -{ -} - -HttpServer::~HttpServer() {} - -void HttpServer::Accept() -{ - if (!m_acceptor.is_open()) - return; - - m_acceptor.async_accept( - make_strand(m_ioContext), - bind_front_handler( - &HttpServer::OnAccept, - shared_from_this() - ) - ); -} - -void HttpServer::OnAccept(error_code ec, tcp::socket socket) -{ - if (ec) - { - // ISS:2735328 - Implement failure propagation mechanism - return; - } - else - { - make_shared(std::move(socket), m_callbacks, m_ioContext)->Start(); - } - - Accept(); -} - -void HttpServer::Start() -{ - Accept(); - - m_ioThreads.reserve(m_ioThreadCount); - for (size_t i = 0; i < m_ioThreadCount; i++) - { - m_ioThreads.emplace_back([self = shared_from_this()]() - { - // See - // https://www.boost.org/doc/libs/1_76_0/doc/html/boost_asio/reference/io_context/run/overload1.html - // The run() function blocks until all work has finished and there are no - // more handlers to be dispatched, or until the io_context has been stopped. - self->m_ioContext.run(); - }); - } -} - -void HttpServer::Stop() -{ - m_ioContext.stop(); - - for (auto& t : m_ioThreads) - if (t.joinable()) - t.join(); -} - -void HttpServer::Abort() -{ - if (m_ioContext.stopped()) - return; - - Stop(); -} - -HttpCallbacks& HttpServer::Callbacks() -{ - return m_callbacks; -} - -#pragma endregion HttpServer - -} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/HttpServer.h b/vnext/Desktop.IntegrationTests/HttpServer.h deleted file mode 100644 index 6ba24c18082..00000000000 --- a/vnext/Desktop.IntegrationTests/HttpServer.h +++ /dev/null @@ -1,142 +0,0 @@ -// clang-format off -#pragma once - -#include -#include -#include -#include -#include - -#include - -namespace Microsoft::React::Test -{ -using DynamicRequest = boost::beast::http::request; - -using DynamicResponse = boost::beast::http::response; - -using EmptyResponse = boost::beast::http::response; - -using FileResponse = boost::beast::http::response; - -using StringResponse = boost::beast::http::response; - -class ResponseWrapper -{ -public: - enum class ResponseType : size_t { Empty, Dynamic, File, String }; - -private: - std::shared_ptr m_response; - ResponseType m_type; - -public: - ResponseWrapper(DynamicResponse&& response); - - ResponseWrapper(EmptyResponse&& response); - - ResponseWrapper(FileResponse&& response); - - ResponseWrapper(StringResponse&& response); - - ResponseWrapper(DynamicResponse const& response); - - ResponseWrapper(EmptyResponse const& response); - - ResponseWrapper(StringResponse const& response); - - ResponseWrapper(ResponseWrapper&&) = default; - - std::shared_ptr Response(); - - ResponseType Type(); -}; - -#pragma region Utility functions - -boost::beast::multi_buffer CreateStringResponseBody(std::string&& content); - -#pragma endregion Utility functions - -struct HttpCallbacks -{ - std::function OnResponseSent; - std::function OnRequest; - std::function OnGet; - std::function OnPost; - std::function OnPatch; - std::function OnOptions; - - std::function OnConnect; - std::function OnTrace; - - //Not supported by Boost/Beast - #if 0 - std::function OnTrack; - #endif // 0 -}; - -/// -// Represents a client session in within an owning server instance. -// Generates and submits the appropriate HTTP response. -/// -class HttpSession : public std::enable_shared_from_this -{ - boost::beast::tcp_stream m_stream; - boost::beast::flat_buffer m_buffer; - DynamicRequest m_request; - std::shared_ptr m_response; // Generic response - HttpCallbacks& m_callbacks; - std::function m_sendLambda; - - void Read(); - void Respond(); - void Close(); - - void OnRead(boost::system::error_code ec, std::size_t transferred); - void OnWrite(bool close, boost::system::error_code ec, std::size_t transferred); - - public: - HttpSession(boost::asio::ip::tcp::socket&& socket, HttpCallbacks &callbacks, - boost::asio::io_context& ioContext - ); - - ~HttpSession(); - - void Start(); -}; - -/// -// Represents an HTTP server endpoint (IP:PORT). -// Accepts client connections and dispatches sessions for each incoming -// connection. -/// -class HttpServer : public std::enable_shared_from_this -{ - size_t m_ioThreadCount; - std::vector m_ioThreads; - boost::asio::io_context m_ioContext; - boost::asio::ip::tcp::acceptor m_acceptor; - HttpCallbacks m_callbacks; - - void OnAccept(boost::system::error_code ec, boost::asio::ip::tcp::socket socket); - - public: - /// - // address - Valid IP address string (i.e. "127.0.0.1). - // port - TCP port number (i.e. 80). - /// - HttpServer(std::string &&address, uint16_t port, int concurrency = 1); - HttpServer(uint16_t port, int concurrency = 1); - - ~HttpServer(); - - void Accept(); - void Start(); - void Stop(); - void Abort();//TODO: Remove? - - HttpCallbacks& Callbacks(); -}; - -} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj index 36aa5da6d54..aaaed8d196a 100644 --- a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj +++ b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj @@ -108,7 +108,6 @@ - @@ -137,7 +136,6 @@ - diff --git a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters index f7f55cfb3da..3010c0cc9c0 100644 --- a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters +++ b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters @@ -39,9 +39,6 @@ Integration Tests - - Source Files - Source Files @@ -71,9 +68,6 @@ Header Files\Modules - - Header Files - Header Files From e3c0a5a3fb8e3262d0f1fa537a8a9a1af632ac78 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 16 Jun 2026 02:08:02 -0700 Subject: [PATCH 09/13] Migrate WebSocket integration tests --- ...t.Windows.Desktop.IntegrationTests.vcxproj | 2 - ...s.Desktop.IntegrationTests.vcxproj.filters | 6 - .../WebSocketIntegrationTest.cpp | 82 ++--- .../WebSocketServer.cpp | 328 ------------------ .../WebSocketServer.h | 108 ------ .../Microsoft/React/WebSocketTests.cs | 89 +++++ vnext/TestWebSite/Program.cs | 10 + 7 files changed, 119 insertions(+), 506 deletions(-) delete mode 100644 vnext/Desktop.IntegrationTests/WebSocketServer.cpp delete mode 100644 vnext/Desktop.IntegrationTests/WebSocketServer.h diff --git a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj index aaaed8d196a..3f206877319 100644 --- a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj +++ b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj @@ -118,7 +118,6 @@ - @@ -145,7 +144,6 @@ - diff --git a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters index 3010c0cc9c0..425ea8258f8 100644 --- a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters +++ b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters @@ -39,9 +39,6 @@ Integration Tests - - Source Files - Source Files @@ -71,9 +68,6 @@ Header Files - - Header Files - Header Files diff --git a/vnext/Desktop.IntegrationTests/WebSocketIntegrationTest.cpp b/vnext/Desktop.IntegrationTests/WebSocketIntegrationTest.cpp index 97ab167a361..ffb3be1818a 100644 --- a/vnext/Desktop.IntegrationTests/WebSocketIntegrationTest.cpp +++ b/vnext/Desktop.IntegrationTests/WebSocketIntegrationTest.cpp @@ -14,13 +14,10 @@ // Standard Library #include -#include "WebSocketServer.h" - using namespace Microsoft::React; using namespace Microsoft::VisualStudio::CppUnitTestFramework; using std::chrono::milliseconds; -using std::make_shared; using std::once_flag; using std::promise; using std::string; @@ -52,8 +49,6 @@ namespace Microsoft::React::Test { TEST_CLASS (WebSocketIntegrationTest) { - static uint16_t s_port; - void SendReceiveCloseBase(bool isSecure) { string scheme = "ws"; @@ -93,13 +88,6 @@ TEST_CLASS (WebSocketIntegrationTest) Assert::AreEqual({"prefix_response"}, received); } - TEST_METHOD_CLEANUP(MethodCleanup) - { - // Bug in WebSocketServer does not correctly release TCP port between test methods. - // Using a different por per test for now. - s_port++; - } - TEST_METHOD(ConnectClose) { auto ws = IWebSocketResource::Make(); @@ -246,49 +234,34 @@ TEST_CLASS (WebSocketIntegrationTest) Assert::AreEqual(static_cast(LEN + string("_response").length()), result.length()); } - /* - Currently, this test requires a modified websocket test server. - TODO: Write an in-place werver with this behavior: - - server.on('connection', (ws) => { - ws.on('message', (message) => { - for (var propName in ws.upgradeReq.headers) { - console.log(`${propName}: [${ws.upgradeReq.headers[propName]}]`); - } - - // Send the cookie back to the client. - ws.send(ws.upgradeReq.headers.cookie); - }); - }); - - Test passes, otherwise. - */ - BEGIN_TEST_METHOD_ATTRIBUTE(AdditionalHeaders) - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(AdditionalHeaders) { - string cookie; - auto server = make_shared(s_port); - server->SetOnHandshake([server](boost::beast::websocket::response_type &response) { - auto cookie = string{response[boost::beast::http::field::cookie]}; - server->SetMessageFactory([cookie](string &&) { return cookie; }); - }); auto ws = IWebSocketResource::Make(); - promise response; - ws->SetOnMessage([&response](size_t size, const string &message, bool isBinary) { response.set_value(message); }); + once_flag responseFlag; + promise responsePromise; + ws->SetOnMessage([&responseFlag, &responsePromise](size_t size, const string &message, bool isBinary) { + SetPromise(responseFlag, responsePromise, message); + }); + string errorMessage; + ws->SetOnError([&errorMessage, &responseFlag, &responsePromise](Error error) { + errorMessage = error.Message; + SetPromise(responseFlag, responsePromise, ""); + }); - server->Start(); - ws->Connect("ws://localhost:" + std::to_string(s_port), {}, {{L"Cookie", "JSESSIONID=AD9A320CC4034641997FF903F1D10906"}}); - ws->Send(""); + ws->Connect( + "ws://localhost:5555/rnw/websockets/echocookie", + {}, + {{L"Cookie", "JSESSIONID=AD9A320CC4034641997FF903F1D10906"}} + ); + ws->Send("request"); - auto future = response.get_future(); + auto future = responsePromise.get_future(); future.wait(); string result = future.get(); + Assert::AreEqual({}, errorMessage); Assert::AreEqual({"JSESSIONID=AD9A320CC4034641997FF903F1D10906"}, result); ws->Close(CloseCode::Normal, "No reason"); - server->Stop(); } BEGIN_TEST_METHOD_ATTRIBUTE(SendReceiveSsl) @@ -328,17 +301,6 @@ TEST_CLASS (WebSocketIntegrationTest) BEGIN_TEST_METHOD_ATTRIBUTE(SendBinary) END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(SendBinary) { - - auto server = make_shared(s_port); - - // The result should be the message "grown" by one element which value is the result's size. - server->SetMessageFactory([](vector&& message) - { - message.push_back(static_cast(message.size() + 1)); - - return message; - }); - auto ws = IWebSocketResource::Make(); std::vector messages{ @@ -360,8 +322,7 @@ TEST_CLASS (WebSocketIntegrationTest) errorMessage = error.Message; }); - server->Start(); - ws->Connect("ws://localhost:" + std::to_string(s_port)); + ws->Connect("ws://localhost:5555/rnw/websockets/echobinarygrow"); // Send all but the last message. // Compare result with the next message in the sequence. @@ -378,7 +339,6 @@ TEST_CLASS (WebSocketIntegrationTest) } ws->Close(CloseCode::Normal, "Closing after reading"); - server->Stop(); Assert::AreEqual({}, errorMessage); } @@ -427,7 +387,7 @@ TEST_CLASS (WebSocketIntegrationTest) } BEGIN_TEST_METHOD_ATTRIBUTE(AbruptDisconnectFailsWithSpecificMessage) - TEST_IGNORE() //TODO: Find a way to emulate abrupt disconnection using Test::WebSocketServer + TEST_IGNORE() //TODO: Find a way to emulate abrupt disconnection using TestWebsite. END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(AbruptDisconnectFailsWithSpecificMessage) { @@ -456,6 +416,4 @@ TEST_CLASS (WebSocketIntegrationTest) } }; -uint16_t WebSocketIntegrationTest::s_port = 6666; - } // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/WebSocketServer.cpp b/vnext/Desktop.IntegrationTests/WebSocketServer.cpp deleted file mode 100644 index fc14967f76e..00000000000 --- a/vnext/Desktop.IntegrationTests/WebSocketServer.cpp +++ /dev/null @@ -1,328 +0,0 @@ -// clang-format off - -// C4996: 'gethostbyaddr': Use getnameinfo() or GetNameInfoW() instead -#define _WINSOCK_DEPRECATED_NO_WARNINGS - -#include "WebSocketServer.h" - -#include -#include -#include - -using namespace boost::asio; - -namespace websocket = boost::beast::websocket; - -using boost::beast::bind_front_handler; -using boost::beast::tcp_stream; -using boost::system::error_code; -using std::function; -using std::string; -using std::vector; - -using Error = Microsoft::React::Networking::IWebSocketResource::Error; -using ErrorType = Microsoft::React::Networking::IWebSocketResource::ErrorType; - -namespace Microsoft::React::Test -{ - -#pragma region BaseWebSocketSession - -template -BaseWebSocketSession::BaseWebSocketSession(WebSocketServiceCallbacks& callbacks) - : m_callbacks{callbacks} - , m_state{State::Stopped}{} - -template -BaseWebSocketSession::~BaseWebSocketSession() {} - -template -void BaseWebSocketSession::Start() -{ - Accept(); -} - -template -void BaseWebSocketSession::Accept() -{ - // Turn off the timeout on the tcp_stream, because - // the websocket stream has its own timeout system. - boost::beast::get_lowest_layer(*m_stream).expires_never(); - - m_stream->set_option( - websocket::stream_base::timeout::suggested(boost::beast::role_type::server) - ); - - m_stream->set_option(websocket::stream_base::decorator([self = this->SharedFromThis()](websocket::response_type& response) - { - response.set(boost::beast::http::field::server, string(BOOST_BEAST_VERSION_STRING) + "Test WebSocket Server"); - self->OnHandshake(response); - })); - - m_stream->async_accept( - bind_front_handler(&BaseWebSocketSession::OnAccept, this->SharedFromThis()) - ); -} - -template -void BaseWebSocketSession::OnHandshake(websocket::response_type& response) -{ - if (m_callbacks.OnHandshake) - m_callbacks.OnHandshake(response); -} - -template -void BaseWebSocketSession::OnAccept(error_code ec) -{ - if (ec) - { - if (m_callbacks.OnError) - m_callbacks.OnError({ec.message(), ErrorType::Connection}); - - return; - } - - m_state = State::Started; - - if (m_callbacks.OnConnection) - m_callbacks.OnConnection(); - - Read(); -} - -template -void BaseWebSocketSession::Read() -{ - if (State::Stopped == m_state) - return; - - m_stream->async_read( - m_buffer, - bind_front_handler(&BaseWebSocketSession::OnRead, this->SharedFromThis()) - ); -} - -template -void BaseWebSocketSession::OnRead(error_code ec, size_t /*transferred*/) -{ - if (websocket::error::closed == ec) - return; - - if (ec) - { - if (ec.value() != error::connection_reset && - ec.value() != error::connection_aborted && - ec.value() != 335544539 /*short read*/) - if (m_callbacks.OnError) - m_callbacks.OnError({ec.message(), ErrorType::Receive}); - } - - if (m_stream->got_binary()) - { - if (!m_callbacks.BinaryMessageFactory) - { - m_buffer.consume(m_buffer.size()); - return Read(); - } - - // Obtain raw pointer to underlying buffer memory. - // multi_buffer -> ConstBufferSequence -> const_buffer -> void* data() - // https://stackoverflow.com/questions/9510684 - auto data = boost::asio::buffer_cast(*boost::asio::buffer_sequence_begin(m_buffer.data())); - m_binaryMessage = m_callbacks.BinaryMessageFactory({data, data + m_buffer.size()}); - m_buffer.consume(m_buffer.size()); - - m_stream->binary(true); - m_stream->async_write( - buffer(m_binaryMessage), - bind_front_handler(&BaseWebSocketSession::OnWrite, this->SharedFromThis()) - ); - } - else - { - if (!m_callbacks.MessageFactory) - { - m_buffer.consume(m_buffer.size()); - return Read(); - } - - m_message = m_callbacks.MessageFactory(buffers_to_string(m_buffer.data())); - m_buffer.consume(m_buffer.size()); - - m_stream->text(true); - m_stream->async_write( - buffer(m_message), - bind_front_handler(&BaseWebSocketSession::OnWrite, this->SharedFromThis()) - ); - } -} - -template -void BaseWebSocketSession::OnWrite(error_code ec, size_t /*transferred*/) -{ - if (ec) - { - if (ec.value() != error::operation_aborted) - if (m_callbacks.OnError) - m_callbacks.OnError({ec.message(), ErrorType::Send}); - - return; - } - - // Clear outgoing message contents. - m_message.clear(); - - Read(); -} - -#pragma endregion BaseWebSocketSession - -#pragma region WebSocketSession - -WebSocketSession::WebSocketSession(ip::tcp::socket socket, WebSocketServiceCallbacks& callbacks) - : BaseWebSocketSession(callbacks) -{ - m_stream = std::make_shared>(std::move(socket)); -} - -WebSocketSession::~WebSocketSession() {} - -#pragma region BaseWebSocketSession - -std::shared_ptr> WebSocketSession::SharedFromThis() /*override*/ -{ - return this->shared_from_this(); -} - -#pragma endregion BaseWebSocketSession - -#pragma endregion WebSocketSession - -#pragma region WebSocketServer - -WebSocketServer::WebSocketServer(uint16_t port, bool useTLS) - : m_acceptor{make_strand(m_context)}, m_sessions{}, m_useTLS{useTLS} -{ - ip::tcp::endpoint ep{ip::make_address("0.0.0.0"), port}; - error_code ec; - - m_acceptor.open(ep.protocol(), ec); - if (ec) - { - if (m_callbacks.OnError) - m_callbacks.OnError({ec.message(), ErrorType::Connection}); - - return; - } - - m_acceptor.set_option(socket_base::reuse_address(true), ec); - if (ec) - { - if (m_callbacks.OnError) - m_callbacks.OnError({ec.message(), ErrorType::Connection}); - - return; - } - - m_acceptor.bind(ep, ec); - if (ec) - { - if (m_callbacks.OnError) - m_callbacks.OnError({ec.message(), ErrorType::Connection}); - - return; - } - - m_acceptor.listen(socket_base::max_listen_connections, ec); - if (ec) - { - if (m_callbacks.OnError) - m_callbacks.OnError({ec.message(), ErrorType::Connection}); - - return; - } -} - -WebSocketServer::WebSocketServer(int port, bool useTLS) : WebSocketServer(static_cast(port), useTLS) {} - -void WebSocketServer::Start() -{ - if (!m_acceptor.is_open()) - return; - - Accept(); - - m_contextThread = std::thread([self = shared_from_this()]() - { - self->m_context.run(); - }); -} - -void WebSocketServer::Accept() -{ - m_acceptor.async_accept( - make_strand(m_context), - bind_front_handler(&WebSocketServer::OnAccept, shared_from_this()) - ); -} - -void WebSocketServer::Stop() -{ - if (m_acceptor.is_open()) - m_acceptor.close(); - - m_contextThread.join(); -} - -void WebSocketServer::OnAccept(error_code ec, ip::tcp::socket socket) -{ - if (ec) - { - if (ec.value() != error::operation_aborted) - if (m_callbacks.OnError) - m_callbacks.OnError({ec.message(), ErrorType::Connection}); - - return; - } - - std::shared_ptr session = std::shared_ptr(new WebSocketSession(std::move(socket), m_callbacks)); - - m_sessions.push_back(session); - session->Start(); - - Accept(); -} - -void WebSocketServer::SetOnConnection(function&& func) -{ - m_callbacks.OnConnection = std::move(func); -} - -void WebSocketServer::SetOnHandshake(function&& func) -{ - m_callbacks.OnHandshake = std::move(func); -} - -void WebSocketServer::SetOnMessage(function&& func) -{ - m_callbacks.OnMessage = std::move(func); -} - -void WebSocketServer::SetMessageFactory(function&& func) -{ - m_callbacks.MessageFactory = std::move(func); -} - -void WebSocketServer::SetMessageFactory(function(vector&&)>&& func) -{ - m_callbacks.BinaryMessageFactory = std::move(func); -} - -void WebSocketServer::SetOnError(function&& func) -{ - m_callbacks.OnError = std::move(func); -} - -#pragma endregion WebSocketServer - -} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/WebSocketServer.h b/vnext/Desktop.IntegrationTests/WebSocketServer.h deleted file mode 100644 index 63b01788fdc..00000000000 --- a/vnext/Desktop.IntegrationTests/WebSocketServer.h +++ /dev/null @@ -1,108 +0,0 @@ -// clang-format off -#pragma once - -#include - -// Boost Framework -#include -#include -#include - -// Standard Library -#include -#include - -namespace Microsoft::React::Test -{ - -struct WebSocketServiceCallbacks -{ - std::function OnConnection; - std::function OnHandshake; - std::function OnMessage; - std::function MessageFactory; - std::function(std::vector&&)> BinaryMessageFactory; - std::function OnError; -}; - -struct IWebSocketSession -{ - virtual ~IWebSocketSession() {} - - virtual void Start() = 0; -}; - -template -class BaseWebSocketSession : public IWebSocketSession -{ - enum class State : std::size_t { Started, Stopped }; - - boost::beast::multi_buffer m_buffer; - std::string m_message; - std::vector m_binaryMessage; - State m_state; - - std::function m_errorHandler; - - void Read(); - - void OnAccept(boost::system::error_code ec); - void OnHandshake(boost::beast::websocket::response_type& response); - void OnRead(boost::system::error_code ec, std::size_t transferred); - void OnWrite(boost::system::error_code ec, std::size_t transferred); - - protected: - std::shared_ptr> m_stream; - WebSocketServiceCallbacks &m_callbacks; - - void Accept(); - - virtual std::shared_ptr> SharedFromThis() = 0; - - public: - BaseWebSocketSession(WebSocketServiceCallbacks& callbacks); - ~BaseWebSocketSession(); - - virtual void Start() override; -}; - -class WebSocketSession : - public std::enable_shared_from_this, - public BaseWebSocketSession -{ - std::shared_ptr> SharedFromThis() override; - - public: - WebSocketSession(boost::asio::ip::tcp::socket socket, WebSocketServiceCallbacks& callbacks); - ~WebSocketSession(); -}; - -class WebSocketServer : public std::enable_shared_from_this -{ - std::thread m_contextThread; - boost::asio::io_context m_context; - boost::asio::ip::tcp::acceptor m_acceptor; - WebSocketServiceCallbacks m_callbacks; - std::vector> m_sessions; - bool m_useTLS; - - void Accept(); - - void OnAccept(boost::system::error_code ec, boost::asio::ip::tcp::socket socket); - - public: - WebSocketServer(std::uint16_t port, bool useTLS); - WebSocketServer(int port, bool useTLS = false); - - void Start(); - void Stop(); - - void SetOnConnection(std::function&& func); - void SetOnHandshake(std::function&& func); - void SetOnMessage(std::function&& func); - void SetMessageFactory(std::function&& func); - void SetMessageFactory(std::function(std::vector&&)>&& func); - void SetOnError(std::function&& func); -}; - -} // namespace Microsoft::React::Test diff --git a/vnext/TestWebSite/Microsoft/React/WebSocketTests.cs b/vnext/TestWebSite/Microsoft/React/WebSocketTests.cs index 724af9bc75b..678580e0225 100644 --- a/vnext/TestWebSite/Microsoft/React/WebSocketTests.cs +++ b/vnext/TestWebSite/Microsoft/React/WebSocketTests.cs @@ -58,6 +58,95 @@ public static async Task EchoBinary(HttpContext context) } } + public static async Task EchoCookie(HttpContext context) + { + using var ws = await context.WebSockets.AcceptWebSocketAsync(); + wsConnections.Add(ws); + + var cookie = context.Request.Headers.Cookie.ToString(); + + while (true) + { + if (ws.State == WebSocketState.Closed || + ws.State == WebSocketState.CloseSent || + ws.State == WebSocketState.CloseReceived || + ws.State == WebSocketState.Aborted) + break; + + if (ws.State != WebSocketState.Open) + continue; + + try + { + await WebSocketUtils.ReceiveStringAsync(ws); + } + catch (WebSocketException) + { + break; + } + + if (ws.State != WebSocketState.Open) + break; + + var responseBytes = Encoding.UTF8.GetBytes(cookie); + + try + { + await ws.SendAsync(responseBytes, WebSocketMessageType.Text, true, CancellationToken.None); + } + catch (WebSocketException) + { + break; + } + } + } + + public static async Task EchoBinaryGrow(HttpContext context) + { + using var ws = await context.WebSockets.AcceptWebSocketAsync(); + wsConnections.Add(ws); + + while (true) + { + if (ws.State == WebSocketState.Closed || + ws.State == WebSocketState.CloseSent || + ws.State == WebSocketState.CloseReceived || + ws.State == WebSocketState.Aborted) + break; + + if (ws.State != WebSocketState.Open) + continue; + + var inputBuffer = new byte[1024]; + var payload = new List(); + + while (true) + { + var segment = new ArraySegment(inputBuffer); + var receiveResult = await ws.ReceiveAsync(segment, CancellationToken.None); + + if (receiveResult.MessageType == WebSocketMessageType.Close) + { + await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + return; + } + + for (var i = 0; i < receiveResult.Count; i++) + { + payload.Add(inputBuffer[i]); + } + + if (receiveResult.EndOfMessage) + break; + } + + payload.Add((byte)(payload.Count + 1)); + var outputBytes = payload.ToArray(); + + await ws.SendAsync(outputBytes, WebSocketMessageType.Binary, true, CancellationToken.None); + } + } + public static async Task EchoSuffix(HttpContext context) { var announcement = @"This will send each incoming message back, with the string '_response' appended."; diff --git a/vnext/TestWebSite/Program.cs b/vnext/TestWebSite/Program.cs index bf97383c83c..839150e5460 100644 --- a/vnext/TestWebSite/Program.cs +++ b/vnext/TestWebSite/Program.cs @@ -80,6 +80,16 @@ async Task DefaultRequestDelegate(HttpContext context) Microsoft.React.Test.WebSocketTests.EchoSuffix ); +app.Map( + "/rnw/websockets/echocookie", + Microsoft.React.Test.WebSocketTests.EchoCookie + ); + +app.Map( + "/rnw/websockets/echobinarygrow", + Microsoft.React.Test.WebSocketTests.EchoBinaryGrow + ); + app.Map( "/rnw/websockets/pong", Microsoft.React.Test.WebSocketTests.Pong From bbbcf3949b6ad651f1f22302c0df48ff4d707967 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 16 Jun 2026 02:19:23 -0700 Subject: [PATCH 10/13] Keep reference code above AdditionalHeaders --- .../WebSocketIntegrationTest.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/vnext/Desktop.IntegrationTests/WebSocketIntegrationTest.cpp b/vnext/Desktop.IntegrationTests/WebSocketIntegrationTest.cpp index ffb3be1818a..aa6b0de7662 100644 --- a/vnext/Desktop.IntegrationTests/WebSocketIntegrationTest.cpp +++ b/vnext/Desktop.IntegrationTests/WebSocketIntegrationTest.cpp @@ -234,6 +234,22 @@ TEST_CLASS (WebSocketIntegrationTest) Assert::AreEqual(static_cast(LEN + string("_response").length()), result.length()); } + /* + Example: + + server.on('connection', (ws) => { + ws.on('message', (message) => { + for (var propName in ws.upgradeReq.headers) { + console.log(`${propName}: [${ws.upgradeReq.headers[propName]}]`); + } + + // Send the cookie back to the client. + ws.send(ws.upgradeReq.headers.cookie); + }); + }); + + Test passes, otherwise. + */ TEST_METHOD(AdditionalHeaders) { auto ws = IWebSocketResource::Make(); once_flag responseFlag; From b5a658272485bef130ea68df27d4a65a30cb55a4 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 16 Jun 2026 14:51:04 -0700 Subject: [PATCH 11/13] Disable WebSocketIntegrationTest::AdditionalHeaders in CI --- .ado/build-template.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.ado/build-template.yml b/.ado/build-template.yml index f78b0d85af8..6a1e643cf32 100644 --- a/.ado/build-template.yml +++ b/.ado/build-template.yml @@ -279,9 +279,11 @@ extends: value: false #10732 - WebSocketIntegrationTest::SendReceiveSsl fails on Windows Server 2022. #12714 - Disable for first deployment of test website. + #other - WebSocketIntegrationTest::AdditionalHeaders throws '[0x801901f4] Internal server error (500).' - name: Desktop.IntegrationTests.Filter value: > (FullyQualifiedName!~Microsoft::React::Test::RNTesterHeadlessTests)& + (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::AdditionalHeaders)& (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::ConnectClose)& (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::ConnectNoClose)& (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::SendReceiveClose)& From dbf8009fa9fbaa19268026f0cc871b18bcafcd53 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 16 Jun 2026 14:52:11 -0700 Subject: [PATCH 12/13] clang format --- vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp b/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp index 23f8094f4ea..65732de18d0 100644 --- a/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp +++ b/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp @@ -44,10 +44,10 @@ using winrt::Windows::Web::Http::Headers::HttpMediaTypeHeaderValue; namespace { // Clearly fake endpoint host names. These never reach the network (all requests are served by a // mock filter), so they intentionally avoid "localhost" to prevent confusion with a real service. -constexpr wchar_t s_mockServerHost[]{L"mockserver.rnw"}; // Primary endpoint. +constexpr wchar_t s_mockServerHost[]{L"mockserver.rnw"}; // Primary endpoint. constexpr wchar_t s_redirectServerHost[]{L"redirserver.rnw"}; // Secondary endpoint (redirect target). -constexpr char s_crossOriginUrl[]{"http://example.rnw"}; // Narrow form, for runtime options. +constexpr char s_crossOriginUrl[]{"http://example.rnw"}; // Narrow form, for runtime options. constexpr wchar_t s_crossOriginUrlW[]{L"http://example.rnw"}; // Wide form, for header values. // Common header field names (mirror the boost::beast::http::field values used by the original integration test). From a2012ed7dad88cb1fb2f21779b8be0c93dbca3ce Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 16 Jun 2026 16:31:40 -0700 Subject: [PATCH 13/13] Skip all WebSocketIntegrationTest tests on CI --- .ado/build-template.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.ado/build-template.yml b/.ado/build-template.yml index 6a1e643cf32..5674f1d7cd2 100644 --- a/.ado/build-template.yml +++ b/.ado/build-template.yml @@ -280,16 +280,11 @@ extends: #10732 - WebSocketIntegrationTest::SendReceiveSsl fails on Windows Server 2022. #12714 - Disable for first deployment of test website. #other - WebSocketIntegrationTest::AdditionalHeaders throws '[0x801901f4] Internal server error (500).' + # - WebSocketIntegrationTest are currently unsable on CI hosts. Work fine on local machines. - name: Desktop.IntegrationTests.Filter value: > (FullyQualifiedName!~Microsoft::React::Test::RNTesterHeadlessTests)& - (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::AdditionalHeaders)& - (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::ConnectClose)& - (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::ConnectNoClose)& - (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::SendReceiveClose)& - (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::SendConsecutive)& - (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::SendReceiveLargeMessage)& - (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::SendReceiveSsl) + (FullyQualifiedName!~Microsoft::React::Test::WebSocketIntegrationTest) #6799 - HostFunctionTest, HostObjectProtoTest crash under JSI/V8; # PreparedJavaScriptSourceTest asserts/fails under JSI/ChakraCore - name: Desktop.UnitTests.Filter