Skip to content

Firebase Storage: Implement List and ListAll API for StorageReference #1726

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions storage/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ set(common_SRCS
src/common/common.cc
src/common/controller.cc
src/common/listener.cc
src/common/list_result.cc
src/common/metadata.cc
src/common/storage.cc
src/common/storage_reference.cc
Expand All @@ -36,6 +37,7 @@ binary_to_array("storage_resources"
set(android_SRCS
${storage_resources_source}
src/android/controller_android.cc
src/android/list_result_android.cc
src/android/metadata_android.cc
src/android/storage_android.cc
src/android/storage_reference_android.cc)
Expand All @@ -44,6 +46,7 @@ set(android_SRCS
set(ios_SRCS
src/ios/controller_ios.mm
src/ios/listener_ios.mm
src/ios/list_result_ios.mm
src/ios/metadata_ios.mm
src/ios/storage_ios.mm
src/ios/storage_reference_ios.mm
Expand All @@ -54,6 +57,7 @@ set(desktop_SRCS
src/desktop/controller_desktop.cc
src/desktop/curl_requests.cc
src/desktop/listener_desktop.cc
src/desktop/list_result_desktop.cc
src/desktop/metadata_desktop.cc
src/desktop/rest_operation.cc
src/desktop/storage_desktop.cc
Expand Down
284 changes: 283 additions & 1 deletion storage/integration_test/src/integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <cstring>
#include <ctime>
#include <thread> // NOLINT
#include <vector> // For std::vector in list tests

#include "app_framework.h" // NOLINT
#include "firebase/app.h"
Expand Down Expand Up @@ -80,6 +81,8 @@ using app_framework::PathForResource;
using app_framework::ProcessEvents;
using firebase_test_framework::FirebaseTest;
using testing::ElementsAreArray;
using testing::IsEmpty;
using testing::UnorderedElementsAreArray;

class FirebaseStorageTest : public FirebaseTest {
public:
Expand All @@ -96,7 +99,6 @@ class FirebaseStorageTest : public FirebaseTest {
// Called after each test.
void TearDown() override;

// File references that we need to delete on test exit.
protected:
// Initialize Firebase App and Firebase Auth.
static void InitializeAppAndAuth();
Expand All @@ -118,6 +120,17 @@ class FirebaseStorageTest : public FirebaseTest {
// Create a unique working folder and return a reference to it.
firebase::storage::StorageReference CreateFolder();

// Uploads a string as a file to the given StorageReference.
void UploadStringAsFile(firebase::storage::StorageReference& ref,
const std::string& content,
const char* content_type = nullptr);

// Verifies the contents of a ListResult.
void VerifyListResultContains(
const firebase::storage::ListResult& list_result,
const std::vector<std::string>& expected_item_names,
const std::vector<std::string>& expected_prefix_names);

static firebase::App* shared_app_;
static firebase::auth::Auth* shared_auth_;

Expand Down Expand Up @@ -212,6 +225,7 @@ void FirebaseStorageTest::TerminateAppAndAuth() {
void FirebaseStorageTest::SetUp() {
FirebaseTest::SetUp();
InitializeStorage();
// list_test_root_ removed from SetUp
}

void FirebaseStorageTest::TearDown() {
Expand Down Expand Up @@ -313,6 +327,65 @@ void FirebaseStorageTest::SignOut() {
EXPECT_FALSE(shared_auth_->current_user().is_valid());
}

void FirebaseStorageTest::UploadStringAsFile(
firebase::storage::StorageReference& ref, const std::string& content,
const char* content_type) {
LogDebug("Uploading string content to: gs://%s%s", ref.bucket().c_str(),
ref.full_path().c_str());
firebase::storage::Metadata metadata;
if (content_type) {
metadata.set_content_type(content_type);
}
firebase::Future<firebase::storage::Metadata> future =
RunWithRetry<firebase::storage::Metadata>([&]() {
return ref.PutBytes(content.c_str(), content.length(), metadata);
});
WaitForCompletion(future, "UploadStringAsFile");
ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
<< "Failed to upload to " << ref.full_path() << ": "
<< future.error_message();
ASSERT_NE(future.result(), nullptr);
// On some platforms (iOS), size_bytes might not be immediately available or
// might be 0 if the upload was very fast and metadata propagation is slow.
// For small files, this is less critical than the content being there.
// For larger files in other tests, size_bytes is asserted.
// ASSERT_EQ(future.result()->size_bytes(), content.length());
cleanup_files_.push_back(ref);
}

void FirebaseStorageTest::VerifyListResultContains(
const firebase::storage::ListResult& list_result,
const std::vector<std::string>& expected_item_names,
const std::vector<std::string>& expected_prefix_names) {
ASSERT_TRUE(list_result.is_valid());

std::vector<std::string> actual_item_names;
for (const auto& item_ref : list_result.items()) {
actual_item_names.push_back(item_ref.name());
}
std::sort(actual_item_names.begin(), actual_item_names.end());
std::vector<std::string> sorted_expected_item_names = expected_item_names;
std::sort(sorted_expected_item_names.begin(),
sorted_expected_item_names.end());

EXPECT_THAT(actual_item_names,
::testing::ContainerEq(sorted_expected_item_names))
<< "Item names do not match expected.";

std::vector<std::string> actual_prefix_names;
for (const auto& prefix_ref : list_result.prefixes()) {
actual_prefix_names.push_back(prefix_ref.name());
}
std::sort(actual_prefix_names.begin(), actual_prefix_names.end());
std::vector<std::string> sorted_expected_prefix_names = expected_prefix_names;
std::sort(sorted_expected_prefix_names.begin(),
sorted_expected_prefix_names.end());

EXPECT_THAT(actual_prefix_names,
::testing::ContainerEq(sorted_expected_prefix_names))
<< "Prefix names do not match expected.";
}

firebase::storage::StorageReference FirebaseStorageTest::CreateFolder() {
// Generate a folder for the test data based on the time in milliseconds.
int64_t time_in_microseconds = GetCurrentTimeInMicroseconds();
Expand Down Expand Up @@ -1622,4 +1695,213 @@ TEST_F(FirebaseStorageTest, TestInvalidatingReferencesWhenDeletingApp) {
InitializeAppAndAuth();
}

TEST_F(FirebaseStorageTest, ListAllBasic) {
// SKIP_TEST_ON_ANDROID_EMULATOR; // Removed
SignIn();
firebase::storage::StorageReference test_root =
CreateFolder().Child("list_all_basic_root");
ASSERT_TRUE(test_root.is_valid())
<< "Test root for ListAllBasic is not valid.";

UploadStringAsFile(test_root.Child("file_a.txt"), "content_a");
UploadStringAsFile(test_root.Child("file_b.txt"), "content_b");
UploadStringAsFile(test_root.Child("prefix1/file_c.txt"),
"content_c_in_prefix1");
UploadStringAsFile(test_root.Child("prefix2/file_e.txt"),
"content_e_in_prefix2");

LogDebug("Calling ListAll() on gs://%s%s", test_root.bucket().c_str(),
test_root.full_path().c_str());
firebase::Future<firebase::storage::ListResult> future = test_root.ListAll();
WaitForCompletion(future, "ListAllBasic");

ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
<< future.error_message();
ASSERT_NE(future.result(), nullptr);
const firebase::storage::ListResult* result = future.result();

VerifyListResultContains(*result, {"file_a.txt", "file_b.txt"},
{"prefix1/", "prefix2/"});
EXPECT_TRUE(result->page_token().empty())
<< "Page token should be empty for ListAll.";
}

TEST_F(FirebaseStorageTest, ListPaginated) {
// SKIP_TEST_ON_ANDROID_EMULATOR; // Removed
SignIn();
firebase::storage::StorageReference test_root =
CreateFolder().Child("list_paginated_root");
ASSERT_TRUE(test_root.is_valid())
<< "Test root for ListPaginated is not valid.";

// Expected total entries: file_aa.txt, file_bb.txt, file_ee.txt, prefix_x/,
// prefix_y/ (5 entries)
UploadStringAsFile(test_root.Child("file_aa.txt"), "content_aa");
UploadStringAsFile(test_root.Child("prefix_x/file_cc.txt"),
"content_cc_in_prefix_x");
UploadStringAsFile(test_root.Child("file_bb.txt"), "content_bb");
UploadStringAsFile(test_root.Child("prefix_y/file_dd.txt"),
"content_dd_in_prefix_y");
UploadStringAsFile(test_root.Child("file_ee.txt"), "content_ee");

std::vector<std::string> all_item_names_collected;
std::vector<std::string> all_prefix_names_collected;
std::string page_token = "";
const int page_size = 2;
int page_count = 0;
const int max_pages = 5; // Safety break for loop

LogDebug("Starting paginated List() on gs://%s%s with page_size %d",
test_root.bucket().c_str(), test_root.full_path().c_str(),
page_size);

do {
page_count++;
LogDebug("Fetching page %d, token: '%s'", page_count, page_token.c_str());
firebase::Future<firebase::storage::ListResult> future =
page_token.empty() ? test_root.List(page_size)
: test_root.List(page_size, page_token.c_str());
WaitForCompletion(future,
"ListPaginated - Page " + std::to_string(page_count));

ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
<< future.error_message();
ASSERT_NE(future.result(), nullptr);
const firebase::storage::ListResult* result = future.result();
ASSERT_TRUE(result->is_valid());

LogDebug("Page %d items: %zu, prefixes: %zu", page_count,
result->items().size(), result->prefixes().size());
for (const auto& item : result->items()) {
all_item_names_collected.push_back(item.name());
LogDebug(" Item: %s", item.name().c_str());
}
for (const auto& prefix : result->prefixes()) {
all_prefix_names_collected.push_back(prefix.name());
LogDebug(" Prefix: %s", prefix.name().c_str());
}

page_token = result->page_token();

size_t entries_on_page = result->items().size() + result->prefixes().size();

if (!page_token.empty()) {
EXPECT_EQ(entries_on_page, page_size)
<< "A non-last page should have full page_size entries.";
} else {
// This is the last page
size_t total_entries = 5;
size_t expected_entries_on_last_page = total_entries % page_size;
if (expected_entries_on_last_page == 0 &&
total_entries > 0) { // if total is a multiple of page_size
expected_entries_on_last_page = page_size;
}
EXPECT_EQ(entries_on_page, expected_entries_on_last_page);
}
} while (!page_token.empty() && page_count < max_pages);

EXPECT_LT(page_count, max_pages)
<< "Exceeded max_pages, possible infinite loop.";
EXPECT_EQ(page_count, (5 + page_size - 1) / page_size)
<< "Unexpected number of pages.";

std::vector<std::string> expected_final_items = {"file_aa.txt", "file_bb.txt",
"file_ee.txt"};
std::vector<std::string> expected_final_prefixes = {"prefix_x/", "prefix_y/"};

// VerifyListResultContains needs a ListResult object. We can't directly use
// it with collected names. Instead, we sort and compare the collected names.
std::sort(all_item_names_collected.begin(), all_item_names_collected.end());
std::sort(all_prefix_names_collected.begin(),
all_prefix_names_collected.end());
std::sort(expected_final_items.begin(), expected_final_items.end());
std::sort(expected_final_prefixes.begin(), expected_final_prefixes.end());

EXPECT_THAT(all_item_names_collected,
::testing::ContainerEq(expected_final_items));
EXPECT_THAT(all_prefix_names_collected,
::testing::ContainerEq(expected_final_prefixes));
}

TEST_F(FirebaseStorageTest, ListEmpty) {
// SKIP_TEST_ON_ANDROID_EMULATOR; // No skip needed as it's a lightweight
// test.
SignIn();
firebase::storage::StorageReference test_root =
CreateFolder().Child("list_empty_root");
ASSERT_TRUE(test_root.is_valid()) << "Test root for ListEmpty is not valid.";

// Do not upload anything to test_root.

LogDebug("Calling ListAll() on empty folder: gs://%s%s",
test_root.bucket().c_str(), test_root.full_path().c_str());
firebase::Future<firebase::storage::ListResult> future = test_root.ListAll();
WaitForCompletion(future, "ListEmpty");

ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
<< future.error_message();
ASSERT_NE(future.result(), nullptr);
const firebase::storage::ListResult* result = future.result();

VerifyListResultContains(*result, {}, {});
EXPECT_TRUE(result->page_token().empty());
}

TEST_F(FirebaseStorageTest, ListWithMaxResultsGreaterThanActual) {
// SKIP_TEST_ON_ANDROID_EMULATOR; // No skip needed.
SignIn();
firebase::storage::StorageReference test_root =
CreateFolder().Child("list_max_greater_root");
ASSERT_TRUE(test_root.is_valid())
<< "Test root for ListWithMaxResultsGreaterThanActual is not valid.";

UploadStringAsFile(test_root.Child("only_file.txt"), "content_only");
UploadStringAsFile(test_root.Child("only_prefix/another.txt"),
"content_another_in_prefix");

LogDebug("Calling List(10) on gs://%s%s", test_root.bucket().c_str(),
test_root.full_path().c_str());
firebase::Future<firebase::storage::ListResult> future =
test_root.List(10); // Max results (10) > actual (1 file + 1 prefix = 2)
WaitForCompletion(future, "ListWithMaxResultsGreaterThanActual");

ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
<< future.error_message();
ASSERT_NE(future.result(), nullptr);
const firebase::storage::ListResult* result = future.result();

VerifyListResultContains(*result, {"only_file.txt"}, {"only_prefix/"});
EXPECT_TRUE(result->page_token().empty());
}

TEST_F(FirebaseStorageTest, ListNonExistentPath) {
// SKIP_TEST_ON_ANDROID_EMULATOR; // No skip needed.
SignIn();
firebase::storage::StorageReference test_root =
CreateFolder().Child("list_non_existent_parent_root");
ASSERT_TRUE(test_root.is_valid())
<< "Test root for ListNonExistentPath is not valid.";

firebase::storage::StorageReference non_existent_ref =
test_root.Child("this_folder_truly_does_not_exist");
// No cleanup needed as nothing is created.

LogDebug("Calling ListAll() on non-existent path: gs://%s%s",
non_existent_ref.bucket().c_str(),
non_existent_ref.full_path().c_str());
firebase::Future<firebase::storage::ListResult> future =
non_existent_ref.ListAll();
WaitForCompletion(future, "ListNonExistentPath");

// Listing a non-existent path should not be an error, it's just an empty
// list.
ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
<< future.error_message();
ASSERT_NE(future.result(), nullptr);
const firebase::storage::ListResult* result = future.result();

VerifyListResultContains(*result, {}, {});
EXPECT_TRUE(result->page_token().empty());
}

} // namespace firebase_testapp_automated
Loading
Loading