//
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#include "update_engine/libcurl_http_fetcher.h"

#include <string>

#include <brillo/message_loops/fake_message_loop.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include "update_engine/common/fake_hardware.h"
#include "update_engine/mock_libcurl_http_fetcher.h"

using std::string;

namespace chromeos_update_engine {

namespace {

constexpr char kHeaderName[] = "X-Goog-Test-Header";

class LibcurlHttpFetcherTest : public ::testing::Test {
 protected:
  void SetUp() override {
    loop_.SetAsCurrent();
    fake_hardware_.SetIsOfficialBuild(true);
    fake_hardware_.SetIsOOBEEnabled(false);
  }

  brillo::FakeMessageLoop loop_{nullptr};
  FakeHardware fake_hardware_;
  MockLibcurlHttpFetcher libcurl_fetcher_{&fake_hardware_};
  UnresolvedHostStateMachine state_machine_;
};

}  // namespace

TEST_F(LibcurlHttpFetcherTest, GetEmptyHeaderValueTest) {
  const string header_value = "";
  string actual_header_value;
  libcurl_fetcher_.SetHeader(kHeaderName, header_value);
  EXPECT_TRUE(libcurl_fetcher_.GetHeader(kHeaderName, &actual_header_value));
  EXPECT_EQ("", actual_header_value);
}

TEST_F(LibcurlHttpFetcherTest, GetHeaderTest) {
  const string header_value = "This-is-value 123";
  string actual_header_value;
  libcurl_fetcher_.SetHeader(kHeaderName, header_value);
  EXPECT_TRUE(libcurl_fetcher_.GetHeader(kHeaderName, &actual_header_value));
  EXPECT_EQ(header_value, actual_header_value);
}

TEST_F(LibcurlHttpFetcherTest, GetNonExistentHeaderValueTest) {
  string actual_header_value;
  // Skip |SetHeaader()| call.
  EXPECT_FALSE(libcurl_fetcher_.GetHeader(kHeaderName, &actual_header_value));
  // Even after a failed |GetHeaderValue()|, enforce that the passed pointer to
  // modifiable string was cleared to be empty.
  EXPECT_EQ("", actual_header_value);
}

TEST_F(LibcurlHttpFetcherTest, GetHeaderEdgeCaseTest) {
  const string header_value = "\a\b\t\v\f\r\\ edge:-case: \a\b\t\v\f\r\\";
  string actual_header_value;
  libcurl_fetcher_.SetHeader(kHeaderName, header_value);
  EXPECT_TRUE(libcurl_fetcher_.GetHeader(kHeaderName, &actual_header_value));
  EXPECT_EQ(header_value, actual_header_value);
}

TEST_F(LibcurlHttpFetcherTest, InvalidURLTest) {
  int no_network_max_retries = 1;
  libcurl_fetcher_.set_no_network_max_retries(no_network_max_retries);

  libcurl_fetcher_.BeginTransfer("not-a-URL");
  while (loop_.PendingTasks()) {
    loop_.RunOnce(true);
  }

  EXPECT_EQ(libcurl_fetcher_.get_no_network_max_retries(),
            no_network_max_retries);
}

TEST_F(LibcurlHttpFetcherTest, CouldNotResolveHostTest) {
  int no_network_max_retries = 1;
  libcurl_fetcher_.set_no_network_max_retries(no_network_max_retries);

  libcurl_fetcher_.BeginTransfer("https://An-uNres0lvable-uRl.invalid");

  // It's slower on Android that libcurl handle may not finish within 1 cycle.
  // Will need to wait for more cycles until it finishes. Original test didn't
  // correctly handle when we need to re-watch libcurl fds.
  while (loop_.PendingTasks() &&
         libcurl_fetcher_.GetAuxiliaryErrorCode() == ErrorCode::kSuccess) {
    loop_.RunOnce(true);
  }

  EXPECT_EQ(libcurl_fetcher_.GetAuxiliaryErrorCode(),
            ErrorCode::kUnresolvedHostError);

  while (loop_.PendingTasks()) {
    loop_.RunOnce(true);
  }
  // The auxilary error code should've have been changed.
  EXPECT_EQ(libcurl_fetcher_.GetAuxiliaryErrorCode(),
            ErrorCode::kUnresolvedHostError);

  // If libcurl fails to resolve the name, we call res_init() to reload
  // resolv.conf and retry exactly once more. See crbug.com/982813 for details.
  EXPECT_EQ(libcurl_fetcher_.get_no_network_max_retries(),
            no_network_max_retries + 1);
}

TEST_F(LibcurlHttpFetcherTest, HostResolvedTest) {
  int no_network_max_retries = 2;
  libcurl_fetcher_.set_no_network_max_retries(no_network_max_retries);

  // This test actually sends request to internet but according to
  // https://tools.ietf.org/html/rfc2606#section-2, .invalid domain names are
  // reserved and sure to be invalid. Ideally we should mock libcurl or
  // reorganize LibcurlHttpFetcher so the part that sends request can be mocked
  // easily.
  // TODO(xiaochu) Refactor LibcurlHttpFetcher (and its relates) so it's
  // easier to mock the part that depends on internet connectivity.
  libcurl_fetcher_.BeginTransfer("https://An-uNres0lvable-uRl.invalid");

  // It's slower on Android that libcurl handle may not finish within 1 cycle.
  // Will need to wait for more cycles until it finishes. Original test didn't
  // correctly handle when we need to re-watch libcurl fds.
  while (loop_.PendingTasks() &&
         libcurl_fetcher_.GetAuxiliaryErrorCode() == ErrorCode::kSuccess) {
    loop_.RunOnce(true);
  }

  EXPECT_EQ(libcurl_fetcher_.GetAuxiliaryErrorCode(),
            ErrorCode::kUnresolvedHostError);

  // The second time, it will resolve, with error code 200 but we set the
  // download size be smaller than the transfer size so it will retry again.
  EXPECT_CALL(libcurl_fetcher_, GetHttpResponseCode())
      .WillOnce(testing::Invoke(
          [this]() { libcurl_fetcher_.http_response_code_ = 200; }))
      .WillRepeatedly(testing::Invoke(
          [this]() { libcurl_fetcher_.http_response_code_ = 0; }));
  libcurl_fetcher_.transfer_size_ = 10;

  // It's slower on Android that libcurl handle may not finish within 1 cycle.
  // Will need to wait for more cycles until it finishes. Original test didn't
  // correctly handle when we need to re-watch libcurl fds.
  while (loop_.PendingTasks() && libcurl_fetcher_.GetAuxiliaryErrorCode() ==
                                     ErrorCode::kUnresolvedHostError) {
    loop_.RunOnce(true);
  }

  EXPECT_EQ(libcurl_fetcher_.GetAuxiliaryErrorCode(),
            ErrorCode::kUnresolvedHostRecovered);

  while (loop_.PendingTasks()) {
    loop_.RunOnce(true);
  }
  // The auxilary error code should not have been changed.
  EXPECT_EQ(libcurl_fetcher_.GetAuxiliaryErrorCode(),
            ErrorCode::kUnresolvedHostRecovered);

  // If libcurl fails to resolve the name, we call res_init() to reload
  // resolv.conf and retry exactly once more. See crbug.com/982813 for details.
  EXPECT_EQ(libcurl_fetcher_.get_no_network_max_retries(),
            no_network_max_retries + 1);
}

TEST_F(LibcurlHttpFetcherTest, HttpFetcherStateMachineRetryFailedTest) {
  state_machine_.UpdateState(true);
  state_machine_.UpdateState(true);
  EXPECT_EQ(state_machine_.GetState(),
            UnresolvedHostStateMachine::State::kNotRetry);
}

TEST_F(LibcurlHttpFetcherTest, HttpFetcherStateMachineRetrySucceedTest) {
  state_machine_.UpdateState(true);
  state_machine_.UpdateState(false);
  EXPECT_EQ(state_machine_.GetState(),
            UnresolvedHostStateMachine::State::kRetriedSuccess);
}

TEST_F(LibcurlHttpFetcherTest, HttpFetcherStateMachineNoRetryTest) {
  state_machine_.UpdateState(false);
  state_machine_.UpdateState(false);
  EXPECT_EQ(state_machine_.GetState(),
            UnresolvedHostStateMachine::State::kInit);
}

}  // namespace chromeos_update_engine
