Repository: googleapis/google-cloud-python

Issue:
Firestore: client.collections() raising exception on list expansion
```
Python 3.7.2 (default, Mar  7 2019, 12:37:36)
[Clang 10.0.0 (clang-1000.10.44.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from google.cloud import firestore
>>> client = firestore.Client()
>>> collections = client.collections()
>>> list(collections)
```
```
Traceback (most recent call last):
  File "/Users/crwilcox/scratch/google-cloud-firestore/venv/lib/python3.7/site-packages/google/api_core/grpc_helpers.py", line 57, in error_remapped_callable
    return callable_(*args, **kwargs)
  File "/Users/crwilcox/scratch/google-cloud-firestore/venv/lib/python3.7/site-packages/grpc/_channel.py", line 565, in __call__
    return _end_unary_response_blocking(state, call, False, None)
  File "/Users/crwilcox/scratch/google-cloud-firestore/venv/lib/python3.7/site-packages/grpc/_channel.py", line 467, in _end_unary_response_blocking
    raise _Rendezvous(state, None, None, deadline)
grpc._channel._Rendezvous: <_Rendezvous of RPC that terminated with:
	status = StatusCode.INVALID_ARGUMENT
	details = "Document parent name "projects/abstract-disk-227418/databases/(default)" lacks "/" at index 49."
	debug_error_string = "{"created":"@1563560373.944638000","description":"Error received from peer ipv6:[2607:f8b0:400a:804::200a]:443","file":"src/core/lib/surface/call.cc","file_line":1052,"grpc_message":"Document parent name "projects/abstract-disk-227418/databases/(default)" lacks "/" at index 49.","grpc_status":3}"
>

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/crwilcox/scratch/google-cloud-firestore/venv/lib/python3.7/site-packages/google/api_core/page_iterator.py", line 204, in _items_iter
    for page in self._page_iter(increment=False):
  File "/Users/crwilcox/scratch/google-cloud-firestore/venv/lib/python3.7/site-packages/google/api_core/page_iterator.py", line 235, in _page_iter
    page = self._next_page()
  File "/Users/crwilcox/scratch/google-cloud-firestore/venv/lib/python3.7/site-packages/google/api_core/page_iterator.py", line 526, in _next_page
    response = self._method(self._request)
  File "/Users/crwilcox/scratch/google-cloud-firestore/venv/lib/python3.7/site-packages/google/api_core/gapic_v1/method.py", line 143, in __call__
    return wrapped_func(*args, **kwargs)
  File "/Users/crwilcox/scratch/google-cloud-firestore/venv/lib/python3.7/site-packages/google/api_core/retry.py", line 273, in retry_wrapped_func
    on_error=on_error,
  File "/Users/crwilcox/scratch/google-cloud-firestore/venv/lib/python3.7/site-packages/google/api_core/retry.py", line 182, in retry_target
    return target()
  File "/Users/crwilcox/scratch/google-cloud-firestore/venv/lib/python3.7/site-packages/google/api_core/timeout.py", line 214, in func_with_timeout
    return func(*args, **kwargs)
  File "/Users/crwilcox/scratch/google-cloud-firestore/venv/lib/python3.7/site-packages/google/api_core/grpc_helpers.py", line 59, in error_remapped_callable
    six.raise_from(exceptions.from_grpc_error(exc), exc)
  File "<string>", line 3, in raise_from
google.api_core.exceptions.InvalidArgument: 400 Document parent name "projects/abstract-disk-227418/databases/(default)" lacks "/" at index 49.
```


Discussion:
Interesting.  When `Client.collections` was added in #6553, we were on the `v1beta1` API only:  the method actually works for the `firestore_v1beta1.Client` class!

```python
>>> from google.cloud.firestore_v1beta1 import Client
>>> client = Client()
>>> collections = list(client.collections())
>>> type(collections)
list
```

While investigating, I tried poking at the V1 client code to add a trailing `/` to the parent string passed to `ListCollectionIds`, which provoked a *different* exception:

```python
Traceback (most recent call last):
  File ".../api_core/google/api_core/grpc_helpers.py", line 57, in error_remapped_callable
    return callable_(*args, **kwargs)
  File ".../.nox/system-3-7/lib/python3.7/site-packages/grpc/_channel.py", line 565, in __call__
    return _end_unary_response_blocking(state, call, False, None)
  File ".../.nox/system-3-7/lib/python3.7/site-packages/grpc/_channel.py", line 467, in _end_unary_response_blocking
    raise _Rendezvous(state, None, None, deadline)
grpc._channel._Rendezvous: <_Rendezvous of RPC that terminated with:
	status = StatusCode.INVALID_ARGUMENT
	details = "Document parent name "projects/<PROJECT-ID>/databases/(default)/" has invalid trailing "/"."
	debug_error_string = "{"created":"@1563563139.296441652","description":"Error received from peer ipv6:[2607:f8b0:4004:805::200a]:443","file":"src/core/lib/surface/call.cc","file_line":1052,"grpc_message":"Document parent name "projects/<PROJECT-ID>/databases/(default)/" has invalid trailing "/".","grpc_status":3}"
>
```

I then recalled some *more* of the undocumented behavior from other V1 changes, which require that the client pass `/documents` as the top-level path.  I tried hacking that onto the parent passed to `ListCollectionIds`, and voilà! no exception.

Resolving patch:
diff --git a/firestore/google/cloud/firestore_v1/client.py b/firestore/google/cloud/firestore_v1/client.py
--- a/firestore/google/cloud/firestore_v1/client.py
+++ b/firestore/google/cloud/firestore_v1/client.py
@@ -421,7 +421,7 @@ def collections(self):
                 iterator of subcollections of the current document.
         """
         iterator = self._firestore_api.list_collection_ids(
-            self._database_string, metadata=self._rpc_metadata
+            "{}/documents".format(self._database_string), metadata=self._rpc_metadata
         )
         iterator.client = self
         iterator.item_to_value = _item_to_collection_ref


Test patch:
diff --git a/firestore/tests/system.py b/firestore/tests/system.py
--- a/firestore/tests/system.py
+++ b/firestore/tests/system.py
@@ -57,6 +57,11 @@ def cleanup():
         operation()
 
 
+def test_collections(client):
+    collections = list(client.collections())
+    assert isinstance(collections, list)
+
+
 def test_create_document(client, cleanup):
     now = datetime.datetime.utcnow().replace(tzinfo=UTC)
     collection_id = "doc-create" + unique_resource_id("-")
diff --git a/firestore/tests/unit/v1/test_client.py b/firestore/tests/unit/v1/test_client.py
--- a/firestore/tests/unit/v1/test_client.py
+++ b/firestore/tests/unit/v1/test_client.py
@@ -292,8 +292,9 @@ def _next_page(self):
             self.assertEqual(collection.parent, None)
             self.assertEqual(collection.id, collection_id)
 
+        base_path = client._database_string + "/documents"
         firestore_api.list_collection_ids.assert_called_once_with(
-            client._database_string, metadata=client._rpc_metadata
+            base_path, metadata=client._rpc_metadata
         )
 
     def _get_all_helper(self, client, references, document_pbs, **kwargs):