|
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,8 @@ 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; |
83 | 86 |
|
84 | 87 | class FirebaseStorageTest : public FirebaseTest {
|
85 | 88 | public:
|
@@ -96,7 +99,6 @@ class FirebaseStorageTest : public FirebaseTest {
|
96 | 99 | // Called after each test.
|
97 | 100 | void TearDown() override;
|
98 | 101 |
|
99 |
| -// File references that we need to delete on test exit. |
100 | 102 | protected:
|
101 | 103 | // Initialize Firebase App and Firebase Auth.
|
102 | 104 | static void InitializeAppAndAuth();
|
@@ -118,6 +120,17 @@ class FirebaseStorageTest : public FirebaseTest {
|
118 | 120 | // Create a unique working folder and return a reference to it.
|
119 | 121 | firebase::storage::StorageReference CreateFolder();
|
120 | 122 |
|
| 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 | + |
121 | 134 | static firebase::App* shared_app_;
|
122 | 135 | static firebase::auth::Auth* shared_auth_;
|
123 | 136 |
|
@@ -212,6 +225,7 @@ void FirebaseStorageTest::TerminateAppAndAuth() {
|
212 | 225 | void FirebaseStorageTest::SetUp() {
|
213 | 226 | FirebaseTest::SetUp();
|
214 | 227 | InitializeStorage();
|
| 228 | +// list_test_root_ removed from SetUp |
215 | 229 | }
|
216 | 230 |
|
217 | 231 | void FirebaseStorageTest::TearDown() {
|
@@ -313,6 +327,65 @@ void FirebaseStorageTest::SignOut() {
|
313 | 327 | EXPECT_FALSE(shared_auth_->current_user().is_valid());
|
314 | 328 | }
|
315 | 329 |
|
| 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 | + |
316 | 389 | firebase::storage::StorageReference FirebaseStorageTest::CreateFolder() {
|
317 | 390 | // Generate a folder for the test data based on the time in milliseconds.
|
318 | 391 | int64_t time_in_microseconds = GetCurrentTimeInMicroseconds();
|
@@ -1622,4 +1695,213 @@ TEST_F(FirebaseStorageTest, TestInvalidatingReferencesWhenDeletingApp) {
|
1622 | 1695 | InitializeAppAndAuth();
|
1623 | 1696 | }
|
1624 | 1697 |
|
| 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 | + |
1625 | 1907 | } // namespace firebase_testapp_automated
|
0 commit comments