blob: dcd524688b9e81fc19ddee8ea0e9f397b5c43e16 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/bind.h"
#include "base/files/scoped_temp_dir.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/values_test_util.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/shell/browser/shell_content_browser_client.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/base/escape.h"
#include "net/base/url_util.h"
#include "net/dns/mock_host_resolver.h"
#include "net/ssl/ssl_server_config.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "url/gurl.h"
namespace content {
namespace {
void GetKey(const base::DictionaryValue& dict,
const std::string& key,
std::string* out_value) {
auto* value = dict.FindKeyOfType(key, base::Value::Type::STRING);
ASSERT_TRUE(value);
*out_value = value->GetString();
}
void GetKey(const base::DictionaryValue& dict,
const std::string& key,
int* out_value) {
auto* value = dict.FindKeyOfType(key, base::Value::Type::INTEGER);
ASSERT_TRUE(value);
*out_value = value->GetInt();
}
// Helper since the default output of EXPECT_EQ isn't useful when debugging
// failures, it doesn't recurse into the dictionary.
void ExpectEqual(const base::DictionaryValue& expected,
const base::DictionaryValue& actual) {
EXPECT_EQ(expected, actual)
<< "\nExpected: " << expected << "\nActual: " << actual;
}
const char kFileContent[] = "uploaded file content";
const size_t kFileSize = base::size(kFileContent) - 1;
} // namespace
// Tests POST requests that include a file and are intercepted by a service
// worker. This is a browser test rather than a web test because as
// https://crbug.com/786510 describes, http tests involving file uploads usually
// need to be in the http/tests/local directory, which runs tests from file:
// URLs while serving http resources from the http server, but this trick can
// break the test when Site Isolation is enabled and content from different
// origins end up in different processes.
class ServiceWorkerFileUploadTest : public ContentBrowserTest {
public:
ServiceWorkerFileUploadTest() = default;
void SetUp() override {
ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
ContentBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
// Make all hosts resolve to 127.0.0.1 so the same embedded test server can
// be used for cross-origin URLs.
host_resolver()->AddRule("*", "127.0.0.1");
embedded_test_server()->StartAcceptingConnections();
}
enum class TargetOrigin { kSameOrigin, kCrossOrigin };
// Builds a target URL for the form to submit to. The URL has path
// "|path|?|query|" so they can be adjusted to tell the service worker how to
// handle the request.
GURL BuildTargetUrl(const std::string& path, const std::string& query) {
return embedded_test_server()->GetURL(path + "?" + query);
}
// Tests submitting a form that is intercepted by a service worker. The form
// has several input elements including a file; this test creates a temp file
// and uploads it. The service worker is expected to respond with JSON
// describing the request data it saw.
// - |target_url|: where to submit the form to.
// - |target_origin|: whether to submit the form from a page that is
// cross-origin to the target.
// - |out_file_name|: the name of the file this test uploaded via the form.
// - |out_result|: the body of the resulting document.
void RunTest(const GURL& target_url,
TargetOrigin target_origin,
std::string* out_file_name,
std::string* out_result) {
// Install the service worker. Use root scope since the network fallback
// test needs it: the service worker will intercept "/echo", then fall back
// to network, and the request gets handled by the default request handler
// for that URL which echoes back the request.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL(
"/service_worker/create_service_worker.html")));
EXPECT_EQ("DONE",
EvalJs(shell(), "register('file_upload_worker.js', '/');"));
// Generate the URL for the page with the file upload form.
GURL page_url = embedded_test_server()->GetURL("/service_worker/form.html");
// If |target_origin| says to test submitting to a cross-origin target, set
// this page to a different origin. The |target_url| is expected to point
// back to the original origin with the service worker.
if (target_origin == TargetOrigin::kCrossOrigin) {
GURL::Replacements replacements;
replacements.SetHostStr("cross-origin.example.com");
page_url = page_url.ReplaceComponents(replacements);
}
// Set the target to |target_url|.
page_url = net::AppendQueryParameter(page_url, "target", target_url.spec());
// Navigate to the page with a file upload form.
EXPECT_TRUE(NavigateToURL(shell(), page_url));
// Prepare a file for the upload form.
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir temp_dir;
base::FilePath file_path;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path));
ASSERT_EQ(static_cast<int>(kFileSize),
base::WriteFile(file_path, kFileContent, kFileSize));
// Fill out the form to refer to the test file.
base::RunLoop run_loop;
auto delegate = std::make_unique<FileChooserDelegate>(
file_path, run_loop.QuitClosure());
shell()->web_contents()->SetDelegate(delegate.get());
EXPECT_TRUE(ExecJs(shell(), "fileInput.click();"));
run_loop.Run();
// Submit the form.
TestNavigationObserver form_post_observer(shell()->web_contents(), 1);
EXPECT_TRUE(ExecJs(shell(), "form.submit();"));
form_post_observer.Wait();
// Extract the body payload.
EvalJsResult result = EvalJs(shell()->web_contents()->GetMainFrame(),
"document.body.textContent");
ASSERT_TRUE(result.error.empty());
*out_file_name = file_path.BaseName().MaybeAsASCII();
*out_result = result.ExtractString();
}
// Helper for tests where the service worker calls respondWith().
void RunRespondWithTest(const std::string& target_query,
TargetOrigin target_origin,
std::string* out_filename,
std::unique_ptr<base::DictionaryValue>* out_result) {
std::string result;
RunTest(BuildTargetUrl("/service_worker/upload", target_query),
TargetOrigin::kSameOrigin, out_filename, &result);
*out_result =
base::DictionaryValue::From(base::test::ParseJsonDeprecated(result));
ASSERT_TRUE(*out_result);
}
// Helper for tests where the service worker falls back to network.
void RunNetworkFallbackTest(TargetOrigin target_origin) {
std::string filename;
std::string result;
// Use "/echo" so the request hits the echo default handler after falling
// back to network.
// Use "getAs=fallback" to tell the service worker to fall back.
RunTest(BuildTargetUrl("/echo", "getAs=fallback"), target_origin, &filename,
&result);
// This isn't as rigorous as BuildExpectedBodyAsFormData(). The test author
// couldn't get that to work maybe because \r\n get stripped somewhere.
EXPECT_THAT(result, ::testing::HasSubstr(kFileContent));
EXPECT_THAT(result, ::testing::HasSubstr(filename));
EXPECT_THAT(result, ::testing::HasSubstr("form-data; name=\"file\""));
}
std::string BuildExpectedBodyAsText(const std::string& boundary,
const std::string& filename) {
return "--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"text1\"\r\n" + "\r\n" +
"textValue1\r\n" + "--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"text2\"\r\n" + "\r\n" +
"textValue2\r\n" + "--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"file\"; "
"filename=\"" +
filename + "\"\r\n" + "Content-Type: application/octet-stream\r\n" +
"\r\n" + kFileContent + "\r\n" + "--" + boundary + "--\r\n";
}
std::unique_ptr<base::DictionaryValue> BuildExpectedBodyAsFormData(
const std::string& filename) {
std::string expectation = R"({
"entries": [
{
"key": "text1",
"value": {
"type": "string",
"data": "textValue1"
}
},
{
"key": "text2",
"value": {
"type": "string",
"data": "textValue2"
}
},
{
"key": "file",
"value": {
"type": "file",
"name": "@PATH@",
"size": @SIZE@
}
}
]
})";
base::ReplaceFirstSubstringAfterOffset(&expectation, 0, "@PATH@", filename);
base::ReplaceFirstSubstringAfterOffset(&expectation, 0, "@SIZE@",
base::NumberToString(kFileSize));
return base::DictionaryValue::From(
base::test::ParseJsonDeprecated(expectation));
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
DISALLOW_COPY_AND_ASSIGN(ServiceWorkerFileUploadTest);
};
// Tests using Request.text().
IN_PROC_BROWSER_TEST_F(ServiceWorkerFileUploadTest, AsText) {
std::string filename;
std::unique_ptr<base::DictionaryValue> dict;
RunRespondWithTest("getAs=text", TargetOrigin::kSameOrigin, &filename, &dict);
std::string boundary;
GetKey(*dict, "boundary", &boundary);
std::string body;
GetKey(*dict, "body", &body);
std::string expected_body = BuildExpectedBodyAsText(boundary, filename);
EXPECT_EQ(expected_body, body);
}
// Tests using Request.blob().
IN_PROC_BROWSER_TEST_F(ServiceWorkerFileUploadTest, AsBlob) {
std::string filename;
std::unique_ptr<base::DictionaryValue> dict;
RunRespondWithTest("getAs=blob", TargetOrigin::kSameOrigin, &filename, &dict);
std::string boundary;
GetKey(*dict, "boundary", &boundary);
int size;
GetKey(*dict, "bodySize", &size);
std::string expected_body = BuildExpectedBodyAsText(boundary, filename);
EXPECT_EQ(base::MakeStrictNum(expected_body.size()), size);
}
// Tests using Request.formData().
IN_PROC_BROWSER_TEST_F(ServiceWorkerFileUploadTest, AsFormData) {
std::string filename;
std::unique_ptr<base::DictionaryValue> dict;
RunRespondWithTest("getAs=formData", TargetOrigin::kSameOrigin, &filename,
&dict);
ExpectEqual(*BuildExpectedBodyAsFormData(filename), *dict);
}
// Tests network fallback.
IN_PROC_BROWSER_TEST_F(ServiceWorkerFileUploadTest, NetworkFallback) {
RunNetworkFallbackTest(TargetOrigin::kSameOrigin);
}
// Tests using Request.formData() when the form was submitted to a cross-origin
// target. Regression test for https://crbug.com/916070.
IN_PROC_BROWSER_TEST_F(ServiceWorkerFileUploadTest, AsFormData_CrossOrigin) {
std::string filename;
std::unique_ptr<base::DictionaryValue> dict;
RunRespondWithTest("getAs=formData", TargetOrigin::kCrossOrigin, &filename,
&dict);
ExpectEqual(*BuildExpectedBodyAsFormData(filename), *dict);
}
// Tests network fallback when the form was submitted to a cross-origin target.
IN_PROC_BROWSER_TEST_F(ServiceWorkerFileUploadTest,
NetworkFallback_CrossOrigin) {
RunNetworkFallbackTest(TargetOrigin::kCrossOrigin);
}
} // namespace content