// Copyright (c) 2012 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 "chrome/installer/setup/setup_util_unittest.h"

#include <windows.h>

#include <string>

#include "base/command_line.h"
#include "base/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/scoped_ptr.h"
#include "base/process/kill.h"
#include "base/process/launch.h"
#include "base/process/process_handle.h"
#include "base/test/test_reg_util_win.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "base/version.h"
#include "base/win/scoped_handle.h"
#include "base/win/windows_version.h"
#include "chrome/installer/setup/setup_util.h"
#include "chrome/installer/setup/setup_constants.h"
#include "chrome/installer/util/google_update_constants.h"
#include "chrome/installer/util/installation_state.h"
#include "chrome/installer/util/installer_state.h"
#include "chrome/installer/util/util_constants.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

class SetupUtilTestWithDir : public testing::Test {
 protected:
  virtual void SetUp() OVERRIDE {
    // Create a temp directory for testing.
    ASSERT_TRUE(test_dir_.CreateUniqueTempDir());
  }

  virtual void TearDown() OVERRIDE {
    // Clean up test directory manually so we can fail if it leaks.
    ASSERT_TRUE(test_dir_.Delete());
  }

  // The temporary directory used to contain the test operations.
  base::ScopedTempDir test_dir_;
};

// The privilege tested in ScopeTokenPrivilege tests below.
// Use SE_RESTORE_NAME as it is one of the many privileges that is available,
// but not enabled by default on processes running at high integrity.
static const wchar_t kTestedPrivilege[] = SE_RESTORE_NAME;

// Returns true if the current process' token has privilege |privilege_name|
// enabled.
bool CurrentProcessHasPrivilege(const wchar_t* privilege_name) {
  HANDLE temp_handle;
  if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY,
                          &temp_handle)) {
    ADD_FAILURE();
    return false;
  }

  base::win::ScopedHandle token(temp_handle);

  // First get the size of the buffer needed for |privileges| below.
  DWORD size;
  EXPECT_FALSE(::GetTokenInformation(token, TokenPrivileges, NULL, 0, &size));

  scoped_ptr<BYTE[]> privileges_bytes(new BYTE[size]);
  TOKEN_PRIVILEGES* privileges =
      reinterpret_cast<TOKEN_PRIVILEGES*>(privileges_bytes.get());

  if (!::GetTokenInformation(token, TokenPrivileges, privileges, size, &size)) {
    ADD_FAILURE();
    return false;
  }

  // There is no point getting a buffer to store more than |privilege_name|\0 as
  // anything longer will obviously not be equal to |privilege_name|.
  const DWORD desired_size = wcslen(privilege_name);
  const DWORD buffer_size = desired_size + 1;
  scoped_ptr<wchar_t[]> name_buffer(new wchar_t[buffer_size]);
  for (int i = privileges->PrivilegeCount - 1; i >= 0 ; --i) {
    LUID_AND_ATTRIBUTES& luid_and_att = privileges->Privileges[i];
    DWORD size = buffer_size;
    ::LookupPrivilegeName(NULL, &luid_and_att.Luid, name_buffer.get(), &size);
    if (size == desired_size &&
        wcscmp(name_buffer.get(), privilege_name) == 0) {
      return luid_and_att.Attributes == SE_PRIVILEGE_ENABLED;
    }
  }
  return false;
}

}  // namespace

// Test that we are parsing Chrome version correctly.
TEST_F(SetupUtilTestWithDir, GetMaxVersionFromArchiveDirTest) {
  // Create a version dir
  base::FilePath chrome_dir = test_dir_.path().AppendASCII("1.0.0.0");
  base::CreateDirectory(chrome_dir);
  ASSERT_TRUE(base::PathExists(chrome_dir));
  scoped_ptr<Version> version(
      installer::GetMaxVersionFromArchiveDir(test_dir_.path()));
  ASSERT_EQ(version->GetString(), "1.0.0.0");

  base::DeleteFile(chrome_dir, true);
  ASSERT_FALSE(base::PathExists(chrome_dir));
  ASSERT_TRUE(installer::GetMaxVersionFromArchiveDir(test_dir_.path()) == NULL);

  chrome_dir = test_dir_.path().AppendASCII("ABC");
  base::CreateDirectory(chrome_dir);
  ASSERT_TRUE(base::PathExists(chrome_dir));
  ASSERT_TRUE(installer::GetMaxVersionFromArchiveDir(test_dir_.path()) == NULL);

  chrome_dir = test_dir_.path().AppendASCII("2.3.4.5");
  base::CreateDirectory(chrome_dir);
  ASSERT_TRUE(base::PathExists(chrome_dir));
  version.reset(installer::GetMaxVersionFromArchiveDir(test_dir_.path()));
  ASSERT_EQ(version->GetString(), "2.3.4.5");

  // Create multiple version dirs, ensure that we select the greatest.
  chrome_dir = test_dir_.path().AppendASCII("9.9.9.9");
  base::CreateDirectory(chrome_dir);
  ASSERT_TRUE(base::PathExists(chrome_dir));
  chrome_dir = test_dir_.path().AppendASCII("1.1.1.1");
  base::CreateDirectory(chrome_dir);
  ASSERT_TRUE(base::PathExists(chrome_dir));

  version.reset(installer::GetMaxVersionFromArchiveDir(test_dir_.path()));
  ASSERT_EQ(version->GetString(), "9.9.9.9");
}

TEST_F(SetupUtilTestWithDir, DeleteFileFromTempProcess) {
  base::FilePath test_file;
  base::CreateTemporaryFileInDir(test_dir_.path(), &test_file);
  ASSERT_TRUE(base::PathExists(test_file));
  base::WriteFile(test_file, "foo", 3);
  EXPECT_TRUE(installer::DeleteFileFromTempProcess(test_file, 0));
  base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(200));
  EXPECT_FALSE(base::PathExists(test_file));
}

// Note: This test is only valid when run at high integrity (i.e. it will fail
// at medium integrity).
TEST(SetupUtilTest, ScopedTokenPrivilegeBasic) {
  ASSERT_FALSE(CurrentProcessHasPrivilege(kTestedPrivilege));

  {
    installer::ScopedTokenPrivilege test_scoped_privilege(kTestedPrivilege);
    ASSERT_TRUE(test_scoped_privilege.is_enabled());
    ASSERT_TRUE(CurrentProcessHasPrivilege(kTestedPrivilege));
  }

  ASSERT_FALSE(CurrentProcessHasPrivilege(kTestedPrivilege));
}

// Note: This test is only valid when run at high integrity (i.e. it will fail
// at medium integrity).
TEST(SetupUtilTest, ScopedTokenPrivilegeAlreadyEnabled) {
  ASSERT_FALSE(CurrentProcessHasPrivilege(kTestedPrivilege));

  {
    installer::ScopedTokenPrivilege test_scoped_privilege(kTestedPrivilege);
    ASSERT_TRUE(test_scoped_privilege.is_enabled());
    ASSERT_TRUE(CurrentProcessHasPrivilege(kTestedPrivilege));
    {
      installer::ScopedTokenPrivilege dup_scoped_privilege(kTestedPrivilege);
      ASSERT_TRUE(dup_scoped_privilege.is_enabled());
      ASSERT_TRUE(CurrentProcessHasPrivilege(kTestedPrivilege));
    }
    ASSERT_TRUE(CurrentProcessHasPrivilege(kTestedPrivilege));
  }

  ASSERT_FALSE(CurrentProcessHasPrivilege(kTestedPrivilege));
}

const char kAdjustProcessPriority[] = "adjust-process-priority";

PriorityClassChangeResult DoProcessPriorityAdjustment() {
  return installer::AdjustProcessPriority() ? PCCR_CHANGED : PCCR_UNCHANGED;
}

namespace {

// A scoper that sets/resets the current process's priority class.
class ScopedPriorityClass {
 public:
  // Applies |priority_class|, returning an instance if a change was made.
  // Otherwise, returns an empty scoped_ptr.
  static scoped_ptr<ScopedPriorityClass> Create(DWORD priority_class);
  ~ScopedPriorityClass();

 private:
  explicit ScopedPriorityClass(DWORD original_priority_class);
  DWORD original_priority_class_;
  DISALLOW_COPY_AND_ASSIGN(ScopedPriorityClass);
};

scoped_ptr<ScopedPriorityClass> ScopedPriorityClass::Create(
    DWORD priority_class) {
  HANDLE this_process = ::GetCurrentProcess();
  DWORD original_priority_class = ::GetPriorityClass(this_process);
  EXPECT_NE(0U, original_priority_class);
  if (original_priority_class && original_priority_class != priority_class) {
    BOOL result = ::SetPriorityClass(this_process, priority_class);
    EXPECT_NE(FALSE, result);
    if (result) {
      return scoped_ptr<ScopedPriorityClass>(
          new ScopedPriorityClass(original_priority_class));
    }
  }
  return scoped_ptr<ScopedPriorityClass>();
}

ScopedPriorityClass::ScopedPriorityClass(DWORD original_priority_class)
    : original_priority_class_(original_priority_class) {}

ScopedPriorityClass::~ScopedPriorityClass() {
  BOOL result = ::SetPriorityClass(::GetCurrentProcess(),
                                   original_priority_class_);
  EXPECT_NE(FALSE, result);
}

PriorityClassChangeResult RelaunchAndDoProcessPriorityAdjustment() {
  CommandLine cmd_line(*CommandLine::ForCurrentProcess());
  cmd_line.AppendSwitch(kAdjustProcessPriority);
  base::ProcessHandle process_handle = NULL;
  int exit_code = 0;
  if (!base::LaunchProcess(cmd_line, base::LaunchOptions(),
                           &process_handle)) {
    ADD_FAILURE() << " to launch subprocess.";
  } else if (!base::WaitForExitCode(process_handle, &exit_code)) {
    ADD_FAILURE() << " to wait for subprocess to exit.";
  } else {
    return static_cast<PriorityClassChangeResult>(exit_code);
  }
  return PCCR_UNKNOWN;
}

}  // namespace

// Launching a subprocess at normal priority class is a noop.
TEST(SetupUtilTest, AdjustFromNormalPriority) {
  ASSERT_EQ(NORMAL_PRIORITY_CLASS, ::GetPriorityClass(::GetCurrentProcess()));
  EXPECT_EQ(PCCR_UNCHANGED, RelaunchAndDoProcessPriorityAdjustment());
}

// Launching a subprocess below normal priority class drops it to bg mode for
// sufficiently recent operating systems.
TEST(SetupUtilTest, AdjustFromBelowNormalPriority) {
  scoped_ptr<ScopedPriorityClass> below_normal =
      ScopedPriorityClass::Create(BELOW_NORMAL_PRIORITY_CLASS);
  ASSERT_TRUE(below_normal);
  if (base::win::GetVersion() > base::win::VERSION_SERVER_2003)
    EXPECT_EQ(PCCR_CHANGED, RelaunchAndDoProcessPriorityAdjustment());
  else
    EXPECT_EQ(PCCR_UNCHANGED, RelaunchAndDoProcessPriorityAdjustment());
}

namespace {

// A test fixture that configures an InstallationState and an InstallerState
// with a product being updated.
class FindArchiveToPatchTest : public SetupUtilTestWithDir {
 protected:
  class FakeInstallationState : public installer::InstallationState {
  };

  class FakeProductState : public installer::ProductState {
   public:
    static FakeProductState* FromProductState(const ProductState* product) {
      return static_cast<FakeProductState*>(const_cast<ProductState*>(product));
    }

    void set_version(const Version& version) {
      if (version.IsValid())
        version_.reset(new Version(version));
      else
        version_.reset();
    }

    void set_uninstall_command(const CommandLine& uninstall_command) {
      uninstall_command_ = uninstall_command;
    }
  };

  virtual void SetUp() OVERRIDE {
    SetupUtilTestWithDir::SetUp();
    product_version_ = Version("30.0.1559.0");
    max_version_ = Version("47.0.1559.0");

    // Install the product according to the version.
    original_state_.reset(new FakeInstallationState());
    InstallProduct();

    // Prepare to update the product in the temp dir.
    installer_state_.reset(new installer::InstallerState(
        kSystemInstall_ ? installer::InstallerState::SYSTEM_LEVEL :
        installer::InstallerState::USER_LEVEL));
    installer_state_->AddProductFromState(
        kProductType_,
        *original_state_->GetProductState(kSystemInstall_, kProductType_));

    // Create archives in the two version dirs.
    ASSERT_TRUE(
        base::CreateDirectory(GetProductVersionArchivePath().DirName()));
    ASSERT_EQ(1, base::WriteFile(GetProductVersionArchivePath(), "a", 1));
    ASSERT_TRUE(
        base::CreateDirectory(GetMaxVersionArchivePath().DirName()));
    ASSERT_EQ(1, base::WriteFile(GetMaxVersionArchivePath(), "b", 1));
  }

  virtual void TearDown() OVERRIDE {
    original_state_.reset();
    SetupUtilTestWithDir::TearDown();
  }

  base::FilePath GetArchivePath(const Version& version) const {
    return test_dir_.path()
        .AppendASCII(version.GetString())
        .Append(installer::kInstallerDir)
        .Append(installer::kChromeArchive);
  }

  base::FilePath GetMaxVersionArchivePath() const {
    return GetArchivePath(max_version_);
  }

  base::FilePath GetProductVersionArchivePath() const {
    return GetArchivePath(product_version_);
  }

  void InstallProduct() {
    FakeProductState* product = FakeProductState::FromProductState(
        original_state_->GetNonVersionedProductState(kSystemInstall_,
                                                     kProductType_));

    product->set_version(product_version_);
    CommandLine uninstall_command(
        test_dir_.path().AppendASCII(product_version_.GetString())
        .Append(installer::kInstallerDir)
        .Append(installer::kSetupExe));
    uninstall_command.AppendSwitch(installer::switches::kUninstall);
    product->set_uninstall_command(uninstall_command);
  }

  void UninstallProduct() {
    FakeProductState::FromProductState(
        original_state_->GetNonVersionedProductState(kSystemInstall_,
                                                     kProductType_))
        ->set_version(Version());
  }

  static const bool kSystemInstall_;
  static const BrowserDistribution::Type kProductType_;
  Version product_version_;
  Version max_version_;
  scoped_ptr<FakeInstallationState> original_state_;
  scoped_ptr<installer::InstallerState> installer_state_;
};

const bool FindArchiveToPatchTest::kSystemInstall_ = false;
const BrowserDistribution::Type FindArchiveToPatchTest::kProductType_ =
    BrowserDistribution::CHROME_BROWSER;

}  // namespace

// Test that the path to the advertised product version is found.
TEST_F(FindArchiveToPatchTest, ProductVersionFound) {
  base::FilePath patch_source(installer::FindArchiveToPatch(
      *original_state_, *installer_state_));
  EXPECT_EQ(GetProductVersionArchivePath().value(), patch_source.value());
}

// Test that the path to the max version is found if the advertised version is
// missing.
TEST_F(FindArchiveToPatchTest, MaxVersionFound) {
  // The patch file is absent.
  ASSERT_TRUE(base::DeleteFile(GetProductVersionArchivePath(), false));
  base::FilePath patch_source(installer::FindArchiveToPatch(
      *original_state_, *installer_state_));
  EXPECT_EQ(GetMaxVersionArchivePath().value(), patch_source.value());

  // The product doesn't appear to be installed, so the max version is found.
  UninstallProduct();
  patch_source = installer::FindArchiveToPatch(
      *original_state_, *installer_state_);
  EXPECT_EQ(GetMaxVersionArchivePath().value(), patch_source.value());
}

// Test that an empty path is returned if no version is found.
TEST_F(FindArchiveToPatchTest, NoVersionFound) {
  // The product doesn't appear to be installed and no archives are present.
  UninstallProduct();
  ASSERT_TRUE(base::DeleteFile(GetProductVersionArchivePath(), false));
  ASSERT_TRUE(base::DeleteFile(GetMaxVersionArchivePath(), false));

  base::FilePath patch_source(installer::FindArchiveToPatch(
      *original_state_, *installer_state_));
  EXPECT_EQ(base::FilePath::StringType(), patch_source.value());
}

namespace {

class MigrateMultiToSingleTest : public testing::Test {
 protected:
  virtual void SetUp() OVERRIDE {
    registry_override_manager_.OverrideRegistry(kRootKey,
                                                L"MigrateMultiToSingleTest");
  }

  static const bool kSystemLevel = false;
  static const HKEY kRootKey;
  static const wchar_t kVersionString[];
  static const wchar_t kMultiChannel[];
  registry_util::RegistryOverrideManager registry_override_manager_;
};

const bool MigrateMultiToSingleTest::kSystemLevel;
const HKEY MigrateMultiToSingleTest::kRootKey =
    kSystemLevel ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER;
const wchar_t MigrateMultiToSingleTest::kVersionString[] = L"30.0.1574.0";
const wchar_t MigrateMultiToSingleTest::kMultiChannel[] =
    L"2.0-dev-multi-chromeframe";

}  // namespace

// Test migrating Chrome Frame from multi to single.
TEST_F(MigrateMultiToSingleTest, ChromeFrame) {
  installer::ProductState chrome_frame;
  installer::ProductState binaries;
  DWORD usagestats = 0;

  // Set up a config with dev-channel multi-install GCF.
  base::win::RegKey key;

  BrowserDistribution* dist = BrowserDistribution::GetSpecificDistribution(
      BrowserDistribution::CHROME_BINARIES);
  ASSERT_EQ(ERROR_SUCCESS,
            base::win::RegKey(kRootKey, dist->GetVersionKey().c_str(),
                              KEY_SET_VALUE)
                .WriteValue(google_update::kRegVersionField, kVersionString));
  ASSERT_EQ(ERROR_SUCCESS,
            base::win::RegKey(kRootKey, dist->GetStateKey().c_str(),
                              KEY_SET_VALUE)
                .WriteValue(google_update::kRegApField, kMultiChannel));
  ASSERT_EQ(ERROR_SUCCESS,
            base::win::RegKey(kRootKey, dist->GetStateKey().c_str(),
                              KEY_SET_VALUE)
                .WriteValue(google_update::kRegUsageStatsField, 1U));

  dist = BrowserDistribution::GetSpecificDistribution(
      BrowserDistribution::CHROME_FRAME);
  ASSERT_EQ(ERROR_SUCCESS,
            base::win::RegKey(kRootKey, dist->GetVersionKey().c_str(),
                              KEY_SET_VALUE)
                .WriteValue(google_update::kRegVersionField, kVersionString));
  ASSERT_EQ(ERROR_SUCCESS,
            base::win::RegKey(kRootKey, dist->GetStateKey().c_str(),
                              KEY_SET_VALUE)
                .WriteValue(google_update::kRegApField, kMultiChannel));

  // Do the registry migration.
  installer::InstallationState machine_state;
  machine_state.Initialize();

  installer::MigrateGoogleUpdateStateMultiToSingle(
      kSystemLevel,
      BrowserDistribution::CHROME_FRAME,
      machine_state);

  // Confirm that usagestats were copied to CF and that its channel was
  // stripped.
  ASSERT_TRUE(chrome_frame.Initialize(kSystemLevel,
                                      BrowserDistribution::CHROME_FRAME));
  EXPECT_TRUE(chrome_frame.GetUsageStats(&usagestats));
  EXPECT_EQ(1U, usagestats);
  EXPECT_EQ(L"2.0-dev", chrome_frame.channel().value());

  // Confirm that the binaries' channel no longer contains GCF.
  ASSERT_TRUE(binaries.Initialize(kSystemLevel,
                                  BrowserDistribution::CHROME_BINARIES));
  EXPECT_EQ(L"2.0-dev-multi", binaries.channel().value());
}

TEST(SetupUtilTest, ContainsUnsupportedSwitch) {
  EXPECT_FALSE(installer::ContainsUnsupportedSwitch(
      CommandLine::FromString(L"foo.exe")));
  EXPECT_FALSE(installer::ContainsUnsupportedSwitch(
      CommandLine::FromString(L"foo.exe --multi-install --chrome")));
  EXPECT_TRUE(installer::ContainsUnsupportedSwitch(
      CommandLine::FromString(L"foo.exe --chrome-frame")));
}