// Copyright 2014 The Chromium OS 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 <brillo/http/http_form_data.h>

#include <set>

#include <base/files/file_util.h>
#include <base/files/scoped_temp_dir.h>
#include <brillo/mime_utils.h>
#include <brillo/streams/file_stream.h>
#include <brillo/streams/input_stream_set.h>
#include <gtest/gtest.h>

namespace brillo {
namespace http {
namespace {
std::string GetFormFieldData(FormField* field) {
  std::vector<StreamPtr> streams;
  CHECK(field->ExtractDataStreams(&streams));
  StreamPtr stream = InputStreamSet::Create(std::move(streams), nullptr);

  std::vector<uint8_t> data(stream->GetSize());
  EXPECT_TRUE(stream->ReadAllBlocking(data.data(), data.size(), nullptr));
  return std::string{data.begin(), data.end()};
}
}  // anonymous namespace

TEST(HttpFormData, TextFormField) {
  TextFormField form_field{"field1", "abcdefg", mime::text::kPlain, "7bit"};
  const char expected_header[] =
      "Content-Disposition: form-data; name=\"field1\"\r\n"
      "Content-Type: text/plain\r\n"
      "Content-Transfer-Encoding: 7bit\r\n"
      "\r\n";
  EXPECT_EQ(expected_header, form_field.GetContentHeader());
  EXPECT_EQ("abcdefg", GetFormFieldData(&form_field));
}

TEST(HttpFormData, FileFormField) {
  base::ScopedTempDir dir;
  ASSERT_TRUE(dir.CreateUniqueTempDir());
  std::string file_content{"text line1\ntext line2\n"};
  base::FilePath file_name = dir.GetPath().Append("sample.txt");
  ASSERT_EQ(file_content.size(),
            static_cast<size_t>(base::WriteFile(
                file_name, file_content.data(), file_content.size())));

  StreamPtr stream = FileStream::Open(file_name, Stream::AccessMode::READ,
                                      FileStream::Disposition::OPEN_EXISTING,
                                      nullptr);
  ASSERT_NE(nullptr, stream);
  FileFormField form_field{"test_file",
                           std::move(stream),
                           "sample.txt",
                           content_disposition::kFormData,
                           mime::text::kPlain,
                           ""};
  const char expected_header[] =
      "Content-Disposition: form-data; name=\"test_file\";"
      " filename=\"sample.txt\"\r\n"
      "Content-Type: text/plain\r\n"
      "\r\n";
  EXPECT_EQ(expected_header, form_field.GetContentHeader());
  EXPECT_EQ(file_content, GetFormFieldData(&form_field));
}

TEST(HttpFormData, MultiPartFormField) {
  base::ScopedTempDir dir;
  ASSERT_TRUE(dir.CreateUniqueTempDir());
  std::string file1{"text line1\ntext line2\n"};
  base::FilePath filename1 = dir.GetPath().Append("sample.txt");
  ASSERT_EQ(file1.size(),
            static_cast<size_t>(
                base::WriteFile(filename1, file1.data(), file1.size())));
  std::string file2{"\x01\x02\x03\x04\x05"};
  base::FilePath filename2 = dir.GetPath().Append("test.bin");
  ASSERT_EQ(file2.size(),
            static_cast<size_t>(
                base::WriteFile(filename2, file2.data(), file2.size())));

  MultiPartFormField form_field{"foo", mime::multipart::kFormData, "Delimiter"};
  form_field.AddTextField("name", "John Doe");
  EXPECT_TRUE(form_field.AddFileField("file1",
                                      filename1,
                                      content_disposition::kFormData,
                                      mime::text::kPlain,
                                      nullptr));
  EXPECT_TRUE(form_field.AddFileField("file2",
                                      filename2,
                                      content_disposition::kFormData,
                                      mime::application::kOctet_stream,
                                      nullptr));
  const char expected_header[] =
      "Content-Disposition: form-data; name=\"foo\"\r\n"
      "Content-Type: multipart/form-data; boundary=\"Delimiter\"\r\n"
      "\r\n";
  EXPECT_EQ(expected_header, form_field.GetContentHeader());
  const char expected_data[] =
      "--Delimiter\r\n"
      "Content-Disposition: form-data; name=\"name\"\r\n"
      "\r\n"
      "John Doe\r\n"
      "--Delimiter\r\n"
      "Content-Disposition: form-data; name=\"file1\";"
      " filename=\"sample.txt\"\r\n"
      "Content-Type: text/plain\r\n"
      "Content-Transfer-Encoding: binary\r\n"
      "\r\n"
      "text line1\ntext line2\n\r\n"
      "--Delimiter\r\n"
      "Content-Disposition: form-data; name=\"file2\";"
      " filename=\"test.bin\"\r\n"
      "Content-Type: application/octet-stream\r\n"
      "Content-Transfer-Encoding: binary\r\n"
      "\r\n"
      "\x01\x02\x03\x04\x05\r\n"
      "--Delimiter--";
  EXPECT_EQ(expected_data, GetFormFieldData(&form_field));
}

TEST(HttpFormData, MultiPartBoundary) {
  const int count = 10;
  std::set<std::string> boundaries;
  for (int i = 0; i < count; i++) {
    MultiPartFormField field{""};
    std::string boundary = field.GetBoundary();
    boundaries.insert(boundary);
    // Our generated boundary must be 16 character long and contain lowercase
    // hexadecimal digits only.
    EXPECT_EQ(16u, boundary.size());
    EXPECT_EQ(std::string::npos,
              boundary.find_first_not_of("0123456789abcdef"));
  }
  // Now make sure the boundary strings were generated at random, so we should
  // get |count| unique boundary strings. However since the strings are random,
  // there is a very slim change of generating the same string twice, so
  // expect at least 90% of unique strings. 90% is picked arbitrarily here.
  int expected_min_unique = count * 9 / 10;
  EXPECT_GE(boundaries.size(), expected_min_unique);
}

TEST(HttpFormData, FormData) {
  base::ScopedTempDir dir;
  ASSERT_TRUE(dir.CreateUniqueTempDir());
  std::string file1{"text line1\ntext line2\n"};
  base::FilePath filename1 = dir.GetPath().Append("sample.txt");
  ASSERT_EQ(file1.size(),
            static_cast<size_t>(
                base::WriteFile(filename1, file1.data(), file1.size())));
  std::string file2{"\x01\x02\x03\x04\x05"};
  base::FilePath filename2 = dir.GetPath().Append("test.bin");
  ASSERT_EQ(file2.size(),
            static_cast<size_t>(
                base::WriteFile(filename2, file2.data(), file2.size())));

  FormData form_data{"boundary1"};
  form_data.AddTextField("name", "John Doe");
  std::unique_ptr<MultiPartFormField> files{
      new MultiPartFormField{"files", "", "boundary2"}};
  EXPECT_TRUE(files->AddFileField(
      "", filename1, content_disposition::kFile, mime::text::kPlain, nullptr));
  EXPECT_TRUE(files->AddFileField("",
                                  filename2,
                                  content_disposition::kFile,
                                  mime::application::kOctet_stream,
                                  nullptr));
  form_data.AddCustomField(std::move(files));
  EXPECT_EQ("multipart/form-data; boundary=\"boundary1\"",
            form_data.GetContentType());

  StreamPtr stream = form_data.ExtractDataStream();
  std::vector<uint8_t> data(stream->GetSize());
  EXPECT_TRUE(stream->ReadAllBlocking(data.data(), data.size(), nullptr));
  const char expected_data[] =
      "--boundary1\r\n"
      "Content-Disposition: form-data; name=\"name\"\r\n"
      "\r\n"
      "John Doe\r\n"
      "--boundary1\r\n"
      "Content-Disposition: form-data; name=\"files\"\r\n"
      "Content-Type: multipart/mixed; boundary=\"boundary2\"\r\n"
      "\r\n"
      "--boundary2\r\n"
      "Content-Disposition: file; filename=\"sample.txt\"\r\n"
      "Content-Type: text/plain\r\n"
      "Content-Transfer-Encoding: binary\r\n"
      "\r\n"
      "text line1\ntext line2\n\r\n"
      "--boundary2\r\n"
      "Content-Disposition: file; filename=\"test.bin\"\r\n"
      "Content-Type: application/octet-stream\r\n"
      "Content-Transfer-Encoding: binary\r\n"
      "\r\n"
      "\x01\x02\x03\x04\x05\r\n"
      "--boundary2--\r\n"
      "--boundary1--";
  EXPECT_EQ(expected_data, (std::string{data.begin(), data.end()}));
}
}  // namespace http
}  // namespace brillo