blob: 55683163b48113fe5b8cf11bc1235ec4ec0ce74b [file]
// Copyright 2026 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/sync/model/processor_entity_metadata.h"
#include <memory>
#include "base/files/file_path.h"
#include "base/time/time.h"
#include "components/sync/base/client_tag_hash.h"
#include "components/sync/base/deletion_origin.h"
#include "components/sync/base/time.h"
#include "components/sync/engine/commit_and_get_updates_types.h"
#include "components/sync/protocol/entity_metadata.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace syncer {
namespace {
constexpr char kHash[] = "hash";
constexpr char kServerId[] = "server_id";
constexpr char kUploadUrl[] = "https://example.com/upload/session";
constexpr char kFilePath[] = "/tmp/attachment.bin";
constexpr char kTemporaryBlobId[] = "temp_blob_123";
sync_pb::EntityMetadata CreateValidMetadataProto() {
sync_pb::EntityMetadata metadata;
metadata.set_client_tag_hash("hash");
metadata.set_creation_time(TimeToProtoTime(base::Time::Now()));
metadata.set_sequence_number(1);
metadata.set_acked_sequence_number(0);
return metadata;
}
CommitResponseData CreateCommitResponse(int64_t sequence_number) {
CommitResponseData response;
response.id = kServerId;
response.client_tag_hash = ClientTagHash::FromHashed(kHash);
response.sequence_number = sequence_number;
response.response_version = 1;
return response;
}
TEST(ProcessorEntityMetadataTest, ShouldBeValidForValidMetadata) {
EXPECT_TRUE(ProcessorEntityMetadata::IsValid(CreateValidMetadataProto()));
}
TEST(ProcessorEntityMetadataTest, ShouldBeInvalidIfClientTagHashIsEmpty) {
sync_pb::EntityMetadata metadata = CreateValidMetadataProto();
metadata.clear_client_tag_hash();
EXPECT_FALSE(ProcessorEntityMetadata::IsValid(metadata));
}
TEST(ProcessorEntityMetadataTest, ShouldBeInvalidIfCreationTimeIsMissing) {
sync_pb::EntityMetadata metadata = CreateValidMetadataProto();
metadata.clear_creation_time();
EXPECT_FALSE(ProcessorEntityMetadata::IsValid(metadata));
}
TEST(ProcessorEntityMetadataTest,
ShouldBeInvalidIfSequenceNumberIsLessThanAcked) {
sync_pb::EntityMetadata metadata = CreateValidMetadataProto();
metadata.set_sequence_number(0);
metadata.set_acked_sequence_number(1);
EXPECT_FALSE(ProcessorEntityMetadata::IsValid(metadata));
}
// Verifies that `IsValid()` rejects metadata whose `pending_upload` has an
// empty `file_path`.
TEST(ProcessorEntityMetadataTest,
ShouldBeInvalidIfPendingUploadFilePathIsEmpty) {
sync_pb::EntityMetadata metadata = CreateValidMetadataProto();
metadata.mutable_attachment_metadata()->mutable_pending_upload();
EXPECT_FALSE(ProcessorEntityMetadata::IsValid(metadata));
}
TEST(ProcessorEntityMetadataTest,
ShouldBeUnsyncedAfterIncrementSequenceNumber) {
ProcessorEntityMetadata metadata = ProcessorEntityMetadata::CreateNew(
ClientTagHash::FromHashed("hash"), "server_id", base::Time::Now());
ASSERT_FALSE(metadata.IsUnsynced());
metadata.IncrementSequenceNumber();
EXPECT_TRUE(metadata.IsUnsynced());
// Test serialization.
std::unique_ptr<ProcessorEntityMetadata> metadata_restored =
ProcessorEntityMetadata::FromProto(metadata.proto());
ASSERT_NE(metadata_restored, nullptr);
EXPECT_TRUE(metadata_restored->IsUnsynced());
}
TEST(ProcessorEntityMetadataTest, ShouldNotBeUnsyncedAfterCommitResponse) {
ProcessorEntityMetadata metadata = ProcessorEntityMetadata::CreateNew(
ClientTagHash::FromHashed("hash"), "server_id", base::Time::Now());
metadata.IncrementSequenceNumber();
ASSERT_TRUE(metadata.IsUnsynced());
CommitResponseData response;
response.id = "server_id";
response.client_tag_hash = ClientTagHash::FromHashed("hash");
response.sequence_number = 1;
response.response_version = 1;
metadata.RecordCommitResponse(response);
EXPECT_FALSE(metadata.IsUnsynced());
// Test serialization.
std::unique_ptr<ProcessorEntityMetadata> metadata_restored =
ProcessorEntityMetadata::FromProto(metadata.proto());
ASSERT_NE(metadata_restored, nullptr);
EXPECT_FALSE(metadata_restored->IsUnsynced());
}
TEST(ProcessorEntityMetadataTest,
ShouldBeUnsyncedLocalCreationAfterLocalUpdate) {
ProcessorEntityMetadata metadata = ProcessorEntityMetadata::CreateNew(
ClientTagHash::FromHashed("hash"), /*server_id=*/"", base::Time::Now());
ASSERT_FALSE(metadata.IsUnsyncedLocalCreation());
metadata.IncrementSequenceNumber();
EXPECT_TRUE(metadata.IsUnsyncedLocalCreation());
// Test serialization.
std::unique_ptr<ProcessorEntityMetadata> metadata_restored =
ProcessorEntityMetadata::FromProto(metadata.proto());
ASSERT_NE(metadata_restored, nullptr);
EXPECT_TRUE(metadata_restored->IsUnsyncedLocalCreation());
}
TEST(ProcessorEntityMetadataTest,
ShouldNotBeUnsyncedLocalCreationAfterCommitResponse) {
ProcessorEntityMetadata metadata = ProcessorEntityMetadata::CreateNew(
ClientTagHash::FromHashed("hash"), /*server_id=*/"", base::Time::Now());
metadata.IncrementSequenceNumber();
ASSERT_TRUE(metadata.IsUnsyncedLocalCreation());
// Simulate commit response.
CommitResponseData response;
response.id = "server_id";
response.client_tag_hash = ClientTagHash::FromHashed("hash");
response.sequence_number = 1;
response.response_version = 12345;
metadata.RecordCommitResponse(response);
EXPECT_FALSE(metadata.IsUnsyncedLocalCreation());
// Test serialization.
std::unique_ptr<ProcessorEntityMetadata> metadata_restored =
ProcessorEntityMetadata::FromProto(metadata.proto());
ASSERT_NE(metadata_restored, nullptr);
EXPECT_FALSE(metadata_restored->IsUnsyncedLocalCreation());
}
TEST(ProcessorEntityMetadataTest,
ShouldUpdateBaseSpecificsHashOnFirstIncrement) {
ProcessorEntityMetadata metadata = ProcessorEntityMetadata::CreateNew(
ClientTagHash::FromHashed("hash"), "server_id", base::Time::Now());
UpdateResponseData update;
update.entity.id = "server_id";
update.entity.specifics.mutable_preference()->set_name("name");
update.response_version = 1;
metadata.RecordAcceptedRemoteUpdate(update, /*trimmed_specifics=*/{},
/*unique_position=*/std::nullopt);
CommitResponseData response1;
response1.id = "server_id";
response1.sequence_number = metadata.proto().sequence_number();
response1.response_version = 1;
response1.specifics_hash = metadata.proto().specifics_hash();
metadata.RecordCommitResponse(response1);
const std::string initial_hash = metadata.proto().specifics_hash();
ASSERT_FALSE(initial_hash.empty());
// First local update (becomes unsynced, base_specifics_hash set to
// initial_hash).
EntityData local_data;
local_data.specifics.mutable_preference()->set_name("new_name");
metadata.RecordLocalUpdate(local_data, /*trimmed_specifics=*/{},
/*unique_position=*/std::nullopt);
EXPECT_EQ(metadata.proto().base_specifics_hash(), initial_hash);
}
TEST(ProcessorEntityMetadataTest,
ShouldNotUpdateBaseSpecificsHashOnSubsequentIncrements) {
ProcessorEntityMetadata metadata = ProcessorEntityMetadata::CreateNew(
ClientTagHash::FromHashed("hash"), "server_id", base::Time::Now());
UpdateResponseData update;
update.entity.id = "server_id";
update.entity.specifics.mutable_preference()->set_name("name1");
update.response_version = 1;
metadata.RecordAcceptedRemoteUpdate(update, /*trimmed_specifics=*/{},
/*unique_position=*/std::nullopt);
CommitResponseData response2;
response2.id = "server_id";
response2.sequence_number = metadata.proto().sequence_number();
response2.response_version = 1;
response2.specifics_hash = metadata.proto().specifics_hash();
metadata.RecordCommitResponse(response2);
const std::string initial_hash = metadata.proto().specifics_hash();
ASSERT_FALSE(initial_hash.empty());
// First local update (becomes unsynced).
EntityData local_data1;
local_data1.specifics.mutable_preference()->set_name("name2");
metadata.RecordLocalUpdate(local_data1, /*trimmed_specifics=*/{},
/*unique_position=*/std::nullopt);
ASSERT_EQ(metadata.proto().base_specifics_hash(), initial_hash);
// Second local update (already unsynced).
EntityData local_data2;
local_data2.specifics.mutable_preference()->set_name("name3");
metadata.RecordLocalUpdate(local_data2, /*trimmed_specifics=*/{},
/*unique_position=*/std::nullopt);
EXPECT_EQ(metadata.proto().base_specifics_hash(), initial_hash);
}
TEST(ProcessorEntityMetadataTest, ShouldRecordLocalDeletion) {
ProcessorEntityMetadata metadata = ProcessorEntityMetadata::CreateNew(
ClientTagHash::FromHashed("hash"), "server_id", base::Time::Now());
ASSERT_FALSE(metadata.IsDeleted());
metadata.RecordLocalDeletion(DeletionOrigin::Unspecified());
EXPECT_TRUE(metadata.IsDeleted());
// Test serialization.
std::unique_ptr<ProcessorEntityMetadata> metadata_restored =
ProcessorEntityMetadata::FromProto(metadata.proto());
ASSERT_NE(metadata_restored, nullptr);
EXPECT_TRUE(metadata_restored->IsDeleted());
}
// Verifies that `RecordAttachmentUploadQueued()` and
// `RecordAttachmentUploadResumeUrlReceived()` mark the entity as having a
// pending attachment upload and record `file_path` and the session `upload_url`
// under `pending_upload`.
TEST(ProcessorEntityMetadataTest,
ShouldRecordAttachmentUploadResumeUrlReceived) {
ProcessorEntityMetadata metadata = ProcessorEntityMetadata::CreateNew(
ClientTagHash::FromHashed(kHash), kServerId, base::Time::Now());
metadata.IncrementSequenceNumber();
metadata.RecordAttachmentUploadQueued(
base::FilePath::FromUTF8Unsafe(kFilePath));
EXPECT_TRUE(metadata.HasPendingAttachmentUpload());
EXPECT_EQ(metadata.proto().attachment_metadata().pending_upload().file_path(),
kFilePath);
EXPECT_FALSE(
metadata.proto().attachment_metadata().pending_upload().has_upload_url());
metadata.RecordAttachmentUploadResumeUrlReceived(GURL(kUploadUrl));
EXPECT_TRUE(metadata.HasPendingAttachmentUpload());
EXPECT_EQ(metadata.proto().attachment_metadata().pending_upload().file_path(),
kFilePath);
EXPECT_EQ(
metadata.proto().attachment_metadata().pending_upload().upload_url(),
kUploadUrl);
}
// Verifies that `RecordAttachmentUploadCompleted()` clears `pending_upload`,
// stores `temporary_blob_id`, and marks the upload as complete.
TEST(ProcessorEntityMetadataTest, ShouldRecordAttachmentUploadCompleted) {
ProcessorEntityMetadata metadata = ProcessorEntityMetadata::CreateNew(
ClientTagHash::FromHashed(kHash), kServerId, base::Time::Now());
metadata.IncrementSequenceNumber();
metadata.RecordAttachmentUploadQueued(
base::FilePath::FromUTF8Unsafe(kFilePath));
metadata.RecordAttachmentUploadResumeUrlReceived(GURL(kUploadUrl));
metadata.RecordAttachmentUploadCompleted(kTemporaryBlobId);
EXPECT_FALSE(metadata.HasPendingAttachmentUpload());
EXPECT_FALSE(metadata.proto().attachment_metadata().has_pending_upload());
EXPECT_EQ(
metadata.proto().attachment_metadata().attachment().temporary_blob_id(),
kTemporaryBlobId);
}
// Verifies that `RecordPendingAttachmentUploadCleared()` clears
// `pending_upload` and clears `attachment_metadata` if no `attachment`
// submessage is present.
TEST(ProcessorEntityMetadataTest, ShouldRecordPendingAttachmentUploadCleared) {
ProcessorEntityMetadata metadata = ProcessorEntityMetadata::CreateNew(
ClientTagHash::FromHashed(kHash), kServerId, base::Time::Now());
metadata.IncrementSequenceNumber();
metadata.RecordAttachmentUploadQueued(
base::FilePath::FromUTF8Unsafe(kFilePath));
metadata.RecordAttachmentUploadResumeUrlReceived(GURL(kUploadUrl));
metadata.RecordPendingAttachmentUploadCleared();
EXPECT_FALSE(metadata.HasPendingAttachmentUpload());
EXPECT_FALSE(metadata.proto().has_attachment_metadata());
}
// Verifies that `RecordCommitResponse()` records the server's
// `attachment_modification_time`, retains `temporary_blob_id` while the entity
// remains unsynced, and clears `temporary_blob_id` (while retaining
// `attachment_metadata` and `attachment`) once all local updates are
// acknowledged.
TEST(ProcessorEntityMetadataTest,
ShouldClearTemporaryBlobIdOnCommitResponseOnlyWhenSynced) {
constexpr int64_t kAttachmentMtimeMillis = 123456789;
ProcessorEntityMetadata metadata = ProcessorEntityMetadata::CreateNew(
ClientTagHash::FromHashed(kHash), kServerId, base::Time::Now());
// Simulate two local updates so the first commit ack leaves the entity
// unsynced (`acked_sequence_number` < `sequence_number`).
metadata.IncrementSequenceNumber();
metadata.IncrementSequenceNumber();
metadata.RecordAttachmentUploadQueued(
base::FilePath::FromUTF8Unsafe(kFilePath));
metadata.RecordAttachmentUploadCompleted(kTemporaryBlobId);
CommitResponseData response1 = CreateCommitResponse(/*sequence_number=*/1);
response1.attachment_modification_time =
ProtoTimeToTime(kAttachmentMtimeMillis);
metadata.RecordCommitResponse(response1);
EXPECT_EQ(
metadata.proto().attachment_metadata().attachment().temporary_blob_id(),
kTemporaryBlobId);
EXPECT_EQ(metadata.proto()
.attachment_metadata()
.attachment()
.modification_time_unix_epoch_millis(),
kAttachmentMtimeMillis);
// Once the final sequence number is acknowledged, `temporary_blob_id` is
// cleared while `attachment_metadata` and its modification timestamp are
// retained.
metadata.RecordCommitResponse(CreateCommitResponse(/*sequence_number=*/2));
EXPECT_TRUE(metadata.proto().has_attachment_metadata());
EXPECT_TRUE(metadata.proto().attachment_metadata().has_attachment());
EXPECT_FALSE(metadata.proto()
.attachment_metadata()
.attachment()
.has_temporary_blob_id());
EXPECT_EQ(metadata.proto()
.attachment_metadata()
.attachment()
.modification_time_unix_epoch_millis(),
kAttachmentMtimeMillis);
}
// Verifies that `RecordLocalDeletion()` clears `attachment_metadata`.
TEST(ProcessorEntityMetadataTest,
ShouldClearAttachmentMetadataOnLocalDeletion) {
ProcessorEntityMetadata metadata = ProcessorEntityMetadata::CreateNew(
ClientTagHash::FromHashed(kHash), kServerId, base::Time::Now());
metadata.IncrementSequenceNumber();
metadata.RecordAttachmentUploadQueued(
base::FilePath::FromUTF8Unsafe(kFilePath));
metadata.RecordAttachmentUploadCompleted(kTemporaryBlobId);
metadata.RecordLocalDeletion(DeletionOrigin::Unspecified());
EXPECT_FALSE(metadata.proto().has_attachment_metadata());
}
} // namespace
} // namespace syncer