20
20
#include < cstring>
21
21
#include < ctime>
22
22
#include < thread> // NOLINT
23
+ #include < vector> // For std::vector in list tests
23
24
24
25
#include " app_framework.h" // NOLINT
25
26
#include " firebase/app.h"
@@ -80,6 +81,9 @@ using app_framework::PathForResource;
80
81
using app_framework::ProcessEvents;
81
82
using firebase_test_framework::FirebaseTest;
82
83
using testing::ElementsAreArray;
84
+ using testing::IsEmpty;
85
+ using testing::UnorderedElementsAreArray;
86
+
83
87
84
88
class FirebaseStorageTest : public FirebaseTest {
85
89
public:
@@ -96,8 +100,10 @@ class FirebaseStorageTest : public FirebaseTest {
96
100
// Called after each test.
97
101
void TearDown () override ;
98
102
99
- // File references that we need to delete on test exit.
100
103
protected:
104
+ // Root reference for list tests.
105
+ firebase::storage::StorageReference list_test_root_;
106
+
101
107
// Initialize Firebase App and Firebase Auth.
102
108
static void InitializeAppAndAuth ();
103
109
// Shut down Firebase App and Firebase Auth.
@@ -118,6 +124,18 @@ class FirebaseStorageTest : public FirebaseTest {
118
124
// Create a unique working folder and return a reference to it.
119
125
firebase::storage::StorageReference CreateFolder ();
120
126
127
+ // Uploads a string as a file to the given StorageReference.
128
+ void UploadStringAsFile (
129
+ firebase::storage::StorageReference& ref, const std::string& content,
130
+ const char * content_type = nullptr );
131
+
132
+ // Verifies the contents of a ListResult.
133
+ void VerifyListResultContains (
134
+ const firebase::storage::ListResult& list_result,
135
+ const std::vector<std::string>& expected_item_names,
136
+ const std::vector<std::string>& expected_prefix_names);
137
+
138
+
121
139
static firebase::App* shared_app_;
122
140
static firebase::auth::Auth* shared_auth_;
123
141
@@ -212,6 +230,16 @@ void FirebaseStorageTest::TerminateAppAndAuth() {
212
230
void FirebaseStorageTest::SetUp () {
213
231
FirebaseTest::SetUp ();
214
232
InitializeStorage ();
233
+ if (storage_ != nullptr && storage_->GetReference ().is_valid ()) {
234
+ list_test_root_ = CreateFolder ().Child (" list_tests_root" );
235
+ // list_test_root_ itself doesn't need to be in cleanup_files_ if its parent from CreateFolder() is.
236
+ // However, specific files/folders created under list_test_root_ for each test *will* be added
237
+ // via UploadStringAsFile or by explicitly adding the parent of a set of files for that test.
238
+ } else {
239
+ // Handle cases where storage might not be initialized (e.g. if InitializeStorage fails)
240
+ // by providing a default, invalid reference.
241
+ list_test_root_ = firebase::storage::StorageReference ();
242
+ }
215
243
}
216
244
217
245
void FirebaseStorageTest::TearDown () {
@@ -313,6 +341,62 @@ void FirebaseStorageTest::SignOut() {
313
341
EXPECT_FALSE (shared_auth_->current_user ().is_valid ());
314
342
}
315
343
344
+ void FirebaseStorageTest::UploadStringAsFile (
345
+ firebase::storage::StorageReference& ref, const std::string& content,
346
+ const char * content_type) {
347
+ LogDebug (" Uploading string content to: gs://%s%s" , ref.bucket ().c_str (),
348
+ ref.full_path ().c_str ());
349
+ firebase::storage::Metadata metadata;
350
+ if (content_type) {
351
+ metadata.set_content_type (content_type);
352
+ }
353
+ firebase::Future<firebase::storage::Metadata> future =
354
+ RunWithRetry<firebase::storage::Metadata>(
355
+ [&]() { return ref.PutBytes (content.c_str (), content.length (), metadata); });
356
+ WaitForCompletion (future, " UploadStringAsFile" );
357
+ ASSERT_EQ (future.error (), firebase::storage::kErrorNone )
358
+ << " Failed to upload to " << ref.full_path () << " : "
359
+ << future.error_message ();
360
+ ASSERT_NE (future.result (), nullptr );
361
+ // On some platforms (iOS), size_bytes might not be immediately available or might be 0
362
+ // if the upload was very fast and metadata propagation is slow.
363
+ // For small files, this is less critical than the content being there.
364
+ // For larger files in other tests, size_bytes is asserted.
365
+ // ASSERT_EQ(future.result()->size_bytes(), content.length());
366
+ cleanup_files_.push_back (ref);
367
+ }
368
+
369
+ void FirebaseStorageTest::VerifyListResultContains (
370
+ const firebase::storage::ListResult& list_result,
371
+ const std::vector<std::string>& expected_item_names,
372
+ const std::vector<std::string>& expected_prefix_names) {
373
+ ASSERT_TRUE (list_result.is_valid ());
374
+
375
+ std::vector<std::string> actual_item_names;
376
+ for (const auto & item_ref : list_result.items ()) {
377
+ actual_item_names.push_back (item_ref.name ());
378
+ }
379
+ std::sort (actual_item_names.begin (), actual_item_names.end ());
380
+ std::vector<std::string> sorted_expected_item_names = expected_item_names;
381
+ std::sort (sorted_expected_item_names.begin (), sorted_expected_item_names.end ());
382
+
383
+ EXPECT_THAT (actual_item_names, ::testing::ContainerEq (sorted_expected_item_names))
384
+ << " Item names do not match expected." ;
385
+
386
+
387
+ std::vector<std::string> actual_prefix_names;
388
+ for (const auto & prefix_ref : list_result.prefixes ()) {
389
+ actual_prefix_names.push_back (prefix_ref.name ());
390
+ }
391
+ std::sort (actual_prefix_names.begin (), actual_prefix_names.end ());
392
+ std::vector<std::string> sorted_expected_prefix_names = expected_prefix_names;
393
+ std::sort (sorted_expected_prefix_names.begin (), sorted_expected_prefix_names.end ());
394
+
395
+ EXPECT_THAT (actual_prefix_names, ::testing::ContainerEq (sorted_expected_prefix_names))
396
+ << " Prefix names do not match expected." ;
397
+ }
398
+
399
+
316
400
firebase::storage::StorageReference FirebaseStorageTest::CreateFolder () {
317
401
// Generate a folder for the test data based on the time in milliseconds.
318
402
int64_t time_in_microseconds = GetCurrentTimeInMicroseconds ();
@@ -1622,4 +1706,200 @@ TEST_F(FirebaseStorageTest, TestInvalidatingReferencesWhenDeletingApp) {
1622
1706
InitializeAppAndAuth ();
1623
1707
}
1624
1708
1709
+ TEST_F (FirebaseStorageTest, ListAllBasic) {
1710
+ SKIP_TEST_ON_ANDROID_EMULATOR; // List tests can be slow on emulators or have quota issues.
1711
+ SignIn ();
1712
+ ASSERT_TRUE (list_test_root_.is_valid ()) << " List test root is not valid." ;
1713
+
1714
+ firebase::storage::StorageReference list_all_base =
1715
+ list_test_root_.Child (" list_all_basic_test" );
1716
+ // cleanup_files_.push_back(list_all_base); // Not a file, its contents are files.
1717
+
1718
+ UploadStringAsFile (list_all_base.Child (" file_a.txt" ), " content_a" );
1719
+ UploadStringAsFile (list_all_base.Child (" file_b.txt" ), " content_b" );
1720
+ UploadStringAsFile (list_all_base.Child (" prefix1/file_c.txt" ), " content_c_in_prefix1" );
1721
+ UploadStringAsFile (list_all_base.Child (" prefix2/file_e.txt" ), " content_e_in_prefix2" );
1722
+
1723
+ LogDebug (" Calling ListAll() on gs://%s%s" , list_all_base.bucket ().c_str (),
1724
+ list_all_base.full_path ().c_str ());
1725
+ firebase::Future<firebase::storage::ListResult> future =
1726
+ list_all_base.ListAll ();
1727
+ WaitForCompletion (future, " ListAllBasic" );
1728
+
1729
+ ASSERT_EQ (future.error (), firebase::storage::kErrorNone )
1730
+ << future.error_message ();
1731
+ ASSERT_NE (future.result (), nullptr );
1732
+ const firebase::storage::ListResult* result = future.result ();
1733
+
1734
+ VerifyListResultContains (*result, {" file_a.txt" , " file_b.txt" },
1735
+ {" prefix1/" , " prefix2/" });
1736
+ EXPECT_TRUE (result->page_token ().empty ()) << " Page token should be empty for ListAll." ;
1737
+ }
1738
+
1739
+ TEST_F (FirebaseStorageTest, ListPaginated) {
1740
+ SKIP_TEST_ON_ANDROID_EMULATOR;
1741
+ SignIn ();
1742
+ ASSERT_TRUE (list_test_root_.is_valid ()) << " List test root is not valid." ;
1743
+
1744
+ firebase::storage::StorageReference list_paginated_base =
1745
+ list_test_root_.Child (" list_paginated_test" );
1746
+ // cleanup_files_.push_back(list_paginated_base);
1747
+
1748
+ // Expected total entries: file_aa.txt, file_bb.txt, file_ee.txt, prefix_x/, prefix_y/ (5 entries)
1749
+ UploadStringAsFile (list_paginated_base.Child (" file_aa.txt" ), " content_aa" );
1750
+ UploadStringAsFile (list_paginated_base.Child (" prefix_x/file_cc.txt" ), " content_cc_in_prefix_x" );
1751
+ UploadStringAsFile (list_paginated_base.Child (" file_bb.txt" ), " content_bb" );
1752
+ UploadStringAsFile (list_paginated_base.Child (" prefix_y/file_dd.txt" ), " content_dd_in_prefix_y" );
1753
+ UploadStringAsFile (list_paginated_base.Child (" file_ee.txt" ), " content_ee" );
1754
+
1755
+
1756
+ std::vector<std::string> all_item_names_collected;
1757
+ std::vector<std::string> all_prefix_names_collected;
1758
+ std::string page_token = " " ;
1759
+ const int page_size = 2 ;
1760
+ int page_count = 0 ;
1761
+ const int max_pages = 5 ; // Safety break for loop
1762
+
1763
+ LogDebug (" Starting paginated List() on gs://%s%s with page_size %d" ,
1764
+ list_paginated_base.bucket ().c_str (), list_paginated_base.full_path ().c_str (), page_size);
1765
+
1766
+ do {
1767
+ page_count++;
1768
+ LogDebug (" Fetching page %d, token: '%s'" , page_count, page_token.c_str ());
1769
+ firebase::Future<firebase::storage::ListResult> future =
1770
+ page_token.empty () ? list_paginated_base.List (page_size)
1771
+ : list_paginated_base.List (page_size, page_token.c_str ());
1772
+ WaitForCompletion (future, " ListPaginated - Page " + std::to_string (page_count));
1773
+
1774
+ ASSERT_EQ (future.error (), firebase::storage::kErrorNone ) << future.error_message ();
1775
+ ASSERT_NE (future.result (), nullptr );
1776
+ const firebase::storage::ListResult* result = future.result ();
1777
+ ASSERT_TRUE (result->is_valid ());
1778
+
1779
+ LogDebug (" Page %d items: %zu, prefixes: %zu" , page_count, result->items ().size (), result->prefixes ().size ());
1780
+ for (const auto & item : result->items ()) {
1781
+ all_item_names_collected.push_back (item.name ());
1782
+ LogDebug (" Item: %s" , item.name ().c_str ());
1783
+ }
1784
+ for (const auto & prefix : result->prefixes ()) {
1785
+ all_prefix_names_collected.push_back (prefix.name ());
1786
+ LogDebug (" Prefix: %s" , prefix.name ().c_str ());
1787
+ }
1788
+
1789
+ page_token = result->page_token ();
1790
+
1791
+ size_t entries_on_page = result->items ().size () + result->prefixes ().size ();
1792
+
1793
+ if (!page_token.empty ()) {
1794
+ EXPECT_EQ (entries_on_page, page_size) << " A non-last page should have full page_size entries." ;
1795
+ } else {
1796
+ // This is the last page
1797
+ size_t total_entries = 5 ;
1798
+ size_t expected_entries_on_last_page = total_entries % page_size;
1799
+ if (expected_entries_on_last_page == 0 && total_entries > 0 ) { // if total is a multiple of page_size
1800
+ expected_entries_on_last_page = page_size;
1801
+ }
1802
+ EXPECT_EQ (entries_on_page, expected_entries_on_last_page);
1803
+ }
1804
+ } while (!page_token.empty () && page_count < max_pages);
1805
+
1806
+ EXPECT_LT (page_count, max_pages) << " Exceeded max_pages, possible infinite loop." ;
1807
+ EXPECT_EQ (page_count, (5 + page_size -1 ) / page_size) << " Unexpected number of pages." ;
1808
+
1809
+
1810
+ std::vector<std::string> expected_final_items = {" file_aa.txt" , " file_bb.txt" , " file_ee.txt" };
1811
+ std::vector<std::string> expected_final_prefixes = {" prefix_x/" , " prefix_y/" };
1812
+
1813
+ // VerifyListResultContains needs a ListResult object. We can't directly use it with collected names.
1814
+ // Instead, we sort and compare the collected names.
1815
+ std::sort (all_item_names_collected.begin (), all_item_names_collected.end ());
1816
+ std::sort (all_prefix_names_collected.begin (), 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, ::testing::ContainerEq (expected_final_items));
1821
+ EXPECT_THAT (all_prefix_names_collected, ::testing::ContainerEq (expected_final_prefixes));
1822
+ }
1823
+
1824
+
1825
+ TEST_F (FirebaseStorageTest, ListEmpty) {
1826
+ SKIP_TEST_ON_ANDROID_EMULATOR;
1827
+ SignIn ();
1828
+ ASSERT_TRUE (list_test_root_.is_valid ()) << " List test root is not valid." ;
1829
+
1830
+ firebase::storage::StorageReference list_empty_ref =
1831
+ list_test_root_.Child (" list_empty_folder_test" );
1832
+ // Do not upload anything to this reference.
1833
+ // cleanup_files_.push_back(list_empty_ref); // Not a file
1834
+
1835
+ LogDebug (" Calling ListAll() on empty folder: gs://%s%s" ,
1836
+ list_empty_ref.bucket ().c_str (), list_empty_ref.full_path ().c_str ());
1837
+ firebase::Future<firebase::storage::ListResult> future =
1838
+ list_empty_ref.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;
1852
+ SignIn ();
1853
+ ASSERT_TRUE (list_test_root_.is_valid ()) << " List test root is not valid." ;
1854
+
1855
+ firebase::storage::StorageReference list_max_greater_base =
1856
+ list_test_root_.Child (" list_max_greater_test" );
1857
+ // cleanup_files_.push_back(list_max_greater_base);
1858
+
1859
+ UploadStringAsFile (list_max_greater_base.Child (" only_file.txt" ), " content_only" );
1860
+ UploadStringAsFile (list_max_greater_base.Child (" only_prefix/another.txt" ), " content_another_in_prefix" );
1861
+
1862
+ LogDebug (" Calling List(10) on gs://%s%s" ,
1863
+ list_max_greater_base.bucket ().c_str (),
1864
+ list_max_greater_base.full_path ().c_str ());
1865
+ firebase::Future<firebase::storage::ListResult> future =
1866
+ list_max_greater_base.List (10 ); // Max results (10) > actual (1 file + 1 prefix = 2)
1867
+ WaitForCompletion (future, " ListWithMaxResultsGreaterThanActual" );
1868
+
1869
+ ASSERT_EQ (future.error (), firebase::storage::kErrorNone )
1870
+ << future.error_message ();
1871
+ ASSERT_NE (future.result (), nullptr );
1872
+ const firebase::storage::ListResult* result = future.result ();
1873
+
1874
+ VerifyListResultContains (*result, {" only_file.txt" }, {" only_prefix/" });
1875
+ EXPECT_TRUE (result->page_token ().empty ());
1876
+ }
1877
+
1878
+ TEST_F (FirebaseStorageTest, ListNonExistentPath) {
1879
+ SKIP_TEST_ON_ANDROID_EMULATOR;
1880
+ SignIn ();
1881
+ ASSERT_TRUE (list_test_root_.is_valid ()) << " List test root is not valid." ;
1882
+
1883
+ firebase::storage::StorageReference list_non_existent_ref =
1884
+ list_test_root_.Child (" this_folder_does_not_exist_for_list_test" );
1885
+ // No cleanup needed as nothing is created.
1886
+
1887
+ LogDebug (" Calling ListAll() on non-existent path: gs://%s%s" ,
1888
+ list_non_existent_ref.bucket ().c_str (),
1889
+ list_non_existent_ref.full_path ().c_str ());
1890
+ firebase::Future<firebase::storage::ListResult> future =
1891
+ list_non_existent_ref.ListAll ();
1892
+ WaitForCompletion (future, " ListNonExistentPath" );
1893
+
1894
+ // Listing a non-existent path should not be an error, it's just an empty list.
1895
+ ASSERT_EQ (future.error (), firebase::storage::kErrorNone )
1896
+ << future.error_message ();
1897
+ ASSERT_NE (future.result (), nullptr );
1898
+ const firebase::storage::ListResult* result = future.result ();
1899
+
1900
+ VerifyListResultContains (*result, {}, {});
1901
+ EXPECT_TRUE (result->page_token ().empty ());
1902
+ }
1903
+
1904
+
1625
1905
} // namespace firebase_testapp_automated
0 commit comments