chromium/chrome/browser/media/android/cdm/media_drm_origin_id_manager_unittest.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/media/android/cdm/media_drm_origin_id_manager.h"

#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_string_value_serializer.h"
#include "base/json/values_util.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/unguessable_token.h"
#include "chrome/browser/media/android/cdm/media_drm_origin_id_manager_factory.h"
#include "chrome/test/base/testing_profile.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/test/browser_task_environment.h"
#include "media/base/android/media_drm_bridge.h"
#include "media/base/media_switches.h"
#include "services/network/test/test_network_connection_tracker.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace {

using testing::InvokeWithoutArgs;
using testing::Return;
using MediaDrmOriginId = MediaDrmOriginIdManager::MediaDrmOriginId;

// These values must match the values specified for the implementation
// in media_drm_origin_id_manager.cc.
const char kMediaDrmOriginIds[] = "media.media_drm_origin_ids";
const char kExpirableToken[] = "expirable_token";
const char kAvailableOriginIds[] = "origin_ids";
constexpr size_t kExpectedPreferenceListSize = 2;
constexpr base::TimeDelta kExpirationDelta = base::Hours(24);
constexpr size_t kConnectionAttempts = 5;
constexpr base::TimeDelta kStartupDelay = base::Minutes(1);

}  // namespace

class MediaDrmOriginIdManagerTest : public testing::Test {
 public:
  // By default MediaDrmOriginIdManager will attempt to pre-provision origin
  // IDs at startup. For most tests this should be disabled.
  void Initialize(bool enable_preprovision_at_startup = false) {
    scoped_feature_list_.InitWithFeatureState(
        media::kMediaDrmPreprovisioningAtStartup,
        enable_preprovision_at_startup);

    TestingProfile::Builder profile_builder;
    profile_ = profile_builder.Build();
    origin_id_manager_ =
        MediaDrmOriginIdManagerFactory::GetForProfile(profile_.get());
    origin_id_manager_->SetProvisioningResultCBForTesting(
        base::BindRepeating(&MediaDrmOriginIdManagerTest::GetProvisioningResult,
                            base::Unretained(this)));
  }

  MOCK_METHOD0(GetProvisioningResult, MediaDrmOriginId());

  // Call MediaDrmOriginIdManager::GetOriginId() synchronously.
  MediaDrmOriginId GetOriginId() {
    base::RunLoop run_loop;
    MediaDrmOriginId result;

    origin_id_manager_->GetOriginId(base::BindOnce(
        [](base::OnceClosure callback, MediaDrmOriginId* result,
           MediaDrmOriginIdManager::GetOriginIdStatus status,
           const MediaDrmOriginId& origin_id) {
          // If |status| = kFailure, then |origin_id| should be null.
          // Otherwise (successful), |origin_id| should be not null.
          EXPECT_EQ(
              status != MediaDrmOriginIdManager::GetOriginIdStatus::kFailure,
              origin_id.has_value());
          *result = origin_id;
          std::move(callback).Run();
        },
        run_loop.QuitClosure(), &result));
    run_loop.Run();
    return result;
  }

  void PreProvision() {
    origin_id_manager_->PreProvisionIfNecessary();
  }

  std::string DisplayPref(const base::Value::Dict& value) {
    std::string output;
    JSONStringValueSerializer serializer(&output);
    EXPECT_TRUE(serializer.Serialize(value));
    return output;
  }

  const base::Value::Dict& GetDict(const std::string& path) const {
    return profile_->GetTestingPrefService()->GetDict(path);
  }

  void VerifyListSize() {
    auto& dict = GetDict(kMediaDrmOriginIds);
    DVLOG(1) << DisplayPref(dict);
    const auto* list = dict.FindList(kAvailableOriginIds);
    EXPECT_TRUE(list);
    EXPECT_EQ(list->size(), kExpectedPreferenceListSize);
  }

  // On devices that support per-application provisioning pre-provisioning
  // should fully populate the list of pre-provisioned origin IDs (as long as
  // provisioning succeeds). On devices that don't the list should be empty.
  void CheckPreferenceForPreProvisioning() {
    DVLOG(1) << "Checking preference " << kMediaDrmOriginIds;

    auto& dict = GetDict(kMediaDrmOriginIds);
    DVLOG(1) << DisplayPref(dict);

    const auto* list = dict.FindList(kAvailableOriginIds);
    if (media::MediaDrmBridge::IsPerApplicationProvisioningSupported()) {
      // PreProvision() should have pre-provisioned
      // |kExpectedPreferenceListSize| origin IDs.
      DVLOG(1) << "Per-application provisioning is supported.";
      EXPECT_TRUE(list);
      EXPECT_EQ(list->size(), kExpectedPreferenceListSize);
    } else {
      // No pre-provisioned origin IDs should exist. In fact, the dictionary
      // should not have any entries.
      DVLOG(1) << "Per-application provisioning is NOT supported.";
      EXPECT_FALSE(list);
      EXPECT_EQ(dict.size(), 0u);
    }
  }

 protected:
  content::BrowserTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  base::test::ScopedFeatureList scoped_feature_list_;
  std::unique_ptr<TestingProfile> profile_;
  raw_ptr<MediaDrmOriginIdManager> origin_id_manager_;
};

TEST_F(MediaDrmOriginIdManagerTest, DisablePreProvisioningAtStartup) {
  // Test verifies that the construction of MediaDrmOriginIdManager is
  // successful. Pre-provisioning origin IDs at startup should be disabled
  // so no calls to GetProvisioningResult() are expected.
  Initialize();

  EXPECT_FALSE(
      base::FeatureList::IsEnabled(media::kMediaDrmPreprovisioningAtStartup));
  EXPECT_FALSE(
      base::FeatureList::IsEnabled(media::kFailUrlProvisionFetcherForTesting));

  task_environment_.RunUntilIdle();

  // Preference should not exist. Not using GetDict() as it will
  // create the preference if it doesn't exist.
  EXPECT_FALSE(
      profile_->GetTestingPrefService()->HasPrefPath(kMediaDrmOriginIds));
}

TEST_F(MediaDrmOriginIdManagerTest, OneOriginId) {
  EXPECT_CALL(*this, GetProvisioningResult())
      .WillRepeatedly(InvokeWithoutArgs(&base::UnguessableToken::Create));
  Initialize();

  EXPECT_TRUE(GetOriginId());
}

TEST_F(MediaDrmOriginIdManagerTest, TwoOriginIds) {
  EXPECT_CALL(*this, GetProvisioningResult())
      .WillRepeatedly(InvokeWithoutArgs(&base::UnguessableToken::Create));
  Initialize();

  MediaDrmOriginId origin_id1 = GetOriginId();
  MediaDrmOriginId origin_id2 = GetOriginId();
  EXPECT_TRUE(origin_id1);
  EXPECT_TRUE(origin_id2);
  EXPECT_NE(origin_id1, origin_id2);
}

TEST_F(MediaDrmOriginIdManagerTest, PreProvision) {
  // On devices that support per-application provisioning PreProvision() will
  // pre-provisioned several origin IDs and populate the preference. On devices
  // that don't, the list will be empty.
  EXPECT_CALL(*this, GetProvisioningResult())
      .WillRepeatedly(InvokeWithoutArgs(&base::UnguessableToken::Create));
  Initialize();

  PreProvision();
  task_environment_.RunUntilIdle();

  CheckPreferenceForPreProvisioning();
}

TEST_F(MediaDrmOriginIdManagerTest, PreProvisionAtStartup) {
  // Initialize without disabling kMediaDrmPreprovisioningAtStartup. Check
  // that pre-provisioning actually runs at profile creation (on devices
  // that support it).
  EXPECT_CALL(*this, GetProvisioningResult())
      .WillRepeatedly(InvokeWithoutArgs(&base::UnguessableToken::Create));
  Initialize(true);

  DVLOG(1) << "Advancing Time";
  task_environment_.FastForwardBy(kStartupDelay);
  task_environment_.RunUntilIdle();

  CheckPreferenceForPreProvisioning();
}

TEST_F(MediaDrmOriginIdManagerTest, PreProvisionFailAtStartup) {
  // Initialize without disabling kMediaDrmPreprovisioningAtStartup. Have
  // provisioning fail at startup, if it is attempted.
  if (media::MediaDrmBridge::IsPerApplicationProvisioningSupported()) {
    EXPECT_CALL(*this, GetProvisioningResult()).WillOnce(Return(std::nullopt));
  } else {
    // If per-application provisioning is NOT supported, no attempt will be made
    // to pre-provision any origin IDs at startup.
    EXPECT_CALL(*this, GetProvisioningResult()).Times(0);
  }

  Initialize(true);

  DVLOG(1) << "Advancing Time";
  task_environment_.FastForwardBy(kStartupDelay);
  task_environment_.RunUntilIdle();

  // Pre-provisioning should have failed.
  DVLOG(1) << "Checking preference " << kMediaDrmOriginIds;
  auto& dict = GetDict(kMediaDrmOriginIds);
  DVLOG(1) << DisplayPref(dict);

  // After failure the preference should not contain |kExpireableToken| as that
  // should only be set if the user requested an origin ID on devices that
  // support per-application provisioning.
  EXPECT_FALSE(dict.Find(kExpirableToken));

  // There should be no pre-provisioned origin IDs.
  EXPECT_FALSE(dict.Find(kAvailableOriginIds));

  // Now let provisioning succeed.
  if (media::MediaDrmBridge::IsPerApplicationProvisioningSupported()) {
    // If per-application provisioning is NOT supported, no attempt will be made
    // to pre-provision any origin IDs. So only expect calls if per-application
    // provisioning is supported.
    EXPECT_CALL(*this, GetProvisioningResult())
        .WillRepeatedly(InvokeWithoutArgs(&base::UnguessableToken::Create));
  }

  // Trigger a network connection to force pre-provisioning to run again.
  network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
      network::mojom::ConnectionType::CONNECTION_ETHERNET);
  task_environment_.RunUntilIdle();

  // Pre-provisioning should have run again. Should return the same result as if
  // pre-provisioning had succeeded at startup.
  CheckPreferenceForPreProvisioning();
}

TEST_F(MediaDrmOriginIdManagerTest, GetOriginIdCreatesList) {
  // After fetching an origin ID the code should pre-provision more origins
  // and fill up the list. This is independent of whether the device supports
  // per-application provisioning or not.
  EXPECT_CALL(*this, GetProvisioningResult())
      .WillRepeatedly(InvokeWithoutArgs(&base::UnguessableToken::Create));
  Initialize();

  GetOriginId();
  task_environment_.RunUntilIdle();

  DVLOG(1) << "Checking preference " << kMediaDrmOriginIds;
  VerifyListSize();
}

TEST_F(MediaDrmOriginIdManagerTest, OriginIdNotInList) {
  // After fetching one origin ID MediaDrmOriginIdManager will create the list
  // of pre-provisioned origin IDs (asynchronously). It doesn't matter if the
  // device supports per-application provisioning or not.
  EXPECT_CALL(*this, GetProvisioningResult())
      .WillRepeatedly(InvokeWithoutArgs(&base::UnguessableToken::Create));
  Initialize();

  MediaDrmOriginId origin_id = GetOriginId();
  task_environment_.RunUntilIdle();

  // Check that the preference does not contain |origin_id|.
  DVLOG(1) << "Checking preference " << kMediaDrmOriginIds;
  auto& dict = GetDict(kMediaDrmOriginIds);
  auto* list = dict.FindList(kAvailableOriginIds);
  EXPECT_FALSE(
      base::Contains(*list, base::UnguessableTokenToValue(origin_id.value())));
}

TEST_F(MediaDrmOriginIdManagerTest, ProvisioningFail) {
  // Provisioning fails, so GetOriginId() returns an empty origin ID.
  EXPECT_CALL(*this, GetProvisioningResult()).WillOnce(Return(std::nullopt));
  Initialize();

  EXPECT_FALSE(GetOriginId());

  task_environment_.RunUntilIdle();

  // After failure the preference should contain |kExpireableToken| only if
  // per-application provisioning is NOT supported.
  DVLOG(1) << "Checking preference " << kMediaDrmOriginIds;
  auto& dict = GetDict(kMediaDrmOriginIds);
  DVLOG(1) << DisplayPref(dict);

  if (media::MediaDrmBridge::IsPerApplicationProvisioningSupported()) {
    DVLOG(1) << "Per-application provisioning is supported.";
    EXPECT_FALSE(dict.Find(kExpirableToken));
  } else {
    DVLOG(1) << "Per-application provisioning is NOT supported.";
    EXPECT_TRUE(dict.Find(kExpirableToken));
  }
}

TEST_F(MediaDrmOriginIdManagerTest, ProvisioningSuccessAfterFail) {
  // Provisioning fails, so GetOriginId() returns an empty origin ID.
  EXPECT_CALL(*this, GetProvisioningResult())
      .WillOnce(Return(std::nullopt))
      .WillRepeatedly(InvokeWithoutArgs(&base::UnguessableToken::Create));
  Initialize();

  EXPECT_FALSE(GetOriginId());
  EXPECT_TRUE(GetOriginId());  // Provisioning will succeed on the second call.

  // Let pre-provisioning of other origin IDs finish.
  task_environment_.RunUntilIdle();

  // After success the preference should not contain |kExpireableToken|.
  DVLOG(1) << "Checking preference " << kMediaDrmOriginIds;
  auto& dict = GetDict(kMediaDrmOriginIds);
  DVLOG(1) << DisplayPref(dict);
  EXPECT_FALSE(dict.Find(kExpirableToken));

  // As well, the list of available pre-provisioned origin IDs should be full.
  VerifyListSize();
}

TEST_F(MediaDrmOriginIdManagerTest, ProvisioningAfterExpiration) {
  // Provisioning fails, so GetOriginId() returns an empty origin ID.
  DVLOG(1) << "Current time: " << base::Time::Now();
  EXPECT_CALL(*this, GetProvisioningResult())
      .WillOnce(Return(std::nullopt))
      .WillRepeatedly(InvokeWithoutArgs(&base::UnguessableToken::Create));
  Initialize();

  EXPECT_FALSE(GetOriginId());
  task_environment_.RunUntilIdle();

  {
    // Check that |kAvailableOriginIds| in the preference is empty.
    DVLOG(1) << "Checking preference " << kMediaDrmOriginIds;
    auto& dict = GetDict(kMediaDrmOriginIds);
    DVLOG(1) << DisplayPref(dict);
    EXPECT_FALSE(dict.Find(kAvailableOriginIds));

    // Check that |kExpirableToken| is only set if per-application provisioning
    // is not supported.
    EXPECT_TRUE(
        media::MediaDrmBridge::IsPerApplicationProvisioningSupported() ||
        dict.Find(kExpirableToken));
  }

  // Advance clock by |kExpirationDelta| (plus one minute) and attempt to
  // pre-provision more origin Ids.
  DVLOG(1) << "Advancing Time";
  task_environment_.FastForwardBy(kExpirationDelta);
  task_environment_.FastForwardBy(base::Minutes(1));
  DVLOG(1) << "Adjusted time: " << base::Time::Now();
  PreProvision();
  task_environment_.RunUntilIdle();

  // Look at the preference again.
  DVLOG(1) << "Checking preference " << kMediaDrmOriginIds << " again";
  auto& dict = GetDict(kMediaDrmOriginIds);
  DVLOG(1) << DisplayPref(dict);
  auto* list = dict.FindList(kAvailableOriginIds);

  if (media::MediaDrmBridge::IsPerApplicationProvisioningSupported()) {
    // If per-application provisioning is supported, it's OK to attempt
    // to pre-provision origin IDs any time.
    DVLOG(1) << "Per-application provisioning is supported.";
    ASSERT_TRUE(list);
    EXPECT_EQ(list->size(), kExpectedPreferenceListSize);
  } else {
    // Per-application provisioning is not supported, so attempting to
    // pre-provision origin IDs after |kExpirationDelta| should not do anything.
    // As well, |kExpirableToken| should be removed.
    DVLOG(1) << "Per-application provisioning is NOT supported.";
    EXPECT_FALSE(list);
  }
  EXPECT_FALSE(dict.Find(kExpirableToken));
}

TEST_F(MediaDrmOriginIdManagerTest, Incognito) {
  // No MediaDrmOriginIdManager should be created for an incognito profile.
  Initialize();
  auto* incognito_profile =
      profile_->GetPrimaryOTRProfile(/*create_if_needed=*/true);
  EXPECT_FALSE(
      MediaDrmOriginIdManagerFactory::GetForProfile(incognito_profile));
}

TEST_F(MediaDrmOriginIdManagerTest, NetworkChange) {
  // Try to pre-provision a bunch of origin IDs. Provisioning will fail, so
  // there will not be a bunch of origin IDs created. However, it should be
  // watching for a network change.
  // TODO(crbug.com/41433110): Currently the code returns an origin ID even if
  // provisioning fails. Update this once it returns an empty origin ID when
  // pre-provisioning fails.
  EXPECT_CALL(*this, GetProvisioningResult())
      .WillOnce(Return(std::nullopt))
      .WillRepeatedly(InvokeWithoutArgs(&base::UnguessableToken::Create));
  Initialize();

  EXPECT_FALSE(GetOriginId());
  task_environment_.RunUntilIdle();

  // Check that |kAvailableOriginIds| in the preference is empty.
  DVLOG(1) << "Checking preference " << kMediaDrmOriginIds;
  {
    auto& dict = GetDict(kMediaDrmOriginIds);
    DVLOG(1) << DisplayPref(dict);
    EXPECT_FALSE(dict.Find(kAvailableOriginIds));
  }

  // Provisioning will now "succeed", so trigger a network change to
  // unconnected.
  network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
      network::mojom::ConnectionType::CONNECTION_NONE);
  task_environment_.RunUntilIdle();

  // Check that |kAvailableOriginIds| is still empty.
  DVLOG(1) << "Checking preference " << kMediaDrmOriginIds << " again";
  {
    auto& dict = GetDict(kMediaDrmOriginIds);
    DVLOG(1) << DisplayPref(dict);
    EXPECT_FALSE(dict.Find(kAvailableOriginIds));
  }

  // Now trigger a network change to connected.
  network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
      network::mojom::ConnectionType::CONNECTION_ETHERNET);
  task_environment_.RunUntilIdle();

  // Pre-provisioning should have run and filled up the list.
  DVLOG(1) << "Checking preference " << kMediaDrmOriginIds << " again";
  VerifyListSize();
}

TEST_F(MediaDrmOriginIdManagerTest, NetworkChangeFails) {
  // Try to pre-provision a bunch of origin IDs. Provisioning will fail the
  // first time, so there will not be a bunch of origin IDs created. However, it
  // should be watching for a network change, and will try again on the next
  // |kConnectionAttempts| connections to a network. GetProvisioningResult()
  // should only be called once for the GetOriginId() call +
  // |kConnectionAttempts| when a network connection is detected.
  // TODO(crbug.com/41433110): Currently the code returns an origin ID even if
  // provisioning fails. Update this once it returns an empty origin ID when
  // pre-provisioning fails.
  EXPECT_CALL(*this, GetProvisioningResult())
      .Times(kConnectionAttempts + 1)
      .WillOnce(Return(std::nullopt));
  Initialize();

  EXPECT_FALSE(GetOriginId());
  task_environment_.RunUntilIdle();

  // Check that |kAvailableOriginIds| in the preference is empty.
  DVLOG(1) << "Checking preference " << kMediaDrmOriginIds;
  {
    auto& dict = GetDict(kMediaDrmOriginIds);
    DVLOG(1) << DisplayPref(dict);
    EXPECT_FALSE(dict.Find(kAvailableOriginIds));
  }

  // Trigger multiple network connections (provisioning still fails). Call more
  // than |kConnectionAttempts| to ensure that the network change is ignored
  // after several failed attempts.
  for (size_t i = 0; i < kConnectionAttempts + 3; ++i) {
    network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
        network::mojom::ConnectionType::CONNECTION_ETHERNET);
    task_environment_.RunUntilIdle();
  }

  // Check that |kAvailableOriginIds| is still empty.
  DVLOG(1) << "Checking preference " << kMediaDrmOriginIds << " again";
  {
    auto& dict = GetDict(kMediaDrmOriginIds);
    DVLOG(1) << DisplayPref(dict);
    EXPECT_FALSE(dict.Find(kAvailableOriginIds));
  }
}

TEST_F(MediaDrmOriginIdManagerTest, InvalidEntry) {
  // After fetching an origin ID the code should pre-provision more origins
  // and fill up the list. This is independent of whether the device supports
  // per-application provisioning or not.
  EXPECT_CALL(*this, GetProvisioningResult())
      .WillRepeatedly(InvokeWithoutArgs(&base::UnguessableToken::Create));
  Initialize();

  EXPECT_TRUE(GetOriginId());
  task_environment_.RunUntilIdle();
  VerifyListSize();

  // Fetching the first origin ID has now filled up the list. Replace the
  // first entry in the list with something (a boolean value) that cannot
  // be converted to a base::UnguessableToken.
  {
    ScopedDictPrefUpdate update(profile_->GetTestingPrefService(),
                                kMediaDrmOriginIds);
    base::Value::List* origin_ids = update->FindList(kAvailableOriginIds);
    EXPECT_FALSE(origin_ids->empty());
    auto first_entry = origin_ids->begin();
    *first_entry = base::Value(true);
  }

  // Next GetOriginId() call should attempt to use the invalid entry. Since
  // it's invalid, a new origin ID will be created and used. And then an
  // additional one is created to replace the one that should have been taken
  // from the list.
  EXPECT_TRUE(GetOriginId());
  task_environment_.RunUntilIdle();
  VerifyListSize();
}