blob: 555901b86517d593200d85a6dd7c0a5976888e8d [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/commerce/core/product_specifications/product_specifications_service.h"
#include <optional>
#include <vector>
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "base/uuid.h"
#include "components/commerce/core/commerce_types.h"
#include "components/commerce/core/product_specifications/product_specifications_set.h"
#include "components/commerce/core/product_specifications/product_specifications_sync_bridge.h"
#include "components/sync/model/in_memory_metadata_change_list.h"
#include "components/sync/protocol/compare_specifics.pb.h"
#include "components/sync/test/mock_model_type_change_processor.h"
#include "components/sync/test/model_type_store_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
const char kProductOneUrl[] = "https://example.com/productone";
const char kProductTwoUrl[] = "https://example.com.com/producttwo";
const char kProductSpecsName[] = "name";
sync_pb::CompareSpecifics BuildCompareSpecifics(
const std::string& uuid,
int64_t creation_time_micros_epoch,
int64_t update_time_micros_epoch,
const std::string& name,
std::vector<std::string> urls) {
sync_pb::CompareSpecifics specifics;
specifics.set_uuid(uuid);
specifics.set_creation_time_unix_epoch_micros(creation_time_micros_epoch);
specifics.set_update_time_unix_epoch_micros(update_time_micros_epoch);
specifics.set_name(name);
for (auto& url : urls) {
sync_pb::ComparisonData* compare_data = specifics.add_data();
compare_data->set_url(url);
}
return specifics;
}
const sync_pb::CompareSpecifics kCompareSpecifics[] = {
BuildCompareSpecifics("abe18411-bd7e-4819-b9b5-11e66e0ad8b4",
1710953277,
1710953277 + base::Time::kMillisecondsPerDay,
"my first set",
{"https://foo.com", "https://bar.com"}),
BuildCompareSpecifics(
"f448709c-fe1f-44ea-883e-f46267b97d29",
1711035900,
1711035900 + (2 * base::Time::kMillisecondsPerDay) / 3,
"my next set",
{"https://some-url.com", "https://another-url.com"})};
syncer::EntityData MakeEntityData(const sync_pb::CompareSpecifics& specifics) {
syncer::EntityData entity_data;
*entity_data.specifics.mutable_compare() = specifics;
entity_data.name = base::StringPrintf("%s_%s", specifics.name().c_str(),
specifics.uuid().c_str());
return entity_data;
}
void AddTestSpecifics(commerce::ProductSpecificationsSyncBridge* bridge) {
syncer::EntityChangeList add_changes;
for (const auto& specifics : kCompareSpecifics) {
add_changes.push_back(syncer::EntityChange::CreateAdd(
specifics.uuid(), MakeEntityData(specifics)));
}
bridge->ApplyIncrementalSyncChanges(
std::make_unique<syncer::InMemoryMetadataChangeList>(),
std::move(add_changes));
}
MATCHER_P(HasAllProductSpecs, compare_specifics, "") {
std::vector<GURL> specifics_urls;
for (const sync_pb::ComparisonData& data : compare_specifics.data()) {
specifics_urls.emplace_back(data.url());
}
return arg.uuid().AsLowercaseString() == compare_specifics.uuid() &&
arg.creation_time() ==
base::Time::FromMillisecondsSinceUnixEpoch(
compare_specifics.creation_time_unix_epoch_micros()) &&
arg.update_time() ==
base::Time::FromMillisecondsSinceUnixEpoch(
compare_specifics.update_time_unix_epoch_micros()) &&
arg.name() == compare_specifics.name() && arg.urls() == specifics_urls;
}
MATCHER_P(IsSetWithUuid, uuid, "") {
return arg.uuid() == uuid;
}
MATCHER_P(IsUuid, uuid, "") {
return arg.AsLowercaseString() == uuid;
}
MATCHER_P2(HasProductSpecsNameUrl, name, urls, "") {
return arg.name() == name && arg.urls() == urls;
}
} // namespace
namespace commerce {
class MockProductSpecificationsSyncBridge
: public ProductSpecificationsSyncBridge {
public:
MockProductSpecificationsSyncBridge(
syncer::OnceModelTypeStoreFactory create_store_callback,
std::unique_ptr<syncer::ModelTypeChangeProcessor> change_processor)
: ProductSpecificationsSyncBridge(std::move(create_store_callback),
std::move(change_processor)) {}
~MockProductSpecificationsSyncBridge() override = default;
MOCK_METHOD(std::unique_ptr<syncer::MetadataChangeList>,
CreateMetadataChangeList,
(),
(override));
MOCK_METHOD(std::optional<syncer::ModelError>,
MergeFullSyncData,
(std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
syncer::EntityChangeList entity_changes),
(override));
MOCK_METHOD(std::string,
GetStorageKey,
(const syncer::EntityData& entity_data),
(override));
MOCK_METHOD(std::string,
GetClientTag,
(const syncer::EntityData& entity_data),
(override));
MOCK_METHOD(void,
GetData,
(StorageKeyList storage_keys, DataCallback callback),
(override));
MOCK_METHOD(void,
GetAllDataForDebugging,
(DataCallback callback),
(override));
void AddCompareSpecifics(const sync_pb::CompareSpecifics& compare_specifics) {
entries_.emplace(compare_specifics.uuid(), compare_specifics);
}
};
class MockProductSpecificationsSetObserver
: public ProductSpecificationsSet::Observer {
public:
MOCK_METHOD(void,
OnProductSpecificationsSetAdded,
(const ProductSpecificationsSet& set),
(override));
MOCK_METHOD(void,
OnProductSpecificationsSetUpdate,
(const ProductSpecificationsSet& set),
(override));
MOCK_METHOD(void,
OnProductSpecificationsSetRemoved,
(const base::Uuid& uuid),
(override));
};
class ProductSpecificationsServiceTest : public testing::Test {
public:
void SetUp() override {
store_ = syncer::ModelTypeStoreTestUtil::CreateInMemoryStoreForTest();
ON_CALL(processor_, IsTrackingMetadata())
.WillByDefault(testing::Return(true));
std::unique_ptr<MockProductSpecificationsSyncBridge> bridge =
std::make_unique<MockProductSpecificationsSyncBridge>(
syncer::ModelTypeStoreTestUtil::FactoryForForwardingStore(store()),
change_processor().CreateForwardingProcessor());
bridge_ = bridge.get();
service_ =
std::make_unique<ProductSpecificationsService>(std::move(bridge));
service_->AddObserver(&observer_);
base::RunLoop().RunUntilIdle();
}
void TearDown() override { service_->RemoveObserver(&observer_); }
MockProductSpecificationsSyncBridge* bridge() { return bridge_; }
ProductSpecificationsService* service() { return service_.get(); }
testing::NiceMock<MockProductSpecificationsSetObserver>* observer() {
return &observer_;
}
void CheckSpecsAgainstSpecifics(
const ProductSpecificationsSet& specifications,
const sync_pb::CompareSpecifics& specifics) const {
EXPECT_EQ(base::Uuid::ParseLowercase(specifics.uuid()),
specifications.uuid());
EXPECT_EQ(base::Time::FromMillisecondsSinceUnixEpoch(
specifics.creation_time_unix_epoch_micros()),
specifications.creation_time());
EXPECT_EQ(base::Time::FromMillisecondsSinceUnixEpoch(
specifics.update_time_unix_epoch_micros()),
specifications.update_time());
EXPECT_EQ(specifics.name(), specifications.name());
std::vector<GURL> urls;
for (const sync_pb::ComparisonData& data : specifics.data()) {
urls.emplace_back(data.url());
}
EXPECT_EQ(urls, specifications.urls());
}
private:
base::test::TaskEnvironment task_environment_;
std::unique_ptr<ProductSpecificationsService> service_;
raw_ptr<MockProductSpecificationsSyncBridge> bridge_;
std::unique_ptr<syncer::ModelTypeStore> store_;
testing::NiceMock<syncer::MockModelTypeChangeProcessor> processor_;
testing::NiceMock<MockProductSpecificationsSetObserver> observer_;
syncer::ModelTypeStore* store() { return store_.get(); }
testing::NiceMock<syncer::MockModelTypeChangeProcessor>& change_processor() {
return processor_;
}
};
TEST_F(ProductSpecificationsServiceTest, TestGetProductSpecifications) {
for (const sync_pb::CompareSpecifics& specifics : kCompareSpecifics) {
bridge()->AddCompareSpecifics(specifics);
}
const std::vector<ProductSpecificationsSet> specifications =
service()->GetAllProductSpecifications();
EXPECT_EQ(2u, specifications.size());
for (uint64_t i = 0; i < specifications.size(); i++) {
CheckSpecsAgainstSpecifics(specifications[i], kCompareSpecifics[i]);
}
}
TEST_F(ProductSpecificationsServiceTest, TestAddProductSpecificationsSuccess) {
std::vector<GURL> expected_product_urls{GURL(kProductOneUrl),
GURL(kProductTwoUrl)};
EXPECT_CALL(*observer(),
OnProductSpecificationsSetAdded(HasProductSpecsNameUrl(
kProductSpecsName, expected_product_urls)))
.Times(1);
std::optional<ProductSpecificationsSet> product_spec_set =
service()->AddProductSpecificationsSet(
kProductSpecsName, {GURL(kProductOneUrl), GURL(kProductTwoUrl)});
EXPECT_TRUE(product_spec_set.has_value());
EXPECT_EQ(kProductSpecsName, product_spec_set.value().name());
EXPECT_EQ(kProductOneUrl, product_spec_set.value().urls()[0].spec());
EXPECT_EQ(kProductTwoUrl, product_spec_set.value().urls()[1].spec());
}
TEST_F(ProductSpecificationsServiceTest, TestRemoveProductSpecifications) {
AddTestSpecifics(bridge());
EXPECT_CALL(*observer(), OnProductSpecificationsSetRemoved(
IsUuid(kCompareSpecifics[0].uuid())))
.Times(1);
service()->DeleteProductSpecificationsSet(kCompareSpecifics[0].uuid());
}
TEST_F(ProductSpecificationsServiceTest, TestObserverNewSpecifics) {
syncer::EntityChangeList add_changes;
for (const auto& specifics : kCompareSpecifics) {
add_changes.push_back(syncer::EntityChange::CreateAdd(
specifics.uuid(), MakeEntityData(specifics)));
EXPECT_CALL(*observer(),
OnProductSpecificationsSetAdded(HasAllProductSpecs(specifics)))
.Times(1);
}
bridge()->ApplyIncrementalSyncChanges(
std::make_unique<syncer::InMemoryMetadataChangeList>(),
std::move(add_changes));
}
TEST_F(ProductSpecificationsServiceTest, TestSetUrls) {
for (const sync_pb::CompareSpecifics& specifics : kCompareSpecifics) {
bridge()->AddCompareSpecifics(specifics);
}
const std::vector<ProductSpecificationsSet> specifications =
service()->GetAllProductSpecifications();
const base::Uuid uuid_to_modify = specifications[0].uuid();
EXPECT_CALL(*observer(),
OnProductSpecificationsSetUpdate(IsSetWithUuid(uuid_to_modify)))
.Times(1);
const std::vector<GURL> new_urls = {GURL("http://example.com/updated")};
service()->SetUrls(uuid_to_modify, new_urls);
const std::optional<ProductSpecificationsSet> updated_set =
service()->GetSetByUuid(uuid_to_modify);
EXPECT_TRUE(updated_set.has_value());
EXPECT_EQ(new_urls[0].spec(), updated_set->urls()[0].spec());
EXPECT_GT(updated_set->update_time(), specifications[0].update_time());
EXPECT_EQ(updated_set->creation_time(), specifications[0].creation_time());
}
TEST_F(ProductSpecificationsServiceTest, TestSetName) {
for (const sync_pb::CompareSpecifics& specifics : kCompareSpecifics) {
bridge()->AddCompareSpecifics(specifics);
}
const std::vector<ProductSpecificationsSet> specifications =
service()->GetAllProductSpecifications();
const base::Uuid uuid_to_modify = specifications[0].uuid();
EXPECT_CALL(*observer(),
OnProductSpecificationsSetUpdate(IsSetWithUuid(uuid_to_modify)))
.Times(1);
const std::string new_name = "updated name";
service()->SetName(uuid_to_modify, new_name);
const std::optional<ProductSpecificationsSet> updated_set =
service()->GetSetByUuid(uuid_to_modify);
EXPECT_TRUE(updated_set.has_value());
EXPECT_EQ(new_name, updated_set->name());
EXPECT_GT(updated_set->update_time(), specifications[0].update_time());
EXPECT_EQ(updated_set->creation_time(), specifications[0].creation_time());
}
TEST_F(ProductSpecificationsServiceTest, TestSetNameAndUrls_BadId) {
for (const sync_pb::CompareSpecifics& specifics : kCompareSpecifics) {
bridge()->AddCompareSpecifics(specifics);
}
const std::vector<ProductSpecificationsSet> specifications =
service()->GetAllProductSpecifications();
const base::Uuid uuid_to_modify =
base::Uuid::ParseLowercase("90000000-0000-0000-0000-000000000000");
EXPECT_CALL(*observer(), OnProductSpecificationsSetUpdate(testing::_))
.Times(0);
const std::vector<GURL> new_urls = {GURL("http://example.com/updated")};
service()->SetUrls(uuid_to_modify, new_urls);
service()->SetName(uuid_to_modify, "new name");
const std::optional<ProductSpecificationsSet> updated_set =
service()->GetSetByUuid(uuid_to_modify);
EXPECT_FALSE(updated_set.has_value());
}
TEST_F(ProductSpecificationsServiceTest, TestObserverUpdateSpecifics) {
AddTestSpecifics(bridge());
syncer::EntityChangeList update_changes;
sync_pb::CompareSpecifics new_specifics = kCompareSpecifics[0];
sync_pb::ComparisonData* new_specifics_data = new_specifics.add_data();
new_specifics_data->set_url("https://newurl.com/");
new_specifics.set_update_time_unix_epoch_micros(
new_specifics.update_time_unix_epoch_micros() +
base::Time::kMillisecondsPerDay);
update_changes.push_back(syncer::EntityChange::CreateUpdate(
new_specifics.uuid(), MakeEntityData(new_specifics)));
// Won't be updated because the update timestamp hasn't increased.
sync_pb::CompareSpecifics noupdate_specifics = kCompareSpecifics[1];
sync_pb::ComparisonData* noupdate_specifics_data =
noupdate_specifics.add_data();
noupdate_specifics_data->set_url("https://newurl.com/");
update_changes.push_back(syncer::EntityChange::CreateUpdate(
noupdate_specifics.uuid(), MakeEntityData(noupdate_specifics)));
EXPECT_CALL(*observer(), OnProductSpecificationsSetUpdate(
HasAllProductSpecs(new_specifics)))
.Times(1);
EXPECT_CALL(*observer(), OnProductSpecificationsSetUpdate(
HasAllProductSpecs(noupdate_specifics)))
.Times(0);
bridge()->ApplyIncrementalSyncChanges(
std::make_unique<syncer::InMemoryMetadataChangeList>(),
std::move(update_changes));
}
TEST_F(ProductSpecificationsServiceTest, TestObserverRemoveSpecifics) {
AddTestSpecifics(bridge());
syncer::EntityChangeList remove_changes;
for (const auto& specifics : kCompareSpecifics) {
remove_changes.push_back(
syncer::EntityChange::CreateDelete(specifics.uuid()));
EXPECT_CALL(*observer(),
OnProductSpecificationsSetRemoved(IsUuid(specifics.uuid())))
.Times(1);
}
bridge()->ApplyIncrementalSyncChanges(
std::make_unique<syncer::InMemoryMetadataChangeList>(),
std::move(remove_changes));
}
} // namespace commerce