// 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 "third_party/blink/public/common/features.h"
#include "url/gurl.h"

namespace content {

namespace {
enum class ServicifiedFeatures { kNone, kServiceWorker };

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 ::testing::WithParamInterface<ServicifiedFeatures> {
 public:
  ServiceWorkerFileUploadTest() = default;

  void SetUp() override {
    ServicifiedFeatures param = GetParam();
    switch (param) {
      case ServicifiedFeatures::kNone:
        scoped_feature_list_.InitAndDisableFeature(
            blink::features::kServiceWorkerServicification);
        break;
      case ServicifiedFeatures::kServiceWorker:
        scoped_feature_list_.InitAndEnableFeature(
            blink::features::kServiceWorkerServicification);
        break;
    }

    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::ParseJson(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::ParseJson(expectation));
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;

  DISALLOW_COPY_AND_ASSIGN(ServiceWorkerFileUploadTest);
};

// Tests using Request.text().
IN_PROC_BROWSER_TEST_P(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_P(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_P(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_P(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_P(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_P(ServiceWorkerFileUploadTest,
                       NetworkFallback_CrossOrigin) {
  RunNetworkFallbackTest(TargetOrigin::kCrossOrigin);
}

INSTANTIATE_TEST_CASE_P(/* no prefix */,
                        ServiceWorkerFileUploadTest,
                        ::testing::Values(ServicifiedFeatures::kNone,
                                          ServicifiedFeatures::kServiceWorker));

}  // namespace content
