Skip to content

Commit b28ff1d

Browse files
authored
Merge 61e6bbb into e171c8a
2 parents e171c8a + 61e6bbb commit b28ff1d

19 files changed

+1663
-4
lines changed

storage/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ set(common_SRCS
1919
src/common/common.cc
2020
src/common/controller.cc
2121
src/common/listener.cc
22+
src/common/list_result.cc
2223
src/common/metadata.cc
2324
src/common/storage.cc
2425
src/common/storage_reference.cc
@@ -36,6 +37,7 @@ binary_to_array("storage_resources"
3637
set(android_SRCS
3738
${storage_resources_source}
3839
src/android/controller_android.cc
40+
src/android/list_result_android.cc
3941
src/android/metadata_android.cc
4042
src/android/storage_android.cc
4143
src/android/storage_reference_android.cc)
@@ -44,6 +46,7 @@ set(android_SRCS
4446
set(ios_SRCS
4547
src/ios/controller_ios.mm
4648
src/ios/listener_ios.mm
49+
src/ios/list_result_ios.mm
4750
src/ios/metadata_ios.mm
4851
src/ios/storage_ios.mm
4952
src/ios/storage_reference_ios.mm
@@ -54,6 +57,7 @@ set(desktop_SRCS
5457
src/desktop/controller_desktop.cc
5558
src/desktop/curl_requests.cc
5659
src/desktop/listener_desktop.cc
60+
src/desktop/list_result_desktop.cc
5761
src/desktop/metadata_desktop.cc
5862
src/desktop/rest_operation.cc
5963
src/desktop/storage_desktop.cc

storage/integration_test/src/integration_test.cc

Lines changed: 283 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <cstring>
2121
#include <ctime>
2222
#include <thread> // NOLINT
23+
#include <vector> // For std::vector in list tests
2324

2425
#include "app_framework.h" // NOLINT
2526
#include "firebase/app.h"
@@ -80,6 +81,8 @@ using app_framework::PathForResource;
8081
using app_framework::ProcessEvents;
8182
using firebase_test_framework::FirebaseTest;
8283
using testing::ElementsAreArray;
84+
using testing::IsEmpty;
85+
using testing::UnorderedElementsAreArray;
8386

8487
class FirebaseStorageTest : public FirebaseTest {
8588
public:
@@ -96,7 +99,6 @@ class FirebaseStorageTest : public FirebaseTest {
9699
// Called after each test.
97100
void TearDown() override;
98101

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

123+
// Uploads a string as a file to the given StorageReference.
124+
void UploadStringAsFile(firebase::storage::StorageReference& ref,
125+
const std::string& content,
126+
const char* content_type = nullptr);
127+
128+
// Verifies the contents of a ListResult.
129+
void VerifyListResultContains(
130+
const firebase::storage::ListResult& list_result,
131+
const std::vector<std::string>& expected_item_names,
132+
const std::vector<std::string>& expected_prefix_names);
133+
121134
static firebase::App* shared_app_;
122135
static firebase::auth::Auth* shared_auth_;
123136

@@ -212,6 +225,7 @@ void FirebaseStorageTest::TerminateAppAndAuth() {
212225
void FirebaseStorageTest::SetUp() {
213226
FirebaseTest::SetUp();
214227
InitializeStorage();
228+
// list_test_root_ removed from SetUp
215229
}
216230

217231
void FirebaseStorageTest::TearDown() {
@@ -313,6 +327,65 @@ void FirebaseStorageTest::SignOut() {
313327
EXPECT_FALSE(shared_auth_->current_user().is_valid());
314328
}
315329

330+
void FirebaseStorageTest::UploadStringAsFile(
331+
firebase::storage::StorageReference& ref, const std::string& content,
332+
const char* content_type) {
333+
LogDebug("Uploading string content to: gs://%s%s", ref.bucket().c_str(),
334+
ref.full_path().c_str());
335+
firebase::storage::Metadata metadata;
336+
if (content_type) {
337+
metadata.set_content_type(content_type);
338+
}
339+
firebase::Future<firebase::storage::Metadata> future =
340+
RunWithRetry<firebase::storage::Metadata>([&]() {
341+
return ref.PutBytes(content.c_str(), content.length(), metadata);
342+
});
343+
WaitForCompletion(future, "UploadStringAsFile");
344+
ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
345+
<< "Failed to upload to " << ref.full_path() << ": "
346+
<< future.error_message();
347+
ASSERT_NE(future.result(), nullptr);
348+
// On some platforms (iOS), size_bytes might not be immediately available or
349+
// might be 0 if the upload was very fast and metadata propagation is slow.
350+
// For small files, this is less critical than the content being there.
351+
// For larger files in other tests, size_bytes is asserted.
352+
// ASSERT_EQ(future.result()->size_bytes(), content.length());
353+
cleanup_files_.push_back(ref);
354+
}
355+
356+
void FirebaseStorageTest::VerifyListResultContains(
357+
const firebase::storage::ListResult& list_result,
358+
const std::vector<std::string>& expected_item_names,
359+
const std::vector<std::string>& expected_prefix_names) {
360+
ASSERT_TRUE(list_result.is_valid());
361+
362+
std::vector<std::string> actual_item_names;
363+
for (const auto& item_ref : list_result.items()) {
364+
actual_item_names.push_back(item_ref.name());
365+
}
366+
std::sort(actual_item_names.begin(), actual_item_names.end());
367+
std::vector<std::string> sorted_expected_item_names = expected_item_names;
368+
std::sort(sorted_expected_item_names.begin(),
369+
sorted_expected_item_names.end());
370+
371+
EXPECT_THAT(actual_item_names,
372+
::testing::ContainerEq(sorted_expected_item_names))
373+
<< "Item names do not match expected.";
374+
375+
std::vector<std::string> actual_prefix_names;
376+
for (const auto& prefix_ref : list_result.prefixes()) {
377+
actual_prefix_names.push_back(prefix_ref.name());
378+
}
379+
std::sort(actual_prefix_names.begin(), actual_prefix_names.end());
380+
std::vector<std::string> sorted_expected_prefix_names = expected_prefix_names;
381+
std::sort(sorted_expected_prefix_names.begin(),
382+
sorted_expected_prefix_names.end());
383+
384+
EXPECT_THAT(actual_prefix_names,
385+
::testing::ContainerEq(sorted_expected_prefix_names))
386+
<< "Prefix names do not match expected.";
387+
}
388+
316389
firebase::storage::StorageReference FirebaseStorageTest::CreateFolder() {
317390
// Generate a folder for the test data based on the time in milliseconds.
318391
int64_t time_in_microseconds = GetCurrentTimeInMicroseconds();
@@ -1622,4 +1695,213 @@ TEST_F(FirebaseStorageTest, TestInvalidatingReferencesWhenDeletingApp) {
16221695
InitializeAppAndAuth();
16231696
}
16241697

1698+
TEST_F(FirebaseStorageTest, ListAllBasic) {
1699+
// SKIP_TEST_ON_ANDROID_EMULATOR; // Removed
1700+
SignIn();
1701+
firebase::storage::StorageReference test_root =
1702+
CreateFolder().Child("list_all_basic_root");
1703+
ASSERT_TRUE(test_root.is_valid())
1704+
<< "Test root for ListAllBasic is not valid.";
1705+
1706+
UploadStringAsFile(test_root.Child("file_a.txt"), "content_a");
1707+
UploadStringAsFile(test_root.Child("file_b.txt"), "content_b");
1708+
UploadStringAsFile(test_root.Child("prefix1/file_c.txt"),
1709+
"content_c_in_prefix1");
1710+
UploadStringAsFile(test_root.Child("prefix2/file_e.txt"),
1711+
"content_e_in_prefix2");
1712+
1713+
LogDebug("Calling ListAll() on gs://%s%s", test_root.bucket().c_str(),
1714+
test_root.full_path().c_str());
1715+
firebase::Future<firebase::storage::ListResult> future = test_root.ListAll();
1716+
WaitForCompletion(future, "ListAllBasic");
1717+
1718+
ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
1719+
<< future.error_message();
1720+
ASSERT_NE(future.result(), nullptr);
1721+
const firebase::storage::ListResult* result = future.result();
1722+
1723+
VerifyListResultContains(*result, {"file_a.txt", "file_b.txt"},
1724+
{"prefix1/", "prefix2/"});
1725+
EXPECT_TRUE(result->page_token().empty())
1726+
<< "Page token should be empty for ListAll.";
1727+
}
1728+
1729+
TEST_F(FirebaseStorageTest, ListPaginated) {
1730+
// SKIP_TEST_ON_ANDROID_EMULATOR; // Removed
1731+
SignIn();
1732+
firebase::storage::StorageReference test_root =
1733+
CreateFolder().Child("list_paginated_root");
1734+
ASSERT_TRUE(test_root.is_valid())
1735+
<< "Test root for ListPaginated is not valid.";
1736+
1737+
// Expected total entries: file_aa.txt, file_bb.txt, file_ee.txt, prefix_x/,
1738+
// prefix_y/ (5 entries)
1739+
UploadStringAsFile(test_root.Child("file_aa.txt"), "content_aa");
1740+
UploadStringAsFile(test_root.Child("prefix_x/file_cc.txt"),
1741+
"content_cc_in_prefix_x");
1742+
UploadStringAsFile(test_root.Child("file_bb.txt"), "content_bb");
1743+
UploadStringAsFile(test_root.Child("prefix_y/file_dd.txt"),
1744+
"content_dd_in_prefix_y");
1745+
UploadStringAsFile(test_root.Child("file_ee.txt"), "content_ee");
1746+
1747+
std::vector<std::string> all_item_names_collected;
1748+
std::vector<std::string> all_prefix_names_collected;
1749+
std::string page_token = "";
1750+
const int page_size = 2;
1751+
int page_count = 0;
1752+
const int max_pages = 5; // Safety break for loop
1753+
1754+
LogDebug("Starting paginated List() on gs://%s%s with page_size %d",
1755+
test_root.bucket().c_str(), test_root.full_path().c_str(),
1756+
page_size);
1757+
1758+
do {
1759+
page_count++;
1760+
LogDebug("Fetching page %d, token: '%s'", page_count, page_token.c_str());
1761+
firebase::Future<firebase::storage::ListResult> future =
1762+
page_token.empty() ? test_root.List(page_size)
1763+
: test_root.List(page_size, page_token.c_str());
1764+
WaitForCompletion(future,
1765+
"ListPaginated - Page " + std::to_string(page_count));
1766+
1767+
ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
1768+
<< future.error_message();
1769+
ASSERT_NE(future.result(), nullptr);
1770+
const firebase::storage::ListResult* result = future.result();
1771+
ASSERT_TRUE(result->is_valid());
1772+
1773+
LogDebug("Page %d items: %zu, prefixes: %zu", page_count,
1774+
result->items().size(), result->prefixes().size());
1775+
for (const auto& item : result->items()) {
1776+
all_item_names_collected.push_back(item.name());
1777+
LogDebug(" Item: %s", item.name().c_str());
1778+
}
1779+
for (const auto& prefix : result->prefixes()) {
1780+
all_prefix_names_collected.push_back(prefix.name());
1781+
LogDebug(" Prefix: %s", prefix.name().c_str());
1782+
}
1783+
1784+
page_token = result->page_token();
1785+
1786+
size_t entries_on_page = result->items().size() + result->prefixes().size();
1787+
1788+
if (!page_token.empty()) {
1789+
EXPECT_EQ(entries_on_page, page_size)
1790+
<< "A non-last page should have full page_size entries.";
1791+
} else {
1792+
// This is the last page
1793+
size_t total_entries = 5;
1794+
size_t expected_entries_on_last_page = total_entries % page_size;
1795+
if (expected_entries_on_last_page == 0 &&
1796+
total_entries > 0) { // if total is a multiple of page_size
1797+
expected_entries_on_last_page = page_size;
1798+
}
1799+
EXPECT_EQ(entries_on_page, expected_entries_on_last_page);
1800+
}
1801+
} while (!page_token.empty() && page_count < max_pages);
1802+
1803+
EXPECT_LT(page_count, max_pages)
1804+
<< "Exceeded max_pages, possible infinite loop.";
1805+
EXPECT_EQ(page_count, (5 + page_size - 1) / page_size)
1806+
<< "Unexpected number of pages.";
1807+
1808+
std::vector<std::string> expected_final_items = {"file_aa.txt", "file_bb.txt",
1809+
"file_ee.txt"};
1810+
std::vector<std::string> expected_final_prefixes = {"prefix_x/", "prefix_y/"};
1811+
1812+
// VerifyListResultContains needs a ListResult object. We can't directly use
1813+
// it with collected names. Instead, we sort and compare the collected names.
1814+
std::sort(all_item_names_collected.begin(), all_item_names_collected.end());
1815+
std::sort(all_prefix_names_collected.begin(),
1816+
all_prefix_names_collected.end());
1817+
std::sort(expected_final_items.begin(), expected_final_items.end());
1818+
std::sort(expected_final_prefixes.begin(), expected_final_prefixes.end());
1819+
1820+
EXPECT_THAT(all_item_names_collected,
1821+
::testing::ContainerEq(expected_final_items));
1822+
EXPECT_THAT(all_prefix_names_collected,
1823+
::testing::ContainerEq(expected_final_prefixes));
1824+
}
1825+
1826+
TEST_F(FirebaseStorageTest, ListEmpty) {
1827+
// SKIP_TEST_ON_ANDROID_EMULATOR; // No skip needed as it's a lightweight
1828+
// test.
1829+
SignIn();
1830+
firebase::storage::StorageReference test_root =
1831+
CreateFolder().Child("list_empty_root");
1832+
ASSERT_TRUE(test_root.is_valid()) << "Test root for ListEmpty is not valid.";
1833+
1834+
// Do not upload anything to test_root.
1835+
1836+
LogDebug("Calling ListAll() on empty folder: gs://%s%s",
1837+
test_root.bucket().c_str(), test_root.full_path().c_str());
1838+
firebase::Future<firebase::storage::ListResult> future = test_root.ListAll();
1839+
WaitForCompletion(future, "ListEmpty");
1840+
1841+
ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
1842+
<< future.error_message();
1843+
ASSERT_NE(future.result(), nullptr);
1844+
const firebase::storage::ListResult* result = future.result();
1845+
1846+
VerifyListResultContains(*result, {}, {});
1847+
EXPECT_TRUE(result->page_token().empty());
1848+
}
1849+
1850+
TEST_F(FirebaseStorageTest, ListWithMaxResultsGreaterThanActual) {
1851+
// SKIP_TEST_ON_ANDROID_EMULATOR; // No skip needed.
1852+
SignIn();
1853+
firebase::storage::StorageReference test_root =
1854+
CreateFolder().Child("list_max_greater_root");
1855+
ASSERT_TRUE(test_root.is_valid())
1856+
<< "Test root for ListWithMaxResultsGreaterThanActual is not valid.";
1857+
1858+
UploadStringAsFile(test_root.Child("only_file.txt"), "content_only");
1859+
UploadStringAsFile(test_root.Child("only_prefix/another.txt"),
1860+
"content_another_in_prefix");
1861+
1862+
LogDebug("Calling List(10) on gs://%s%s", test_root.bucket().c_str(),
1863+
test_root.full_path().c_str());
1864+
firebase::Future<firebase::storage::ListResult> future =
1865+
test_root.List(10); // Max results (10) > actual (1 file + 1 prefix = 2)
1866+
WaitForCompletion(future, "ListWithMaxResultsGreaterThanActual");
1867+
1868+
ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
1869+
<< future.error_message();
1870+
ASSERT_NE(future.result(), nullptr);
1871+
const firebase::storage::ListResult* result = future.result();
1872+
1873+
VerifyListResultContains(*result, {"only_file.txt"}, {"only_prefix/"});
1874+
EXPECT_TRUE(result->page_token().empty());
1875+
}
1876+
1877+
TEST_F(FirebaseStorageTest, ListNonExistentPath) {
1878+
// SKIP_TEST_ON_ANDROID_EMULATOR; // No skip needed.
1879+
SignIn();
1880+
firebase::storage::StorageReference test_root =
1881+
CreateFolder().Child("list_non_existent_parent_root");
1882+
ASSERT_TRUE(test_root.is_valid())
1883+
<< "Test root for ListNonExistentPath is not valid.";
1884+
1885+
firebase::storage::StorageReference non_existent_ref =
1886+
test_root.Child("this_folder_truly_does_not_exist");
1887+
// No cleanup needed as nothing is created.
1888+
1889+
LogDebug("Calling ListAll() on non-existent path: gs://%s%s",
1890+
non_existent_ref.bucket().c_str(),
1891+
non_existent_ref.full_path().c_str());
1892+
firebase::Future<firebase::storage::ListResult> future =
1893+
non_existent_ref.ListAll();
1894+
WaitForCompletion(future, "ListNonExistentPath");
1895+
1896+
// Listing a non-existent path should not be an error, it's just an empty
1897+
// list.
1898+
ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
1899+
<< future.error_message();
1900+
ASSERT_NE(future.result(), nullptr);
1901+
const firebase::storage::ListResult* result = future.result();
1902+
1903+
VerifyListResultContains(*result, {}, {});
1904+
EXPECT_TRUE(result->page_token().empty());
1905+
}
1906+
16251907
} // namespace firebase_testapp_automated

0 commit comments

Comments
 (0)