| // Copyright 2017 The Fuchsia 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 "src/ledger/cloud_provider_firestore/bin/app/page_cloud_impl.h" |
| |
| #include <fuchsia/ledger/cloud/cpp/fidl.h> |
| #include <google/protobuf/util/time_util.h> |
| #include <lib/callback/capture.h> |
| #include <lib/callback/set_when_called.h> |
| #include <lib/fidl/cpp/binding.h> |
| #include <lib/fidl/cpp/vector.h> |
| #include <lib/fsl/socket/strings.h> |
| #include <lib/fsl/vmo/sized_vmo.h> |
| #include <lib/fsl/vmo/strings.h> |
| #include <lib/gtest/test_loop_fixture.h> |
| |
| #include <iterator> |
| |
| #include "peridot/lib/commit_pack/commit_pack.h" |
| #include "peridot/lib/convert/convert.h" |
| #include "peridot/lib/rng/test_random.h" |
| #include "src/ledger/cloud_provider_firestore/bin/app/testing/test_credentials_provider.h" |
| #include "src/ledger/cloud_provider_firestore/bin/firestore/encoding.h" |
| #include "src/ledger/cloud_provider_firestore/bin/firestore/testing/encoding.h" |
| #include "src/ledger/cloud_provider_firestore/bin/firestore/testing/test_firestore_service.h" |
| |
| namespace cloud_provider_firestore { |
| namespace { |
| |
| void SetTimestamp(google::firestore::v1beta1::Document* document, |
| int64_t seconds, int32_t nanos) { |
| google::protobuf::Timestamp& timestamp = |
| *((*document->mutable_fields())["timestamp"].mutable_timestamp_value()); |
| timestamp.set_seconds(seconds); |
| timestamp.set_nanos(nanos); |
| } |
| |
| class TestPageCloudWatcher : public cloud_provider::PageCloudWatcher { |
| public: |
| TestPageCloudWatcher() {} |
| ~TestPageCloudWatcher() override {} |
| |
| std::vector<cloud_provider::CommitPackEntry> received_commits; |
| std::vector<cloud_provider::Token> received_tokens; |
| OnNewCommitsCallback pending_on_new_commit_callback = nullptr; |
| |
| private: |
| // cloud_provider::PageCloudWatcher: |
| void OnNewCommits(cloud_provider::CommitPack commit_pack, |
| cloud_provider::Token position_token, |
| OnNewCommitsCallback callback) override { |
| std::vector<cloud_provider::CommitPackEntry> entries; |
| EXPECT_TRUE(cloud_provider::DecodeCommitPack(commit_pack, &entries)); |
| std::move(entries.begin(), entries.end(), |
| std::back_inserter(received_commits)); |
| received_tokens.push_back(std::move(position_token)); |
| |
| EXPECT_FALSE(pending_on_new_commit_callback); |
| pending_on_new_commit_callback = std::move(callback); |
| } |
| |
| void OnNewObject(std::vector<uint8_t> /*id*/, fuchsia::mem::Buffer /*buffer*/, |
| OnNewObjectCallback /*callback*/) override { |
| FXL_NOTIMPLEMENTED(); |
| } |
| |
| void OnError(cloud_provider::Status /*status*/) override { |
| FXL_NOTIMPLEMENTED(); |
| } |
| }; |
| |
| class PageCloudImplTest : public gtest::TestLoopFixture { |
| public: |
| PageCloudImplTest() |
| : random_(test_loop().initial_state()), |
| test_credentials_provider_(dispatcher()), |
| page_cloud_impl_("page_path", &random_, &test_credentials_provider_, |
| &firestore_service_, page_cloud_.NewRequest()) {} |
| ~PageCloudImplTest() override {} |
| |
| protected: |
| rng::TestRandom random_; |
| cloud_provider::PageCloudPtr page_cloud_; |
| TestCredentialsProvider test_credentials_provider_; |
| TestFirestoreService firestore_service_; |
| PageCloudImpl page_cloud_impl_; |
| |
| private: |
| FXL_DISALLOW_COPY_AND_ASSIGN(PageCloudImplTest); |
| }; |
| |
| TEST_F(PageCloudImplTest, EmptyWhenDisconnected) { |
| bool on_empty_called = false; |
| page_cloud_impl_.set_on_empty(callback::SetWhenCalled(&on_empty_called)); |
| page_cloud_.Unbind(); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(on_empty_called); |
| } |
| |
| TEST_F(PageCloudImplTest, AddCommits) { |
| bool callback_called = false; |
| auto status = cloud_provider::Status::INTERNAL_ERROR; |
| std::vector<cloud_provider::CommitPackEntry> entries{{"id0", "data0"}}; |
| cloud_provider::CommitPack commit_pack; |
| ASSERT_TRUE(cloud_provider::EncodeCommitPack(entries, &commit_pack)); |
| page_cloud_->AddCommits( |
| std::move(commit_pack), |
| callback::Capture(callback::SetWhenCalled(&callback_called), &status)); |
| |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(callback_called); |
| EXPECT_EQ(1u, firestore_service_.commit_records.size()); |
| const google::firestore::v1beta1::CommitRequest& request = |
| firestore_service_.commit_records.front().request; |
| EXPECT_EQ(2, request.writes_size()); |
| EXPECT_TRUE(request.writes(0).has_update()); |
| EXPECT_TRUE(request.writes(1).has_transform()); |
| EXPECT_EQ(request.writes(0).update().name(), |
| request.writes(1).transform().document()); |
| |
| auto response = google::firestore::v1beta1::CommitResponse(); |
| firestore_service_.commit_records.front().callback(grpc::Status::OK, |
| std::move(response)); |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(callback_called); |
| EXPECT_EQ(cloud_provider::Status::OK, status); |
| } |
| |
| TEST_F(PageCloudImplTest, GetCommits) { |
| bool callback_called = false; |
| auto status = cloud_provider::Status::INTERNAL_ERROR; |
| std::unique_ptr<cloud_provider::CommitPack> commit_pack; |
| std::unique_ptr<cloud_provider::Token> position_token; |
| page_cloud_->GetCommits( |
| nullptr, callback::Capture(callback::SetWhenCalled(&callback_called), |
| &status, &commit_pack, &position_token)); |
| |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(callback_called); |
| EXPECT_EQ(1u, firestore_service_.run_query_records.size()); |
| |
| std::vector<google::firestore::v1beta1::RunQueryResponse> responses; |
| { |
| // First batch contains one commit. |
| std::vector<cloud_provider::CommitPackEntry> entries{{"id0", "data0"}}; |
| cloud_provider::CommitPack commit_pack; |
| ASSERT_TRUE(cloud_provider::EncodeCommitPack(entries, &commit_pack)); |
| google::firestore::v1beta1::RunQueryResponse response; |
| ASSERT_TRUE(EncodeCommitBatch(commit_pack, response.mutable_document())); |
| SetTimestamp(response.mutable_document(), 100, 1); |
| responses.push_back(std::move(response)); |
| } |
| { |
| // The second batch contains two commits. |
| std::vector<cloud_provider::CommitPackEntry> entries{{"id1", "data1"}, |
| {"id2", "data2"}}; |
| cloud_provider::CommitPack commit_pack; |
| ASSERT_TRUE(cloud_provider::EncodeCommitPack(entries, &commit_pack)); |
| google::firestore::v1beta1::RunQueryResponse response; |
| ASSERT_TRUE(EncodeCommitBatch(commit_pack, response.mutable_document())); |
| SetTimestamp(response.mutable_document(), 100, 2); |
| responses.push_back(std::move(response)); |
| } |
| |
| firestore_service_.run_query_records.front().callback(grpc::Status::OK, |
| std::move(responses)); |
| RunLoopUntilIdle(); |
| |
| EXPECT_TRUE(callback_called); |
| EXPECT_EQ(cloud_provider::Status::OK, status); |
| ASSERT_TRUE(commit_pack); |
| std::vector<cloud_provider::CommitPackEntry> entries; |
| ASSERT_TRUE(cloud_provider::DecodeCommitPack(*commit_pack, &entries)); |
| // The result should be a flat vector of all three commits. |
| EXPECT_EQ(3u, entries.size()); |
| |
| EXPECT_TRUE(position_token); |
| google::protobuf::Timestamp decoded_timestamp; |
| ASSERT_TRUE(decoded_timestamp.ParseFromString( |
| convert::ToString(position_token->opaque_id))); |
| EXPECT_EQ(100, decoded_timestamp.seconds()); |
| EXPECT_EQ(2, decoded_timestamp.nanos()); |
| } |
| |
| TEST_F(PageCloudImplTest, GetCommitsQueryPositionToken) { |
| bool callback_called = false; |
| auto status = cloud_provider::Status::INTERNAL_ERROR; |
| std::unique_ptr<cloud_provider::CommitPack> commit_pack; |
| google::protobuf::Timestamp timestamp; |
| timestamp.set_seconds(42); |
| timestamp.set_nanos(1); |
| std::string position_token_str; |
| ASSERT_TRUE(timestamp.SerializeToString(&position_token_str)); |
| auto position_token = std::make_unique<cloud_provider::Token>(); |
| position_token->opaque_id = convert::ToArray(position_token_str); |
| page_cloud_->GetCommits( |
| std::move(position_token), |
| callback::Capture(callback::SetWhenCalled(&callback_called), &status, |
| &commit_pack, &position_token)); |
| |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(callback_called); |
| EXPECT_EQ(1u, firestore_service_.run_query_records.size()); |
| |
| const google::firestore::v1beta1::RunQueryRequest& request = |
| firestore_service_.run_query_records.front().request; |
| EXPECT_TRUE(request.has_structured_query()); |
| EXPECT_TRUE(request.structured_query().has_where()); |
| |
| const google::firestore::v1beta1::StructuredQuery::Filter& filter = |
| request.structured_query().where(); |
| EXPECT_TRUE(filter.has_field_filter()); |
| EXPECT_EQ("timestamp", filter.field_filter().field().field_path()); |
| EXPECT_EQ(google::firestore::v1beta1:: |
| StructuredQuery_FieldFilter_Operator_GREATER_THAN_OR_EQUAL, |
| filter.field_filter().op()); |
| EXPECT_EQ(42, filter.field_filter().value().timestamp_value().seconds()); |
| EXPECT_EQ(1, filter.field_filter().value().timestamp_value().nanos()); |
| } |
| |
| TEST_F(PageCloudImplTest, AddObject) { |
| bool callback_called = false; |
| auto status = cloud_provider::Status::INTERNAL_ERROR; |
| fsl::SizedVmo data; |
| ASSERT_TRUE(fsl::VmoFromString("some_data", &data)); |
| page_cloud_->AddObject( |
| convert::ToArray("some_id"), std::move(data).ToTransport(), |
| callback::Capture(callback::SetWhenCalled(&callback_called), &status)); |
| |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(callback_called); |
| EXPECT_EQ(1u, firestore_service_.create_document_records.size()); |
| |
| firestore_service_.create_document_records.front().callback( |
| grpc::Status::OK, google::firestore::v1beta1::Document()); |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(callback_called); |
| EXPECT_EQ(cloud_provider::Status::OK, status); |
| } |
| |
| TEST_F(PageCloudImplTest, GetObject) { |
| bool callback_called = false; |
| auto status = cloud_provider::Status::INTERNAL_ERROR; |
| ::fuchsia::mem::BufferPtr data; |
| page_cloud_->GetObject( |
| convert::ToArray("some_id"), |
| callback::Capture(callback::SetWhenCalled(&callback_called), &status, |
| &data)); |
| |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(callback_called); |
| EXPECT_EQ(1u, firestore_service_.get_document_records.size()); |
| |
| const std::string response_data = "some_data"; |
| google::firestore::v1beta1::Document response; |
| *((*response.mutable_fields())["data"].mutable_bytes_value()) = response_data; |
| firestore_service_.get_document_records.front().callback(grpc::Status(), |
| response); |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(callback_called); |
| EXPECT_EQ(cloud_provider::Status::OK, status); |
| |
| std::string read_data; |
| ASSERT_TRUE(fsl::StringFromVmo(*data, &read_data)); |
| EXPECT_EQ("some_data", read_data); |
| } |
| |
| TEST_F(PageCloudImplTest, GetObjectParseError) { |
| bool callback_called = false; |
| auto status = cloud_provider::Status::INTERNAL_ERROR; |
| ::fuchsia::mem::BufferPtr data; |
| page_cloud_->GetObject( |
| convert::ToArray("some_id"), |
| callback::Capture(callback::SetWhenCalled(&callback_called), &status, |
| &data)); |
| |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(callback_called); |
| EXPECT_EQ(1u, firestore_service_.get_document_records.size()); |
| |
| // Create empty response with no data field. |
| google::firestore::v1beta1::Document response; |
| firestore_service_.get_document_records.front().callback(grpc::Status(), |
| response); |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(callback_called); |
| EXPECT_EQ(cloud_provider::Status::PARSE_ERROR, status); |
| } |
| |
| TEST_F(PageCloudImplTest, SetWatcherResultOk) { |
| bool callback_called = false; |
| auto status = cloud_provider::Status::INTERNAL_ERROR; |
| TestPageCloudWatcher watcher_impl; |
| cloud_provider::PageCloudWatcherPtr watcher; |
| fidl::Binding<cloud_provider::PageCloudWatcher> watcher_binding( |
| &watcher_impl, watcher.NewRequest()); |
| page_cloud_->SetWatcher( |
| nullptr, std::move(watcher), |
| callback::Capture(callback::SetWhenCalled(&callback_called), &status)); |
| |
| RunLoopUntilIdle(); |
| EXPECT_EQ(1u, firestore_service_.listen_clients.size()); |
| EXPECT_FALSE(callback_called); |
| |
| auto response = google::firestore::v1beta1::ListenResponse(); |
| response.mutable_target_change()->set_target_change_type( |
| google::firestore::v1beta1::TargetChange_TargetChangeType_CURRENT); |
| firestore_service_.listen_clients[0]->OnResponse(std::move(response)); |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(callback_called); |
| EXPECT_EQ(cloud_provider::Status::OK, status); |
| } |
| |
| TEST_F(PageCloudImplTest, SetWatcherGetCommits) { |
| bool callback_called = false; |
| auto status = cloud_provider::Status::INTERNAL_ERROR; |
| TestPageCloudWatcher watcher_impl; |
| cloud_provider::PageCloudWatcherPtr watcher; |
| fidl::Binding<cloud_provider::PageCloudWatcher> watcher_binding( |
| &watcher_impl, watcher.NewRequest()); |
| page_cloud_->SetWatcher( |
| nullptr, std::move(watcher), |
| callback::Capture(callback::SetWhenCalled(&callback_called), &status)); |
| |
| RunLoopUntilIdle(); |
| EXPECT_EQ(1u, firestore_service_.listen_clients.size()); |
| EXPECT_FALSE(callback_called); |
| |
| { |
| auto response = google::firestore::v1beta1::ListenResponse(); |
| response.mutable_target_change()->set_target_change_type( |
| google::firestore::v1beta1::TargetChange_TargetChangeType_CURRENT); |
| firestore_service_.listen_clients[0]->OnResponse(std::move(response)); |
| } |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(callback_called); |
| EXPECT_EQ(cloud_provider::Status::OK, status); |
| |
| std::vector<cloud_provider::CommitPackEntry> entries{{"id0", "data0"}}; |
| google::protobuf::Timestamp protobuf_timestamp; |
| ASSERT_TRUE(google::protobuf::util::TimeUtil::FromString( |
| "2018-06-26T14:39:22+00:00", &protobuf_timestamp)); |
| std::string original_timestamp; |
| ASSERT_TRUE(protobuf_timestamp.SerializeToString(&original_timestamp)); |
| |
| cloud_provider::CommitPack commit_pack; |
| ASSERT_TRUE(cloud_provider::EncodeCommitPack(entries, &commit_pack)); |
| auto response = google::firestore::v1beta1::ListenResponse(); |
| ASSERT_TRUE(EncodeCommitBatchWithTimestamp( |
| commit_pack, original_timestamp, |
| response.mutable_document_change()->mutable_document())); |
| firestore_service_.listen_clients[0]->OnResponse(std::move(response)); |
| |
| RunLoopUntilIdle(); |
| EXPECT_EQ(1u, watcher_impl.received_commits.size()); |
| EXPECT_EQ("id0", convert::ToString(watcher_impl.received_commits.front().id)); |
| EXPECT_EQ("data0", |
| convert::ToString(watcher_impl.received_commits.front().data)); |
| EXPECT_EQ(1u, watcher_impl.received_tokens.size()); |
| EXPECT_EQ(original_timestamp, |
| convert::ToString(watcher_impl.received_tokens.front().opaque_id)); |
| EXPECT_TRUE(watcher_impl.pending_on_new_commit_callback); |
| } |
| |
| TEST_F(PageCloudImplTest, SetWatcherNotificationOneAtATime) { |
| bool callback_called = false; |
| auto status = cloud_provider::Status::INTERNAL_ERROR; |
| TestPageCloudWatcher watcher_impl; |
| cloud_provider::PageCloudWatcherPtr watcher; |
| fidl::Binding<cloud_provider::PageCloudWatcher> watcher_binding( |
| &watcher_impl, watcher.NewRequest()); |
| page_cloud_->SetWatcher( |
| nullptr, std::move(watcher), |
| callback::Capture(callback::SetWhenCalled(&callback_called), &status)); |
| |
| RunLoopUntilIdle(); |
| |
| { |
| auto response = google::firestore::v1beta1::ListenResponse(); |
| response.mutable_target_change()->set_target_change_type( |
| google::firestore::v1beta1::TargetChange_TargetChangeType_CURRENT); |
| firestore_service_.listen_clients[0]->OnResponse(std::move(response)); |
| } |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(callback_called); |
| EXPECT_EQ(cloud_provider::Status::OK, status); |
| |
| // Deliver a commit notificiation from the cloud. |
| { |
| std::vector<cloud_provider::CommitPackEntry> entries{{"id0", "data0"}}; |
| google::protobuf::Timestamp protobuf_timestamp; |
| ASSERT_TRUE(google::protobuf::util::TimeUtil::FromString( |
| "2018-06-26T14:39:22+00:00", &protobuf_timestamp)); |
| std::string timestamp; |
| ASSERT_TRUE(protobuf_timestamp.SerializeToString(×tamp)); |
| |
| cloud_provider::CommitPack commit_pack; |
| ASSERT_TRUE(cloud_provider::EncodeCommitPack(entries, &commit_pack)); |
| auto response = google::firestore::v1beta1::ListenResponse(); |
| ASSERT_TRUE(EncodeCommitBatchWithTimestamp( |
| commit_pack, timestamp, |
| response.mutable_document_change()->mutable_document())); |
| firestore_service_.listen_clients[0]->OnResponse(std::move(response)); |
| } |
| |
| // Verify that the notification was delivered to the watcher. |
| RunLoopUntilIdle(); |
| EXPECT_EQ(1u, watcher_impl.received_commits.size()); |
| EXPECT_TRUE(watcher_impl.pending_on_new_commit_callback); |
| |
| // Deliver another commit notificiation from the cloud without calling the |
| // watcher pending callback. |
| { |
| std::vector<cloud_provider::CommitPackEntry> entries{{"id1", "data1"}}; |
| google::protobuf::Timestamp protobuf_timestamp; |
| ASSERT_TRUE(google::protobuf::util::TimeUtil::FromString( |
| "2018-06-26T14:39:24+00:00", &protobuf_timestamp)); |
| std::string timestamp; |
| ASSERT_TRUE(protobuf_timestamp.SerializeToString(×tamp)); |
| |
| cloud_provider::CommitPack commit_pack; |
| ASSERT_TRUE(cloud_provider::EncodeCommitPack(entries, &commit_pack)); |
| auto response = google::firestore::v1beta1::ListenResponse(); |
| ASSERT_TRUE(EncodeCommitBatchWithTimestamp( |
| commit_pack, timestamp, |
| response.mutable_document_change()->mutable_document())); |
| firestore_service_.listen_clients[0]->OnResponse(std::move(response)); |
| } |
| |
| // Verify that another commit notification was not delivered. |
| RunLoopUntilIdle(); |
| EXPECT_EQ(1u, watcher_impl.received_commits.size()); |
| EXPECT_TRUE(watcher_impl.pending_on_new_commit_callback); |
| |
| // Call the pending callback and verify that the new commit notification was |
| // delivered. |
| watcher_impl.pending_on_new_commit_callback(); |
| watcher_impl.pending_on_new_commit_callback = nullptr; |
| RunLoopUntilIdle(); |
| EXPECT_EQ(2u, watcher_impl.received_commits.size()); |
| EXPECT_EQ("id0", convert::ToString(watcher_impl.received_commits[0].id)); |
| EXPECT_EQ("id1", convert::ToString(watcher_impl.received_commits[1].id)); |
| EXPECT_TRUE(watcher_impl.pending_on_new_commit_callback); |
| } |
| |
| } // namespace |
| } // namespace cloud_provider_firestore |