diff --git a/components/brave_ads/browser/BUILD.gn b/components/brave_ads/browser/BUILD.gn index 228837fd098..8e47f6b34e8 100644 --- a/components/brave_ads/browser/BUILD.gn +++ b/components/brave_ads/browser/BUILD.gn @@ -113,6 +113,7 @@ source_set("test_support") { "//brave/components/services/bat_ads/public/interfaces", "//components/prefs", "//mojo/public/cpp/bindings", + "//testing/gmock", "//ui/base/idle", "//url", ] @@ -137,6 +138,10 @@ source_set("unit_tests") { "ads_service_impl_unittest.cc", "application_state/application_state_monitor_unittest.cc", "component_updater/resource_component_registrar_unittest.cc", + "test/fake_component_updater_delegate.cc", + "test/fake_component_updater_delegate.h", + "test/mock_resource_component.cc", + "test/mock_resource_component.h", ] deps = [ diff --git a/components/brave_ads/browser/ads_service_impl.cc b/components/brave_ads/browser/ads_service_impl.cc index d1538cfca62..7c328ad924c 100644 --- a/components/brave_ads/browser/ads_service_impl.cc +++ b/components/brave_ads/browser/ads_service_impl.cc @@ -192,8 +192,8 @@ AdsServiceImpl::AdsServiceImpl( // upgrades regardless of whether the service is eligible to start. Migrate(); - // Must be active regardless of whether the service starts so that - // pref-driven eligibility changes are always observed. + // Always registered to observe eligibility pref changes even when the + // service is not running. InitializeLocalStatePrefChangeRegistrar(); InitializePrefChangeRegistrar(); @@ -209,15 +209,7 @@ AdsServiceImpl::~AdsServiceImpl() = default; /////////////////////////////////////////////////////////////////////////////// -void AdsServiceImpl::RegisterResourceComponents() { - RegisterCountryResourceComponent(); - if (UserHasOptedInToNotificationAds()) { - // Only utilized for text classification, which requires the user to have - // joined Brave Rewards and opted into notification ads. - RegisterLanguageResourceComponent(); - } -} void AdsServiceImpl::Migrate() { // Added 10/2025. @@ -232,6 +224,16 @@ void AdsServiceImpl::Migrate() { } } +void AdsServiceImpl::RegisterResourceComponents() { + RegisterCountryResourceComponent(); + + if (UserHasOptedInToNotificationAds()) { + // Only utilized for text classification, which requires the user to have + // joined Brave Rewards and opted into notification ads. + RegisterLanguageResourceComponent(); + } +} + void AdsServiceImpl::RegisterCountryResourceComponent() { if (resource_component_) { resource_component_->RegisterCountryComponent( @@ -239,13 +241,25 @@ void AdsServiceImpl::RegisterCountryResourceComponent() { } } +void AdsServiceImpl::UnregisterCountryResourceComponent() { + resource_component_->UnregisterCountryComponent(); +} + void AdsServiceImpl::RegisterLanguageResourceComponent() { if (resource_component_) { resource_component_->RegisterLanguageComponent(CurrentLanguageCode()); } } +void AdsServiceImpl::UnregisterLanguageResourceComponent() { + resource_component_->UnregisterLanguageComponent(); +} + bool AdsServiceImpl::UserHasJoinedBraveRewards() const { + if (prefs_->IsManagedPreference(brave_rewards::prefs::kDisabledByPolicy) && + prefs_->GetBoolean(brave_rewards::prefs::kDisabledByPolicy)) { + return false; + } return prefs_->GetBoolean(brave_rewards::prefs::kEnabled); } @@ -690,6 +704,11 @@ void AdsServiceImpl::InitializePrefChangeRegistrar() { } void AdsServiceImpl::InitializeBraveRewardsPrefChangeRegistrar() { + pref_change_registrar_.Add( + brave_rewards::prefs::kDisabledByPolicy, + base::BindRepeating(&AdsServiceImpl::OnAdsPrefChanged, + base::Unretained(this))); + pref_change_registrar_.Add( brave_rewards::prefs::kEnabled, base::BindRepeating(&AdsServiceImpl::NotifyPrefChanged, @@ -749,18 +768,24 @@ void AdsServiceImpl::InitializeSearchResultAdsPrefChangeRegistrar() { void AdsServiceImpl::OnAdsPrefChanged(const std::string& path) { if (!CanStartBatAdsService()) { - // The pref change made the service ineligible to run, so tear it down. + // The pref change made the service ineligible to run, so tear it down and + // release resource components that are no longer needed. + UnregisterCountryResourceComponent(); + UnregisterLanguageResourceComponent(); return ShutdownAdsService(); } - if (bat_ads_service_remote_.is_bound() && UserHasOptedInToNotificationAds() && + if (bat_ads_service_remote_.is_bound() && path == prefs::kOptedInToNotificationAds) { - // Register language resource components if the user has joined Brave - // Rewards, opted into notification ads, and the Bat Ads Service has - // already started. - RegisterLanguageResourceComponent(); + if (UserHasOptedInToNotificationAds()) { + // Register now that the user has opted in. + RegisterLanguageResourceComponent(); - delegate_->MaybeInitNotificationHelper(); + delegate_->MaybeInitNotificationHelper(); + } else { + // Unregister now that the user has opted out. + UnregisterLanguageResourceComponent(); + } } MaybeStartBatAdsService(); diff --git a/components/brave_ads/browser/ads_service_impl.h b/components/brave_ads/browser/ads_service_impl.h index 5cbc1d416cf..6b1fa8756cc 100644 --- a/components/brave_ads/browser/ads_service_impl.h +++ b/components/brave_ads/browser/ads_service_impl.h @@ -97,11 +97,11 @@ class AdsServiceImpl : public AdsService, #endif public content_settings::Observer { public: - // `http_client`, `resource_component`, `history_service`, and - // `host_content_settings` can be `nullptr` in tests. `rewards_service` - // can be `nullptr` when Rewards is unsupported or disabled by policy. - // `policy_initialization_waiter` defers the initial ads-eligibility gate - // until the policy bundle has been merged into the managed pref store. + // `http_client`, `history_service`, and `host_content_settings` can be + // `nullptr` in tests. `rewards_service` can be `nullptr` when Rewards is + // unsupported or disabled by policy. `policy_initialization_waiter` defers + // the initial ads-eligibility gate until the policy bundle has been merged + // into the managed pref store. explicit AdsServiceImpl( std::unique_ptr delegate, PrefService& prefs, @@ -138,11 +138,13 @@ class AdsServiceImpl : public AdsService, private: friend class BraveAdsAdsServiceImplTest; + void Migrate(); + void RegisterResourceComponents(); void RegisterCountryResourceComponent(); + void UnregisterCountryResourceComponent(); void RegisterLanguageResourceComponent(); - - void Migrate(); + void UnregisterLanguageResourceComponent(); bool UserHasJoinedBraveRewards() const; bool UserHasOptedInToNewTabPageAds() const; diff --git a/components/brave_ads/browser/ads_service_impl_unittest.cc b/components/brave_ads/browser/ads_service_impl_unittest.cc index d707938ad23..1335a691747 100644 --- a/components/brave_ads/browser/ads_service_impl_unittest.cc +++ b/components/brave_ads/browser/ads_service_impl_unittest.cc @@ -22,6 +22,7 @@ #include "brave/components/brave_ads/browser/test/fake_bat_ads_service_factory.h" #include "brave/components/brave_ads/browser/test/fake_device_id.h" #include "brave/components/brave_ads/browser/test/fake_virtual_pref_provider_delegate.h" +#include "brave/components/brave_ads/browser/test/mock_resource_component.h" #include "brave/components/brave_ads/core/public/prefs/pref_names.h" #include "brave/components/brave_ads/core/public/prefs/pref_registry.h" #include "brave/components/brave_policy/policy_initialization_waiter.h" @@ -92,7 +93,7 @@ class BraveAdsAdsServiceImplTest : public testing::Test { std::make_unique(), /*channel_name=*/"foo", profile_dir_.GetPath(), std::make_unique(), std::move(device_id), - std::move(bat_ads_service_factory), /*resource_component=*/nullptr, + std::move(bat_ads_service_factory), &mock_resource_component_, /*history_service=*/nullptr, #if BUILDFLAG(ENABLE_BRAVE_REWARDS) &rewards_service_, @@ -139,6 +140,8 @@ class BraveAdsAdsServiceImplTest : public testing::Test { test::FakeRewardsService rewards_service_; #endif // BUILDFLAG(ENABLE_BRAVE_REWARDS) + testing::NiceMock mock_resource_component_; + std::unique_ptr ads_service_; }; @@ -636,4 +639,59 @@ TEST_F(BraveAdsAdsServiceImplTest, EXPECT_THAT(prefs_.GetList(prefs::kNotificationAds), testing::IsEmpty()); } +#if BUILDFLAG(ENABLE_BRAVE_REWARDS) +TEST_F(BraveAdsAdsServiceImplTest, + RegistersLanguageResourceComponentWhenUserOptsInToNotificationAds) { + // Arrange: start the service via search result ads and wait for + // initialization so `bat_ads_service_remote_` is bound. + prefs_.SetBoolean(prefs::kOptedInToSearchResultAds, true); + prefs_.SetBoolean(brave_rewards::prefs::kEnabled, true); + prefs_.SetBoolean(prefs::kOptedInToNotificationAds, false); + Startup(); + ASSERT_TRUE(base::test::RunUntil( + [&] { return bat_ads_service_factory_->initialize_count() == 1U; })); + + EXPECT_CALL(mock_resource_component_, RegisterLanguageComponent); + + // Act + prefs_.SetBoolean(prefs::kOptedInToNotificationAds, true); +} + +TEST_F(BraveAdsAdsServiceImplTest, + UnregistersLanguageResourceComponentWhenUserOptsOutOfNotificationAds) { + // Arrange: start with notification ads opted in so the language component + // is already registered; service must be running before opting out. + prefs_.SetBoolean(prefs::kOptedInToSearchResultAds, true); + prefs_.SetBoolean(brave_rewards::prefs::kEnabled, true); + prefs_.SetBoolean(prefs::kOptedInToNotificationAds, true); + Startup(); + ASSERT_TRUE(base::test::RunUntil( + [&] { return bat_ads_service_factory_->initialize_count() == 1U; })); + + EXPECT_CALL(mock_resource_component_, UnregisterLanguageComponent()); + + // Act + prefs_.SetBoolean(prefs::kOptedInToNotificationAds, false); +} +#endif // BUILDFLAG(ENABLE_BRAVE_REWARDS) + +#if !BUILDFLAG(IS_ANDROID) +TEST_F(BraveAdsAdsServiceImplTest, + UnregistersResourceComponentsWhenServiceBecomesIneligible) { + // Arrange: start the service so resource components are registered; then + // disable ads via policy to trigger unregistration. + prefs_.SetBoolean(prefs::kOptedInToSearchResultAds, true); + Startup(); + ASSERT_TRUE(base::test::RunUntil( + [&] { return bat_ads_service_factory_->initialize_count() == 1U; })); + + EXPECT_CALL(mock_resource_component_, UnregisterCountryComponent()); + EXPECT_CALL(mock_resource_component_, UnregisterLanguageComponent()); + + // Act + prefs_.SetManagedPref(brave_rewards::prefs::kDisabledByPolicy, + base::Value(true)); +} +#endif // !BUILDFLAG(IS_ANDROID) + } // namespace brave_ads diff --git a/components/brave_ads/browser/component_updater/resource_component.cc b/components/brave_ads/browser/component_updater/resource_component.cc index 4f1b5153ea6..68c47289d25 100644 --- a/components/brave_ads/browser/component_updater/resource_component.cc +++ b/components/brave_ads/browser/component_updater/resource_component.cc @@ -76,12 +76,20 @@ void ResourceComponent::RegisterCountryComponent( country_resource_component_registrar_.RegisterResourceComponent(country_code); } +void ResourceComponent::UnregisterCountryComponent() { + country_resource_component_registrar_.UnregisterResourceComponent(); +} + void ResourceComponent::RegisterLanguageComponent( const std::string& language_code) { language_resource_component_registrar_.RegisterResourceComponent( language_code); } +void ResourceComponent::UnregisterLanguageComponent() { + language_resource_component_registrar_.UnregisterResourceComponent(); +} + std::optional ResourceComponent::MaybeGetPath( const std::string& id, int version) { @@ -127,8 +135,6 @@ void ResourceComponent::OnResourceComponentUnregistered( void ResourceComponent::LoadManifestCallback(const std::string& component_id, const base::FilePath& install_dir, const std::string& json) { - VLOG(8) << "Manifest JSON: " << json; - std::optional dict = base::JSONReader::ReadDict(json, base::JSON_PARSE_RFC); if (!dict) { @@ -155,8 +161,6 @@ void ResourceComponent::LoadResourceCallback( const std::string& component_id, const base::FilePath& install_dir, const std::string& json) { - VLOG(8) << "Resource JSON: " << json; - std::optional root = base::JSONReader::ReadDict(json, base::JSON_PARSE_RFC); if (!root) { diff --git a/components/brave_ads/browser/component_updater/resource_component.h b/components/brave_ads/browser/component_updater/resource_component.h index 11a7f2c70a8..7fde2e818b7 100644 --- a/components/brave_ads/browser/component_updater/resource_component.h +++ b/components/brave_ads/browser/component_updater/resource_component.h @@ -28,7 +28,7 @@ static_assert(BUILDFLAG(ENABLE_BRAVE_ADS)); namespace brave_ads { -class ResourceComponent final : public ResourceComponentRegistrarDelegate { +class ResourceComponent : public ResourceComponentRegistrarDelegate { public: explicit ResourceComponent( brave_component_updater::BraveComponent::Delegate* delegate); @@ -41,8 +41,10 @@ class ResourceComponent final : public ResourceComponentRegistrarDelegate { void AddObserver(ResourceComponentObserver* observer); void RemoveObserver(ResourceComponentObserver* observer); - void RegisterCountryComponent(const std::string& country_code); - void RegisterLanguageComponent(const std::string& language_code); + virtual void RegisterCountryComponent(const std::string& country_code); + virtual void UnregisterCountryComponent(); + virtual void RegisterLanguageComponent(const std::string& language_code); + virtual void UnregisterLanguageComponent(); std::optional MaybeGetPath(const std::string& id, int version); diff --git a/components/brave_ads/browser/component_updater/resource_component_registrar.cc b/components/brave_ads/browser/component_updater/resource_component_registrar.cc index 85d60a5f419..23c6624329f 100644 --- a/components/brave_ads/browser/component_updater/resource_component_registrar.cc +++ b/components/brave_ads/browser/component_updater/resource_component_registrar.cc @@ -68,6 +68,17 @@ void ResourceComponentRegistrar::RegisterResourceComponent( } } +void ResourceComponentRegistrar::UnregisterResourceComponent() { + if (!resource_component_id_) { + return; + } + + Unregister(); + OnComponentUnregistered(*resource_component_id_); + last_install_dir_.reset(); + resource_component_id_.reset(); +} + /////////////////////////////////////////////////////////////////////////////// void ResourceComponentRegistrar::OnComponentReady( diff --git a/components/brave_ads/browser/component_updater/resource_component_registrar.h b/components/brave_ads/browser/component_updater/resource_component_registrar.h index 5589054f5d2..1efb6623870 100644 --- a/components/brave_ads/browser/component_updater/resource_component_registrar.h +++ b/components/brave_ads/browser/component_updater/resource_component_registrar.h @@ -32,6 +32,7 @@ class ResourceComponentRegistrar final ~ResourceComponentRegistrar() override; void RegisterResourceComponent(const std::string& resource_id); + void UnregisterResourceComponent(); private: // brave_component_updater::BraveComponent: diff --git a/components/brave_ads/browser/test/fake_component_updater_delegate.cc b/components/brave_ads/browser/test/fake_component_updater_delegate.cc new file mode 100644 index 00000000000..738b306a25a --- /dev/null +++ b/components/brave_ads/browser/test/fake_component_updater_delegate.cc @@ -0,0 +1,44 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_ads/browser/test/fake_component_updater_delegate.h" + +#include "base/strings/string_util.h" + +namespace brave_ads::test { + +FakeComponentUpdaterDelegate::FakeComponentUpdaterDelegate() = default; + +FakeComponentUpdaterDelegate::~FakeComponentUpdaterDelegate() = default; + +void FakeComponentUpdaterDelegate::Register( + const std::string& /*component_name*/, + const std::string& /*component_base64_public_key*/, + base::OnceClosure registered_callback, + brave_component_updater::BraveComponent::ReadyCallback /*ready_callback*/) { + if (registered_callback) { + std::move(registered_callback).Run(); + } +} + +bool FakeComponentUpdaterDelegate::Unregister( + const std::string& /*component_id*/) { + return true; +} + +scoped_refptr +FakeComponentUpdaterDelegate::GetTaskRunner() { + return nullptr; +} + +const std::string& FakeComponentUpdaterDelegate::locale() const { + return base::EmptyString(); +} + +PrefService* FakeComponentUpdaterDelegate::local_state() { + return nullptr; +} + +} // namespace brave_ads::test diff --git a/components/brave_ads/browser/test/fake_component_updater_delegate.h b/components/brave_ads/browser/test/fake_component_updater_delegate.h new file mode 100644 index 00000000000..902962c2883 --- /dev/null +++ b/components/brave_ads/browser/test/fake_component_updater_delegate.h @@ -0,0 +1,59 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_BRAVE_ADS_BROWSER_TEST_FAKE_COMPONENT_UPDATER_DELEGATE_H_ +#define BRAVE_COMPONENTS_BRAVE_ADS_BROWSER_TEST_FAKE_COMPONENT_UPDATER_DELEGATE_H_ + +#include + +#include "base/functional/callback.h" +#include "base/memory/scoped_refptr.h" +#include "base/task/sequenced_task_runner.h" +#include "brave/components/brave_component_updater/browser/brave_component.h" + +class PrefService; + +namespace brave_ads::test { + +// Minimal no-op `BraveComponent::Delegate` for use in unit tests that need a +// `ResourceComponent` without a real component updater. +class FakeComponentUpdaterDelegate final + : public brave_component_updater::BraveComponent::Delegate { + public: + FakeComponentUpdaterDelegate(); + + FakeComponentUpdaterDelegate(const FakeComponentUpdaterDelegate&) = delete; + FakeComponentUpdaterDelegate& operator=(const FakeComponentUpdaterDelegate&) = + delete; + + ~FakeComponentUpdaterDelegate() override; + + void Register(const std::string& /*component_name*/, + const std::string& /*component_base64_public_key*/, + base::OnceClosure registered_callback, + brave_component_updater::BraveComponent::ReadyCallback + /*ready_callback*/) override; + + bool Unregister(const std::string& /*component_id*/) override; + + void EnsureInstalled(const std::string& /*component_id*/) override {} + + void AddObserver(brave_component_updater::BraveComponent::ComponentObserver* + /*observer*/) override {} + + void RemoveObserver( + brave_component_updater::BraveComponent::ComponentObserver* + /*observer*/) override {} + + scoped_refptr GetTaskRunner() override; + + const std::string& locale() const override; + + PrefService* local_state() override; +}; + +} // namespace brave_ads::test + +#endif // BRAVE_COMPONENTS_BRAVE_ADS_BROWSER_TEST_FAKE_COMPONENT_UPDATER_DELEGATE_H_ diff --git a/components/brave_ads/browser/test/fake_rewards_service.cc b/components/brave_ads/browser/test/fake_rewards_service.cc index 0ede1682597..64515dba4de 100644 --- a/components/brave_ads/browser/test/fake_rewards_service.cc +++ b/components/brave_ads/browser/test/fake_rewards_service.cc @@ -288,7 +288,10 @@ void FakeRewardsService::GetEventLogs( void FakeRewardsService::GetRewardsWallet( brave_rewards::GetRewardsWalletCallback callback) { - std::move(callback).Run(nullptr); + auto wallet = brave_rewards::mojom::RewardsWallet::New(); + wallet->payment_id = "foo"; + wallet->recovery_seed = std::vector(32, 0); + std::move(callback).Run(std::move(wallet)); } void FakeRewardsService::GetEnvironment( diff --git a/components/brave_ads/browser/test/mock_resource_component.cc b/components/brave_ads/browser/test/mock_resource_component.cc new file mode 100644 index 00000000000..b31b603102d --- /dev/null +++ b/components/brave_ads/browser/test/mock_resource_component.cc @@ -0,0 +1,15 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_ads/browser/test/mock_resource_component.h" + +namespace brave_ads::test { + +MockResourceComponent::MockResourceComponent() + : ResourceComponent(&fake_component_updater_delegate_) {} + +MockResourceComponent::~MockResourceComponent() = default; + +} // namespace brave_ads::test diff --git a/components/brave_ads/browser/test/mock_resource_component.h b/components/brave_ads/browser/test/mock_resource_component.h new file mode 100644 index 00000000000..060219b88fd --- /dev/null +++ b/components/brave_ads/browser/test/mock_resource_component.h @@ -0,0 +1,43 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_BRAVE_ADS_BROWSER_TEST_MOCK_RESOURCE_COMPONENT_H_ +#define BRAVE_COMPONENTS_BRAVE_ADS_BROWSER_TEST_MOCK_RESOURCE_COMPONENT_H_ + +#include + +#include "brave/components/brave_ads/browser/component_updater/resource_component.h" +#include "brave/components/brave_ads/browser/test/fake_component_updater_delegate.h" +#include "testing/gmock/include/gmock/gmock.h" + +namespace brave_ads::test { + +class MockResourceComponent : public ResourceComponent { + public: + MockResourceComponent(); + + MockResourceComponent(const MockResourceComponent&) = delete; + MockResourceComponent& operator=(const MockResourceComponent&) = delete; + + ~MockResourceComponent() override; + + MOCK_METHOD(void, + RegisterCountryComponent, + (const std::string& country_code), + (override)); + MOCK_METHOD(void, UnregisterCountryComponent, (), (override)); + MOCK_METHOD(void, + RegisterLanguageComponent, + (const std::string& language_code), + (override)); + MOCK_METHOD(void, UnregisterLanguageComponent, (), (override)); + + private: + FakeComponentUpdaterDelegate fake_component_updater_delegate_; +}; + +} // namespace brave_ads::test + +#endif // BRAVE_COMPONENTS_BRAVE_ADS_BROWSER_TEST_MOCK_RESOURCE_COMPONENT_H_