├── .github └── workflows │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── mockfirestore ├── __init__.py ├── _helpers.py ├── _transformations.py ├── client.py ├── collection.py ├── document.py ├── exceptions.py ├── query.py └── transaction.py ├── requirements-dev-minimal.txt ├── setup.py └── tests ├── __init__.py ├── test_collection_reference.py ├── test_document_reference.py ├── test_document_snapshot.py ├── test_mock_client.py ├── test_timestamp.py └── test_transaction.py /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: '3.x' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install setuptools wheel twine 20 | - name: Build and publish 21 | env: 22 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 23 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 24 | run: | 25 | python setup.py sdist bdist_wheel 26 | twine upload dist/* -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | python-version: [ '3.6', '3.7', '3.8', '3.9', '3.10' ] 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install -r requirements-dev-minimal.txt 21 | - name: Run tests 22 | run: | 23 | python -m unittest discover tests -t / 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv*/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | .idea/ 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 mdowds 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Mock Firestore 2 | 3 | An in-memory implementation of the [Python client library](https://github.com/googleapis/python-firestore) for Google Cloud Firestore, intended for use in tests to replace the real thing. This project is in early stages and is only a partial implementation of the real client library. 4 | 5 | To install: 6 | 7 | `pip install mock-firestore` 8 | 9 | Python 3.6+ is required for it to work. 10 | 11 | ## Usage 12 | 13 | ```python 14 | db = firestore.Client() 15 | mock_db = MockFirestore() 16 | 17 | # Can be used in the same way as a firestore.Client() object would be, e.g.: 18 | db.collection('users').get() 19 | mock_db.collection('users').get() 20 | ``` 21 | 22 | To reset the store to an empty state, use the `reset()` method: 23 | ```python 24 | mock_db = MockFirestore() 25 | mock_db.reset() 26 | ``` 27 | 28 | ## Supported operations 29 | 30 | ```python 31 | mock_db = MockFirestore() 32 | 33 | # Collections 34 | mock_db.collections() 35 | mock_db.collection('users') 36 | mock_db.collection('users').get() 37 | mock_db.collection('users').list_documents() 38 | mock_db.collection('users').stream() 39 | 40 | # Documents 41 | mock_db.collection('users').document() 42 | mock_db.collection('users').document('alovelace') 43 | mock_db.collection('users').document('alovelace').id 44 | mock_db.collection('users').document('alovelace').parent 45 | mock_db.collection('users').document('alovelace').update_time 46 | mock_db.collection('users').document('alovelace').read_time 47 | mock_db.collection('users').document('alovelace').get() 48 | mock_db.collection('users').document('alovelace').get().exists 49 | mock_db.collection('users').document('alovelace').get().to_dict() 50 | mock_db.collection('users').document('alovelace').set({ 51 | 'first': 'Ada', 52 | 'last': 'Lovelace' 53 | }) 54 | mock_db.collection('users').document('alovelace').set({'first': 'Augusta Ada'}, merge=True) 55 | mock_db.collection('users').document('alovelace').update({'born': 1815}) 56 | mock_db.collection('users').document('alovelace').update({'favourite.color': 'red'}) 57 | mock_db.collection('users').document('alovelace').update({'associates': ['Charles Babbage', 'Michael Faraday']}) 58 | mock_db.collection('users').document('alovelace').collection('friends') 59 | mock_db.collection('users').document('alovelace').delete() 60 | mock_db.collection('users').document(document_id: 'alovelace').delete() 61 | mock_db.collection('users').add({'first': 'Ada', 'last': 'Lovelace'}, 'alovelace') 62 | mock_db.get_all([mock_db.collection('users').document('alovelace')]) 63 | mock_db.document('users/alovelace') 64 | mock_db.document('users/alovelace').update({'born': 1815}) 65 | mock_db.collection('users/alovelace/friends') 66 | 67 | # Querying 68 | mock_db.collection('users').order_by('born').get() 69 | mock_db.collection('users').order_by('born', direction='DESCENDING').get() 70 | mock_db.collection('users').limit(5).get() 71 | mock_db.collection('users').where('born', '==', 1815).get() 72 | mock_db.collection('users').where('born', '!=', 1815).get() 73 | mock_db.collection('users').where('born', '<', 1815).get() 74 | mock_db.collection('users').where('born', '>', 1815).get() 75 | mock_db.collection('users').where('born', '<=', 1815).get() 76 | mock_db.collection('users').where('born', '>=', 1815).get() 77 | mock_db.collection('users').where('born', 'in', [1815, 1900]).stream() 78 | mock_db.collection('users').where('born', 'in', [1815, 1900]).stream() 79 | mock_db.collection('users').where('associates', 'array_contains', 'Charles Babbage').stream() 80 | mock_db.collection('users').where('associates', 'array_contains_any', ['Charles Babbage', 'Michael Faraday']).stream() 81 | 82 | # Transforms 83 | mock_db.collection('users').document('alovelace').update({'likes': firestore.Increment(1)}) 84 | mock_db.collection('users').document('alovelace').update({'associates': firestore.ArrayUnion(['Andrew Cross', 'Charles Wheatstone'])}) 85 | mock_db.collection('users').document('alovelace').update({firestore.DELETE_FIELD: "born"}) 86 | mock_db.collection('users').document('alovelace').update({'associates': firestore.ArrayRemove(['Andrew Cross'])}) 87 | 88 | # Cursors 89 | mock_db.collection('users').start_after({'id': 'alovelace'}).stream() 90 | mock_db.collection('users').end_before({'id': 'alovelace'}).stream() 91 | mock_db.collection('users').end_at({'id': 'alovelace'}).stream() 92 | mock_db.collection('users').start_after(mock_db.collection('users').document('alovelace')).stream() 93 | 94 | # Transactions 95 | transaction = mock_db.transaction() 96 | transaction.id 97 | transaction.in_progress 98 | transaction.get(mock_db.collection('users').where('born', '==', 1815)) 99 | transaction.get(mock_db.collection('users').document('alovelace')) 100 | transaction.get_all([mock_db.collection('users').document('alovelace')]) 101 | transaction.set(mock_db.collection('users').document('alovelace'), {'born': 1815}) 102 | transaction.update(mock_db.collection('users').document('alovelace'), {'born': 1815}) 103 | transaction.delete(mock_db.collection('users').document('alovelace')) 104 | transaction.commit() 105 | ``` 106 | 107 | ## Running the tests 108 | * Create and activate a virtualenv with a Python version of at least 3.6 109 | * Install dependencies with `pip install -r requirements-dev-minimal.txt` 110 | * Run tests with `python -m unittest discover tests -t /` 111 | 112 | ## Contributors 113 | 114 | * [Matt Dowds](https://github.com/mdowds) 115 | * [Chris Tippett](https://github.com/christippett) 116 | * [Anton Melnikov](https://github.com/notnami) 117 | * [Ben Riggleman](https://github.com/briggleman) 118 | * [Steve Atwell](https://github.com/satwell) 119 | * [ahti123](https://github.com/ahti123) 120 | * [Billcountry Mwaniki](https://github.com/Billcountry) 121 | * [Lucas Moura](https://github.com/lsantosdemoura) 122 | * [Kamil Romaszko](https://github.com/kromash) 123 | * [Anna Melnikov](https://github.com/notnami) 124 | * [Carl Chipperfield](https://github.com/carl-chipperfield) 125 | * [Aaron Loo](https://github.com/domanchi) 126 | * [Kristof Krenn](https://github.com/KrennKristof) 127 | * [Ben Phillips](https://github.com/tavva) 128 | * [Rene Delgado](https://github.com/RDelg) 129 | * [klanderson](https://github.com/klanderson) 130 | * [William Li](https://github.com/wli) 131 | * [Ugo Marchand](https://github.com/UgoM) 132 | * [Bryce Thornton](https://github.com/brycethornton) 133 | -------------------------------------------------------------------------------- /mockfirestore/__init__.py: -------------------------------------------------------------------------------- 1 | # by analogy with 2 | # https://github.com/mongomock/mongomock/blob/develop/mongomock/__init__.py 3 | # try to import gcloud exceptions 4 | # and if gcloud is not installed, define our own 5 | try: 6 | from google.api_core.exceptions import ClientError, Conflict, NotFound, AlreadyExists 7 | except ImportError: 8 | from mockfirestore.exceptions import ClientError, Conflict, NotFound, AlreadyExists 9 | 10 | from mockfirestore.client import MockFirestore 11 | from mockfirestore.document import DocumentSnapshot, DocumentReference 12 | from mockfirestore.collection import CollectionReference 13 | from mockfirestore.query import Query 14 | from mockfirestore._helpers import Timestamp 15 | from mockfirestore.transaction import Transaction 16 | -------------------------------------------------------------------------------- /mockfirestore/_helpers.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import random 3 | import string 4 | from datetime import datetime as dt 5 | from functools import reduce 6 | from typing import (Dict, Any, Tuple, TypeVar, Sequence, Iterator) 7 | 8 | T = TypeVar('T') 9 | KeyValuePair = Tuple[str, Dict[str, Any]] 10 | Document = Dict[str, Any] 11 | Collection = Dict[str, Document] 12 | Store = Dict[str, Collection] 13 | 14 | 15 | def get_by_path(data: Dict[str, T], path: Sequence[str], create_nested: bool = False) -> T: 16 | """Access a nested object in root by item sequence.""" 17 | 18 | def get_or_create(a, b): 19 | if b not in a: 20 | a[b] = {} 21 | return a[b] 22 | 23 | if create_nested: 24 | return reduce(get_or_create, path, data) 25 | else: 26 | return reduce(operator.getitem, path, data) 27 | 28 | 29 | def set_by_path(data: Dict[str, T], path: Sequence[str], value: T, create_nested: bool = True): 30 | """Set a value in a nested object in root by item sequence.""" 31 | get_by_path(data, path[:-1], create_nested=True)[path[-1]] = value 32 | 33 | 34 | def delete_by_path(data: Dict[str, T], path: Sequence[str]): 35 | """Delete a value in a nested object in root by item sequence.""" 36 | del get_by_path(data, path[:-1])[path[-1]] 37 | 38 | 39 | def generate_random_string(): 40 | return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(20)) 41 | 42 | 43 | class Timestamp: 44 | """ 45 | Imitates some properties of `google.protobuf.timestamp_pb2.Timestamp` 46 | """ 47 | 48 | def __init__(self, timestamp: float): 49 | self._timestamp = timestamp 50 | 51 | @classmethod 52 | def from_now(cls): 53 | timestamp = dt.now().timestamp() 54 | return cls(timestamp) 55 | 56 | @property 57 | def seconds(self): 58 | return str(self._timestamp).split('.')[0] 59 | 60 | @property 61 | def nanos(self): 62 | return str(self._timestamp).split('.')[1] 63 | 64 | 65 | def get_document_iterator(document: Dict[str, Any], prefix: str = '') -> Iterator[Tuple[str, Any]]: 66 | """ 67 | :returns: (dot-delimited path, value,) 68 | """ 69 | for key, value in document.items(): 70 | if isinstance(value, dict): 71 | for item in get_document_iterator(value, prefix=key): 72 | yield item 73 | 74 | if not prefix: 75 | yield key, value 76 | else: 77 | yield '{}.{}'.format(prefix, key), value 78 | -------------------------------------------------------------------------------- /mockfirestore/_transformations.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, List 2 | 3 | from mockfirestore._helpers import get_document_iterator, get_by_path, set_by_path, delete_by_path 4 | 5 | 6 | def apply_transformations(document: Dict[str, Any], data: Dict[str, Any]): 7 | """Handles special fields like INCREMENT.""" 8 | increments = {} 9 | arr_unions = {} 10 | arr_deletes = {} 11 | deletes = [] 12 | 13 | for key, value in list(get_document_iterator(data)): 14 | if not value.__class__.__module__.startswith('google.cloud.firestore'): 15 | # Unfortunately, we can't use `isinstance` here because that would require 16 | # us to declare google-cloud-firestore as a dependency for this library. 17 | # However, it's somewhat strange that the mocked version of the library 18 | # requires the library itself, so we'll just leverage this heuristic as a 19 | # means of identifying it. 20 | # 21 | # Furthermore, we don't hardcode the full module name, since the original 22 | # library seems to use a thin shim to perform versioning. e.g. at the time 23 | # of writing, the full module name is `google.cloud.firestore_v1.transforms`, 24 | # and it can evolve to `firestore_v2` in the future. 25 | continue 26 | 27 | transformer = value.__class__.__name__ 28 | if transformer == 'Increment': 29 | increments[key] = value.value 30 | elif transformer == 'ArrayUnion': 31 | arr_unions[key] = value.values 32 | elif transformer == 'ArrayRemove': 33 | arr_deletes[key] = value.values 34 | del data[key] 35 | elif transformer == 'Sentinel': 36 | if value.description == "Value used to delete a field in a document.": 37 | deletes.append(key) 38 | del data[key] 39 | 40 | # All other transformations can be applied as needed. 41 | # See #29 for tracking. 42 | 43 | def _update_data(new_values: dict, default: Any): 44 | for key, value in new_values.items(): 45 | path = key.split('.') 46 | 47 | try: 48 | item = get_by_path(document, path) 49 | except (TypeError, KeyError): 50 | item = default 51 | 52 | set_by_path(data, path, item + value, create_nested=True) 53 | 54 | _update_data(increments, 0) 55 | _update_data(arr_unions, []) 56 | 57 | _apply_updates(document, data) 58 | _apply_deletes(document, deletes) 59 | _apply_arr_deletes(document, arr_deletes) 60 | 61 | 62 | def _apply_updates(document: Dict[str, Any], data: Dict[str, Any]): 63 | for key, value in data.items(): 64 | path = key.split(".") 65 | set_by_path(document, path, value, create_nested=True) 66 | 67 | 68 | def _apply_deletes(document: Dict[str, Any], data: List[str]): 69 | for key in data: 70 | path = key.split(".") 71 | delete_by_path(document, path) 72 | 73 | 74 | def _apply_arr_deletes(document: Dict[str, Any], data: Dict[str, Any]): 75 | for key, values_to_delete in data.items(): 76 | path = key.split(".") 77 | try: 78 | value = get_by_path(document, path) 79 | except KeyError: 80 | continue 81 | for value_to_delete in values_to_delete: 82 | try: 83 | value.remove(value_to_delete) 84 | except ValueError: 85 | pass 86 | set_by_path(document, path, value) -------------------------------------------------------------------------------- /mockfirestore/client.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Sequence 2 | from mockfirestore.collection import CollectionReference 3 | from mockfirestore.document import DocumentReference, DocumentSnapshot 4 | from mockfirestore.transaction import Transaction 5 | 6 | 7 | class MockFirestore: 8 | 9 | def __init__(self) -> None: 10 | self._data = {} 11 | 12 | def _ensure_path(self, path): 13 | current_position = self 14 | 15 | for el in path[:-1]: 16 | if type(current_position) in (MockFirestore, DocumentReference): 17 | current_position = current_position.collection(el) 18 | else: 19 | current_position = current_position.document(el) 20 | 21 | return current_position 22 | 23 | def document(self, path: str) -> DocumentReference: 24 | path = path.split("/") 25 | 26 | if len(path) % 2 != 0: 27 | raise Exception("Cannot create document at path {}".format(path)) 28 | current_position = self._ensure_path(path) 29 | 30 | return current_position.document(path[-1]) 31 | 32 | def collection(self, path: str) -> CollectionReference: 33 | path = path.split("/") 34 | 35 | if len(path) % 2 != 1: 36 | raise Exception("Cannot create collection at path {}".format(path)) 37 | 38 | name = path[-1] 39 | if len(path) > 1: 40 | current_position = self._ensure_path(path) 41 | return current_position.collection(name) 42 | else: 43 | if name not in self._data: 44 | self._data[name] = {} 45 | return CollectionReference(self._data, [name]) 46 | 47 | def collections(self) -> Sequence[CollectionReference]: 48 | return [CollectionReference(self._data, [collection_name]) for collection_name in self._data] 49 | 50 | def reset(self): 51 | self._data = {} 52 | 53 | def get_all(self, references: Iterable[DocumentReference], 54 | field_paths=None, 55 | transaction=None) -> Iterable[DocumentSnapshot]: 56 | for doc_ref in set(references): 57 | yield doc_ref.get() 58 | 59 | def transaction(self, **kwargs) -> Transaction: 60 | return Transaction(self, **kwargs) 61 | 62 | 63 | -------------------------------------------------------------------------------- /mockfirestore/collection.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from typing import Any, List, Optional, Iterable, Dict, Tuple, Sequence, Union 3 | 4 | from mockfirestore import AlreadyExists 5 | from mockfirestore._helpers import generate_random_string, Store, get_by_path, set_by_path, Timestamp 6 | from mockfirestore.query import Query 7 | from mockfirestore.document import DocumentReference, DocumentSnapshot 8 | 9 | 10 | class CollectionReference: 11 | def __init__(self, data: Store, path: List[str], 12 | parent: Optional[DocumentReference] = None) -> None: 13 | self._data = data 14 | self._path = path 15 | self.parent = parent 16 | 17 | def document(self, document_id: Optional[str] = None) -> DocumentReference: 18 | collection = get_by_path(self._data, self._path) 19 | if document_id is None: 20 | document_id = generate_random_string() 21 | new_path = self._path + [document_id] 22 | if document_id not in collection: 23 | set_by_path(self._data, new_path, {}) 24 | return DocumentReference(self._data, new_path, parent=self) 25 | 26 | def get(self) -> Iterable[DocumentSnapshot]: 27 | warnings.warn('Collection.get is deprecated, please use Collection.stream', 28 | category=DeprecationWarning) 29 | return self.stream() 30 | 31 | def add(self, document_data: Dict, document_id: str = None) \ 32 | -> Tuple[Timestamp, DocumentReference]: 33 | if document_id is None: 34 | document_id = document_data.get('id', generate_random_string()) 35 | collection = get_by_path(self._data, self._path) 36 | new_path = self._path + [document_id] 37 | if document_id in collection: 38 | raise AlreadyExists('Document already exists: {}'.format(new_path)) 39 | doc_ref = DocumentReference(self._data, new_path, parent=self) 40 | doc_ref.set(document_data) 41 | timestamp = Timestamp.from_now() 42 | return timestamp, doc_ref 43 | 44 | def where(self, field: str, op: str, value: Any) -> Query: 45 | query = Query(self, field_filters=[(field, op, value)]) 46 | return query 47 | 48 | def order_by(self, key: str, direction: Optional[str] = None) -> Query: 49 | query = Query(self, orders=[(key, direction)]) 50 | return query 51 | 52 | def limit(self, limit_amount: int) -> Query: 53 | query = Query(self, limit=limit_amount) 54 | return query 55 | 56 | def offset(self, offset: int) -> Query: 57 | query = Query(self, offset=offset) 58 | return query 59 | 60 | def start_at(self, document_fields_or_snapshot: Union[dict, DocumentSnapshot]) -> Query: 61 | query = Query(self, start_at=(document_fields_or_snapshot, True)) 62 | return query 63 | 64 | def start_after(self, document_fields_or_snapshot: Union[dict, DocumentSnapshot]) -> Query: 65 | query = Query(self, start_at=(document_fields_or_snapshot, False)) 66 | return query 67 | 68 | def end_at(self, document_fields_or_snapshot: Union[dict, DocumentSnapshot]) -> Query: 69 | query = Query(self, end_at=(document_fields_or_snapshot, True)) 70 | return query 71 | 72 | def end_before(self, document_fields_or_snapshot: Union[dict, DocumentSnapshot]) -> Query: 73 | query = Query(self, end_at=(document_fields_or_snapshot, False)) 74 | return query 75 | 76 | def list_documents(self, page_size: Optional[int] = None) -> Sequence[DocumentReference]: 77 | docs = [] 78 | for key in get_by_path(self._data, self._path): 79 | docs.append(self.document(key)) 80 | return docs 81 | 82 | def stream(self, transaction=None) -> Iterable[DocumentSnapshot]: 83 | for key in sorted(get_by_path(self._data, self._path)): 84 | doc_snapshot = self.document(key).get() 85 | yield doc_snapshot 86 | -------------------------------------------------------------------------------- /mockfirestore/document.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from functools import reduce 3 | import operator 4 | from typing import List, Dict, Any 5 | from mockfirestore import NotFound 6 | from mockfirestore._helpers import ( 7 | Timestamp, Document, Store, get_by_path, set_by_path, delete_by_path 8 | ) 9 | from mockfirestore._transformations import apply_transformations 10 | 11 | 12 | class DocumentSnapshot: 13 | def __init__(self, reference: 'DocumentReference', data: Document) -> None: 14 | self.reference = reference 15 | self._doc = deepcopy(data) 16 | 17 | @property 18 | def id(self): 19 | return self.reference.id 20 | 21 | @property 22 | def exists(self) -> bool: 23 | return self._doc != {} 24 | 25 | def to_dict(self) -> Document: 26 | return self._doc 27 | 28 | @property 29 | def create_time(self) -> Timestamp: 30 | timestamp = Timestamp.from_now() 31 | return timestamp 32 | 33 | @property 34 | def update_time(self) -> Timestamp: 35 | return self.create_time 36 | 37 | @property 38 | def read_time(self) -> Timestamp: 39 | timestamp = Timestamp.from_now() 40 | return timestamp 41 | 42 | def get(self, field_path: str) -> Any: 43 | if not self.exists: 44 | return None 45 | else: 46 | return reduce(operator.getitem, field_path.split('.'), self._doc) 47 | 48 | def _get_by_field_path(self, field_path: str) -> Any: 49 | try: 50 | return self.get(field_path) 51 | except KeyError: 52 | return None 53 | 54 | 55 | class DocumentReference: 56 | def __init__(self, data: Store, path: List[str], 57 | parent: 'CollectionReference') -> None: 58 | self._data = data 59 | self._path = path 60 | self.parent = parent 61 | 62 | @property 63 | def id(self): 64 | return self._path[-1] 65 | 66 | def get(self) -> DocumentSnapshot: 67 | return DocumentSnapshot(self, get_by_path(self._data, self._path)) 68 | 69 | def delete(self): 70 | delete_by_path(self._data, self._path) 71 | 72 | def set(self, data: Dict, merge=False): 73 | if merge: 74 | try: 75 | self.update(deepcopy(data)) 76 | except NotFound: 77 | self.set(data) 78 | else: 79 | set_by_path(self._data, self._path, deepcopy(data)) 80 | 81 | def update(self, data: Dict[str, Any]): 82 | document = get_by_path(self._data, self._path) 83 | if document == {}: 84 | raise NotFound('No document to update: {}'.format(self._path)) 85 | 86 | apply_transformations(document, deepcopy(data)) 87 | 88 | def collection(self, name) -> 'CollectionReference': 89 | from mockfirestore.collection import CollectionReference 90 | document = get_by_path(self._data, self._path) 91 | new_path = self._path + [name] 92 | if name not in document: 93 | set_by_path(self._data, new_path, {}) 94 | return CollectionReference(self._data, new_path, parent=self) 95 | -------------------------------------------------------------------------------- /mockfirestore/exceptions.py: -------------------------------------------------------------------------------- 1 | class ClientError(Exception): 2 | code = None 3 | 4 | def __init__(self, message, *args): 5 | self.message = message 6 | super().__init__(message, *args) 7 | 8 | def __str__(self): 9 | return "{} {}".format(self.code, self.message) 10 | 11 | 12 | class Conflict(ClientError): 13 | code = 409 14 | 15 | 16 | class NotFound(ClientError): 17 | code = 404 18 | 19 | 20 | class AlreadyExists(Conflict): 21 | pass 22 | -------------------------------------------------------------------------------- /mockfirestore/query.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from itertools import islice, tee 3 | from typing import Iterator, Any, Optional, List, Callable, Union 4 | 5 | from mockfirestore.document import DocumentSnapshot 6 | from mockfirestore._helpers import T 7 | 8 | 9 | class Query: 10 | def __init__(self, parent: 'CollectionReference', projection=None, 11 | field_filters=(), orders=(), limit=None, offset=None, 12 | start_at=None, end_at=None, all_descendants=False) -> None: 13 | self.parent = parent 14 | self.projection = projection 15 | self._field_filters = [] 16 | self.orders = list(orders) 17 | self._limit = limit 18 | self._offset = offset 19 | self._start_at = start_at 20 | self._end_at = end_at 21 | self.all_descendants = all_descendants 22 | 23 | if field_filters: 24 | for field_filter in field_filters: 25 | self._add_field_filter(*field_filter) 26 | 27 | def stream(self, transaction=None) -> Iterator[DocumentSnapshot]: 28 | doc_snapshots = self.parent.stream() 29 | 30 | for field, compare, value in self._field_filters: 31 | doc_snapshots = [doc_snapshot for doc_snapshot in doc_snapshots 32 | if compare(doc_snapshot._get_by_field_path(field), value)] 33 | 34 | if self.orders: 35 | for key, direction in self.orders: 36 | doc_snapshots = sorted(doc_snapshots, 37 | key=lambda doc: doc.to_dict()[key], 38 | reverse=direction == 'DESCENDING') 39 | if self._start_at: 40 | document_fields_or_snapshot, before = self._start_at 41 | doc_snapshots = self._apply_cursor(document_fields_or_snapshot, doc_snapshots, before, True) 42 | 43 | if self._end_at: 44 | document_fields_or_snapshot, before = self._end_at 45 | doc_snapshots = self._apply_cursor(document_fields_or_snapshot, doc_snapshots, before, False) 46 | 47 | if self._offset: 48 | doc_snapshots = islice(doc_snapshots, self._offset, None) 49 | 50 | if self._limit: 51 | doc_snapshots = islice(doc_snapshots, self._limit) 52 | 53 | return iter(doc_snapshots) 54 | 55 | def get(self) -> Iterator[DocumentSnapshot]: 56 | warnings.warn('Query.get is deprecated, please use Query.stream', 57 | category=DeprecationWarning) 58 | return self.stream() 59 | 60 | def _add_field_filter(self, field: str, op: str, value: Any): 61 | compare = self._compare_func(op) 62 | self._field_filters.append((field, compare, value)) 63 | 64 | def where(self, field: str, op: str, value: Any) -> 'Query': 65 | self._add_field_filter(field, op, value) 66 | return self 67 | 68 | def order_by(self, key: str, direction: Optional[str] = 'ASCENDING') -> 'Query': 69 | self.orders.append((key, direction)) 70 | return self 71 | 72 | def limit(self, limit_amount: int) -> 'Query': 73 | self._limit = limit_amount 74 | return self 75 | 76 | def offset(self, offset_amount: int) -> 'Query': 77 | self._offset = offset_amount 78 | return self 79 | 80 | def start_at(self, document_fields_or_snapshot: Union[dict, DocumentSnapshot]) -> 'Query': 81 | self._start_at = (document_fields_or_snapshot, True) 82 | return self 83 | 84 | def start_after(self, document_fields_or_snapshot: Union[dict, DocumentSnapshot]) -> 'Query': 85 | self._start_at = (document_fields_or_snapshot, False) 86 | return self 87 | 88 | def end_at(self, document_fields_or_snapshot: Union[dict, DocumentSnapshot]) -> 'Query': 89 | self._end_at = (document_fields_or_snapshot, True) 90 | return self 91 | 92 | def end_before(self, document_fields_or_snapshot: Union[dict, DocumentSnapshot]) -> 'Query': 93 | self._end_at = (document_fields_or_snapshot, False) 94 | return self 95 | 96 | def _apply_cursor(self, document_fields_or_snapshot: Union[dict, DocumentSnapshot], doc_snapshot: Iterator[DocumentSnapshot], 97 | before: bool, start: bool) -> Iterator[DocumentSnapshot]: 98 | docs, doc_snapshot = tee(doc_snapshot) 99 | for idx, doc in enumerate(doc_snapshot): 100 | index = None 101 | if isinstance(document_fields_or_snapshot, dict): 102 | for k, v in document_fields_or_snapshot.items(): 103 | if doc.to_dict().get(k, None) == v: 104 | index = idx 105 | else: 106 | index = None 107 | break 108 | elif isinstance(document_fields_or_snapshot, DocumentSnapshot): 109 | if doc.id == document_fields_or_snapshot.id: 110 | index = idx 111 | if index is not None: 112 | if before and start: 113 | return islice(docs, index, None, None) 114 | elif not before and start: 115 | return islice(docs, index + 1, None, None) 116 | elif before and not start: 117 | return islice(docs, 0, index + 1, None) 118 | elif not before and not start: 119 | return islice(docs, 0, index, None) 120 | 121 | def _compare_func(self, op: str) -> Callable[[T, T], bool]: 122 | if op == '==': 123 | return lambda x, y: x == y 124 | elif op == '!=': 125 | return lambda x, y: x != y 126 | elif op == '<': 127 | return lambda x, y: x < y 128 | elif op == '<=': 129 | return lambda x, y: x <= y 130 | elif op == '>': 131 | return lambda x, y: x > y 132 | elif op == '>=': 133 | return lambda x, y: x >= y 134 | elif op == 'in': 135 | return lambda x, y: x in y 136 | elif op == 'array_contains': 137 | return lambda x, y: y in x 138 | elif op == 'array_contains_any': 139 | return lambda x, y: any([val in y for val in x]) 140 | -------------------------------------------------------------------------------- /mockfirestore/transaction.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | import random 3 | from typing import Iterable, Callable 4 | from mockfirestore._helpers import generate_random_string, Timestamp 5 | from mockfirestore.document import DocumentReference, DocumentSnapshot 6 | from mockfirestore.query import Query 7 | 8 | MAX_ATTEMPTS = 5 9 | _MISSING_ID_TEMPLATE = "The transaction has no transaction ID, so it cannot be {}." 10 | _CANT_BEGIN = "The transaction has already begun. Current transaction ID: {!r}." 11 | _CANT_ROLLBACK = _MISSING_ID_TEMPLATE.format("rolled back") 12 | _CANT_COMMIT = _MISSING_ID_TEMPLATE.format("committed") 13 | 14 | 15 | class WriteResult: 16 | def __init__(self): 17 | self.update_time = Timestamp.from_now() 18 | 19 | 20 | class Transaction: 21 | """ 22 | This mostly follows the model from 23 | https://googleapis.dev/python/firestore/latest/transaction.html 24 | """ 25 | def __init__(self, client, 26 | max_attempts=MAX_ATTEMPTS, read_only=False): 27 | self._client = client 28 | self._max_attempts = max_attempts 29 | self._read_only = read_only 30 | self._id = None 31 | self._write_ops = [] 32 | self.write_results = None 33 | 34 | @property 35 | def in_progress(self): 36 | return self._id is not None 37 | 38 | @property 39 | def id(self): 40 | return self._id 41 | 42 | def _begin(self, retry_id=None): 43 | # generate a random ID to set the transaction as in_progress 44 | self._id = generate_random_string() 45 | 46 | def _clean_up(self): 47 | self._write_ops.clear() 48 | self._id = None 49 | 50 | def _rollback(self): 51 | if not self.in_progress: 52 | raise ValueError(_CANT_ROLLBACK) 53 | 54 | self._clean_up() 55 | 56 | def _commit(self) -> Iterable[WriteResult]: 57 | if not self.in_progress: 58 | raise ValueError(_CANT_COMMIT) 59 | 60 | results = [] 61 | for write_op in self._write_ops: 62 | write_op() 63 | results.append(WriteResult()) 64 | self.write_results = results 65 | self._clean_up() 66 | return results 67 | 68 | def get_all(self, 69 | references: Iterable[DocumentReference]) -> Iterable[DocumentSnapshot]: 70 | return self._client.get_all(references) 71 | 72 | def get(self, ref_or_query) -> Iterable[DocumentSnapshot]: 73 | if isinstance(ref_or_query, DocumentReference): 74 | return self._client.get_all([ref_or_query]) 75 | elif isinstance(ref_or_query, Query): 76 | return ref_or_query.stream() 77 | else: 78 | raise ValueError( 79 | 'Value for argument "ref_or_query" must be a DocumentReference or a Query.' 80 | ) 81 | 82 | # methods from 83 | # https://googleapis.dev/python/firestore/latest/batch.html#google.cloud.firestore_v1.batch.WriteBatch 84 | 85 | def _add_write_op(self, write_op: Callable): 86 | if self._read_only: 87 | raise ValueError( 88 | "Cannot perform write operation in read-only transaction." 89 | ) 90 | self._write_ops.append(write_op) 91 | 92 | def create(self, reference: DocumentReference, document_data): 93 | # this is a no-op, because if we have a DocumentReference 94 | # it's already in the MockFirestore 95 | ... 96 | 97 | def set(self, reference: DocumentReference, document_data: dict, 98 | merge=False): 99 | write_op = partial(reference.set, document_data, merge=merge) 100 | self._add_write_op(write_op) 101 | 102 | def update(self, reference: DocumentReference, 103 | field_updates: dict, option=None): 104 | write_op = partial(reference.update, field_updates) 105 | self._add_write_op(write_op) 106 | 107 | def delete(self, reference: DocumentReference, option=None): 108 | write_op = reference.delete 109 | self._add_write_op(write_op) 110 | 111 | def commit(self): 112 | return self._commit() 113 | 114 | def __enter__(self): 115 | return self 116 | 117 | def __exit__(self, exc_type, exc_val, exc_tb): 118 | if exc_type is None: 119 | self.commit() 120 | -------------------------------------------------------------------------------- /requirements-dev-minimal.txt: -------------------------------------------------------------------------------- 1 | google-cloud-firestore -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="mock-firestore", 8 | version="0.11.0", 9 | author="Matt Dowds", 10 | description="In-memory implementation of Google Cloud Firestore for use in tests", 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | url="https://github.com/mdowds/mock-firestore", 14 | packages=setuptools.find_packages(), 15 | test_suite='', 16 | classifiers=[ 17 | 'Programming Language :: Python :: 3.6', 18 | 'Programming Language :: Python :: 3.7', 19 | 'Programming Language :: Python :: 3.8', 20 | 'Programming Language :: Python :: 3.9', 21 | 'Programming Language :: Python :: 3.10', 22 | "License :: OSI Approved :: MIT License", 23 | ], 24 | ) -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdowds/python-mock-firestore/0de34b1c319c08ccbcc6887d110f3bf8f5ff2116/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_collection_reference.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from mockfirestore import MockFirestore, DocumentReference, DocumentSnapshot, AlreadyExists 4 | 5 | 6 | class TestCollectionReference(TestCase): 7 | def test_collection_get_returnsDocuments(self): 8 | fs = MockFirestore() 9 | fs._data = {'foo': { 10 | 'first': {'id': 1}, 11 | 'second': {'id': 2} 12 | }} 13 | docs = list(fs.collection('foo').stream()) 14 | 15 | self.assertEqual({'id': 1}, docs[0].to_dict()) 16 | self.assertEqual({'id': 2}, docs[1].to_dict()) 17 | 18 | def test_collection_get_collectionDoesNotExist(self): 19 | fs = MockFirestore() 20 | docs = fs.collection('foo').stream() 21 | self.assertEqual([], list(docs)) 22 | 23 | def test_collection_get_nestedCollection(self): 24 | fs = MockFirestore() 25 | fs._data = {'foo': { 26 | 'first': { 27 | 'id': 1, 28 | 'bar': { 29 | 'first_nested': {'id': 1.1} 30 | } 31 | } 32 | }} 33 | docs = list(fs.collection('foo').document('first').collection('bar').stream()) 34 | self.assertEqual({'id': 1.1}, docs[0].to_dict()) 35 | 36 | def test_collection_get_nestedCollection_by_path(self): 37 | fs = MockFirestore() 38 | fs._data = {'foo': { 39 | 'first': { 40 | 'id': 1, 41 | 'bar': { 42 | 'first_nested': {'id': 1.1} 43 | } 44 | } 45 | }} 46 | docs = list(fs.collection('foo/first/bar').stream()) 47 | self.assertEqual({'id': 1.1}, docs[0].to_dict()) 48 | 49 | def test_collection_get_nestedCollection_collectionDoesNotExist(self): 50 | fs = MockFirestore() 51 | fs._data = {'foo': { 52 | 'first': {'id': 1} 53 | }} 54 | docs = list(fs.collection('foo').document('first').collection('bar').stream()) 55 | self.assertEqual([], docs) 56 | 57 | def test_collection_get_nestedCollection_by_path_collectionDoesNotExist(self): 58 | fs = MockFirestore() 59 | fs._data = {'foo': { 60 | 'first': {'id': 1} 61 | }} 62 | docs = list(fs.collection('foo/first/bar').stream()) 63 | self.assertEqual([], docs) 64 | 65 | def test_collection_get_ordersByAscendingDocumentId_byDefault(self): 66 | fs = MockFirestore() 67 | fs._data = {'foo': { 68 | 'beta': {'id': 1}, 69 | 'alpha': {'id': 2} 70 | }} 71 | docs = list(fs.collection('foo').stream()) 72 | self.assertEqual({'id': 2}, docs[0].to_dict()) 73 | 74 | def test_collection_whereEquals(self): 75 | fs = MockFirestore() 76 | fs._data = {'foo': { 77 | 'first': {'valid': True}, 78 | 'second': {'gumby': False} 79 | }} 80 | 81 | docs = list(fs.collection('foo').where('valid', '==', True).stream()) 82 | self.assertEqual({'valid': True}, docs[0].to_dict()) 83 | 84 | def test_collection_whereNotEquals(self): 85 | fs = MockFirestore() 86 | fs._data = {'foo': { 87 | 'first': {'count': 1}, 88 | 'second': {'count': 5} 89 | }} 90 | 91 | docs = list(fs.collection('foo').where('count', '!=', 1).stream()) 92 | self.assertEqual({'count': 5}, docs[0].to_dict()) 93 | 94 | def test_collection_whereLessThan(self): 95 | fs = MockFirestore() 96 | fs._data = {'foo': { 97 | 'first': {'count': 1}, 98 | 'second': {'count': 5} 99 | }} 100 | 101 | docs = list(fs.collection('foo').where('count', '<', 5).stream()) 102 | self.assertEqual({'count': 1}, docs[0].to_dict()) 103 | 104 | def test_collection_whereLessThanOrEqual(self): 105 | fs = MockFirestore() 106 | fs._data = {'foo': { 107 | 'first': {'count': 1}, 108 | 'second': {'count': 5} 109 | }} 110 | 111 | docs = list(fs.collection('foo').where('count', '<=', 5).stream()) 112 | self.assertEqual({'count': 1}, docs[0].to_dict()) 113 | self.assertEqual({'count': 5}, docs[1].to_dict()) 114 | 115 | def test_collection_whereGreaterThan(self): 116 | fs = MockFirestore() 117 | fs._data = {'foo': { 118 | 'first': {'count': 1}, 119 | 'second': {'count': 5} 120 | }} 121 | 122 | docs = list(fs.collection('foo').where('count', '>', 1).stream()) 123 | self.assertEqual({'count': 5}, docs[0].to_dict()) 124 | 125 | def test_collection_whereGreaterThanOrEqual(self): 126 | fs = MockFirestore() 127 | fs._data = {'foo': { 128 | 'first': {'count': 1}, 129 | 'second': {'count': 5} 130 | }} 131 | 132 | docs = list(fs.collection('foo').where('count', '>=', 1).stream()) 133 | self.assertEqual({'count': 1}, docs[0].to_dict()) 134 | self.assertEqual({'count': 5}, docs[1].to_dict()) 135 | 136 | def test_collection_whereMissingField(self): 137 | fs = MockFirestore() 138 | fs._data = {'foo': { 139 | 'first': {'count': 1}, 140 | 'second': {'count': 5} 141 | }} 142 | 143 | docs = list(fs.collection('foo').where('no_field', '==', 1).stream()) 144 | self.assertEqual(len(docs), 0) 145 | 146 | def test_collection_whereNestedField(self): 147 | fs = MockFirestore() 148 | fs._data = {'foo': { 149 | 'first': {'nested': {'a': 1}}, 150 | 'second': {'nested': {'a': 2}} 151 | }} 152 | 153 | docs = list(fs.collection('foo').where('nested.a', '==', 1).stream()) 154 | self.assertEqual(len(docs), 1) 155 | self.assertEqual({'nested': {'a': 1}}, docs[0].to_dict()) 156 | 157 | def test_collection_whereIn(self): 158 | fs = MockFirestore() 159 | fs._data = {'foo': { 160 | 'first': {'field': 'a1'}, 161 | 'second': {'field': 'a2'}, 162 | 'third': {'field': 'a3'}, 163 | 'fourth': {'field': 'a4'}, 164 | }} 165 | 166 | docs = list(fs.collection('foo').where('field', 'in', ['a1', 'a3']).stream()) 167 | self.assertEqual(len(docs), 2) 168 | self.assertEqual({'field': 'a1'}, docs[0].to_dict()) 169 | self.assertEqual({'field': 'a3'}, docs[1].to_dict()) 170 | 171 | def test_collection_whereArrayContains(self): 172 | fs = MockFirestore() 173 | fs._data = {'foo': { 174 | 'first': {'field': ['val4']}, 175 | 'second': {'field': ['val3', 'val2']}, 176 | 'third': {'field': ['val3', 'val2', 'val1']} 177 | }} 178 | 179 | docs = list(fs.collection('foo').where('field', 'array_contains', 'val1').stream()) 180 | self.assertEqual(len(docs), 1) 181 | self.assertEqual(docs[0].to_dict(), {'field': ['val3', 'val2', 'val1']}) 182 | 183 | def test_collection_whereArrayContainsAny(self): 184 | fs = MockFirestore() 185 | fs._data = {'foo': { 186 | 'first': {'field': ['val4']}, 187 | 'second': {'field': ['val3', 'val2']}, 188 | 'third': {'field': ['val3', 'val2', 'val1']} 189 | }} 190 | 191 | contains_any_docs = list(fs.collection('foo').where('field', 'array_contains_any', ['val1', 'val4']).stream()) 192 | self.assertEqual(len(contains_any_docs), 2) 193 | self.assertEqual({'field': ['val4']}, contains_any_docs[0].to_dict()) 194 | self.assertEqual({'field': ['val3', 'val2', 'val1']}, contains_any_docs[1].to_dict()) 195 | 196 | def test_collection_orderBy(self): 197 | fs = MockFirestore() 198 | fs._data = {'foo': { 199 | 'first': {'order': 2}, 200 | 'second': {'order': 1} 201 | }} 202 | 203 | docs = list(fs.collection('foo').order_by('order').stream()) 204 | self.assertEqual({'order': 1}, docs[0].to_dict()) 205 | self.assertEqual({'order': 2}, docs[1].to_dict()) 206 | 207 | def test_collection_orderBy_descending(self): 208 | fs = MockFirestore() 209 | fs._data = {'foo': { 210 | 'first': {'order': 2}, 211 | 'second': {'order': 3}, 212 | 'third': {'order': 1} 213 | }} 214 | 215 | docs = list(fs.collection('foo').order_by('order', direction="DESCENDING").stream()) 216 | self.assertEqual({'order': 3}, docs[0].to_dict()) 217 | self.assertEqual({'order': 2}, docs[1].to_dict()) 218 | self.assertEqual({'order': 1}, docs[2].to_dict()) 219 | 220 | def test_collection_limit(self): 221 | fs = MockFirestore() 222 | fs._data = {'foo': { 223 | 'first': {'id': 1}, 224 | 'second': {'id': 2} 225 | }} 226 | docs = list(fs.collection('foo').limit(1).stream()) 227 | self.assertEqual({'id': 1}, docs[0].to_dict()) 228 | self.assertEqual(1, len(docs)) 229 | 230 | def test_collection_offset(self): 231 | fs = MockFirestore() 232 | fs._data = {'foo': { 233 | 'first': {'id': 1}, 234 | 'second': {'id': 2}, 235 | 'third': {'id': 3} 236 | }} 237 | docs = list(fs.collection('foo').offset(1).stream()) 238 | 239 | self.assertEqual({'id': 2}, docs[0].to_dict()) 240 | self.assertEqual({'id': 3}, docs[1].to_dict()) 241 | self.assertEqual(2, len(docs)) 242 | 243 | def test_collection_orderby_offset(self): 244 | fs = MockFirestore() 245 | fs._data = {'foo': { 246 | 'first': {'id': 1}, 247 | 'second': {'id': 2}, 248 | 'third': {'id': 3} 249 | }} 250 | docs = list(fs.collection('foo').order_by("id").offset(1).stream()) 251 | 252 | self.assertEqual({'id': 2}, docs[0].to_dict()) 253 | self.assertEqual({'id': 3}, docs[1].to_dict()) 254 | self.assertEqual(2, len(docs)) 255 | 256 | def test_collection_start_at(self): 257 | fs = MockFirestore() 258 | fs._data = {'foo': { 259 | 'first': {'id': 1}, 260 | 'second': {'id': 2}, 261 | 'third': {'id': 3} 262 | }} 263 | docs = list(fs.collection('foo').start_at({'id': 2}).stream()) 264 | self.assertEqual({'id': 2}, docs[0].to_dict()) 265 | self.assertEqual(2, len(docs)) 266 | 267 | def test_collection_start_at_order_by(self): 268 | fs = MockFirestore() 269 | fs._data = {'foo': { 270 | 'first': {'id': 1}, 271 | 'second': {'id': 2}, 272 | 'third': {'id': 3} 273 | }} 274 | docs = list(fs.collection('foo').order_by('id').start_at({'id': 2}).stream()) 275 | self.assertEqual({'id': 2}, docs[0].to_dict()) 276 | self.assertEqual(2, len(docs)) 277 | 278 | def test_collection_start_at_doc_snapshot(self): 279 | fs = MockFirestore() 280 | fs._data = {'foo': { 281 | 'first': {'id': 1}, 282 | 'second': {'id': 2}, 283 | 'third': {'id': 3}, 284 | 'fourth': {'id': 4}, 285 | 'fifth': {'id': 5}, 286 | }} 287 | 288 | doc = fs.collection('foo').document('second').get() 289 | 290 | docs = list(fs.collection('foo').order_by('id').start_at(doc).stream()) 291 | self.assertEqual(4, len(docs)) 292 | self.assertEqual({'id': 2}, docs[0].to_dict()) 293 | self.assertEqual({'id': 3}, docs[1].to_dict()) 294 | self.assertEqual({'id': 4}, docs[2].to_dict()) 295 | self.assertEqual({'id': 5}, docs[3].to_dict()) 296 | 297 | def test_collection_start_after(self): 298 | fs = MockFirestore() 299 | fs._data = {'foo': { 300 | 'first': {'id': 1}, 301 | 'second': {'id': 2}, 302 | 'third': {'id': 3} 303 | }} 304 | docs = list(fs.collection('foo').start_after({'id': 1}).stream()) 305 | self.assertEqual({'id': 2}, docs[0].to_dict()) 306 | self.assertEqual(2, len(docs)) 307 | 308 | def test_collection_start_after_similar_objects(self): 309 | fs = MockFirestore() 310 | fs._data = {'foo': { 311 | 'first': {'id': 1, 'value': 1}, 312 | 'second': {'id': 2, 'value': 2}, 313 | 'third': {'id': 3, 'value': 2}, 314 | 'fourth': {'id': 4, 'value': 3} 315 | }} 316 | docs = list(fs.collection('foo').order_by('id').start_after({'id': 3, 'value': 2}).stream()) 317 | self.assertEqual({'id': 4, 'value': 3}, docs[0].to_dict()) 318 | self.assertEqual(1, len(docs)) 319 | 320 | def test_collection_start_after_order_by(self): 321 | fs = MockFirestore() 322 | fs._data = {'foo': { 323 | 'first': {'id': 1}, 324 | 'second': {'id': 2}, 325 | 'third': {'id': 3} 326 | }} 327 | docs = list(fs.collection('foo').order_by('id').start_after({'id': 2}).stream()) 328 | self.assertEqual({'id': 3}, docs[0].to_dict()) 329 | self.assertEqual(1, len(docs)) 330 | 331 | def test_collection_start_after_doc_snapshot(self): 332 | fs = MockFirestore() 333 | fs._data = {'foo': { 334 | 'second': {'id': 2}, 335 | 'third': {'id': 3}, 336 | 'fourth': {'id': 4}, 337 | 'fifth': {'id': 5}, 338 | }} 339 | 340 | doc = fs.collection('foo').document('second').get() 341 | 342 | docs = list(fs.collection('foo').order_by('id').start_after(doc).stream()) 343 | self.assertEqual(3, len(docs)) 344 | self.assertEqual({'id': 3}, docs[0].to_dict()) 345 | self.assertEqual({'id': 4}, docs[1].to_dict()) 346 | self.assertEqual({'id': 5}, docs[2].to_dict()) 347 | 348 | def test_collection_end_before(self): 349 | fs = MockFirestore() 350 | fs._data = {'foo': { 351 | 'first': {'id': 1}, 352 | 'second': {'id': 2}, 353 | 'third': {'id': 3} 354 | }} 355 | docs = list(fs.collection('foo').end_before({'id': 2}).stream()) 356 | self.assertEqual({'id': 1}, docs[0].to_dict()) 357 | self.assertEqual(1, len(docs)) 358 | 359 | def test_collection_end_before_order_by(self): 360 | fs = MockFirestore() 361 | fs._data = {'foo': { 362 | 'first': {'id': 1}, 363 | 'second': {'id': 2}, 364 | 'third': {'id': 3} 365 | }} 366 | docs = list(fs.collection('foo').order_by('id').end_before({'id': 2}).stream()) 367 | self.assertEqual({'id': 1}, docs[0].to_dict()) 368 | self.assertEqual(1, len(docs)) 369 | 370 | def test_collection_end_before_doc_snapshot(self): 371 | fs = MockFirestore() 372 | fs._data = {'foo': { 373 | 'first': {'id': 1}, 374 | 'second': {'id': 2}, 375 | 'third': {'id': 3}, 376 | 'fourth': {'id': 4}, 377 | 'fifth': {'id': 5}, 378 | }} 379 | 380 | doc = fs.collection('foo').document('fourth').get() 381 | 382 | docs = list(fs.collection('foo').order_by('id').end_before(doc).stream()) 383 | self.assertEqual(3, len(docs)) 384 | 385 | self.assertEqual({'id': 1}, docs[0].to_dict()) 386 | self.assertEqual({'id': 2}, docs[1].to_dict()) 387 | self.assertEqual({'id': 3}, docs[2].to_dict()) 388 | 389 | def test_collection_end_at(self): 390 | fs = MockFirestore() 391 | fs._data = {'foo': { 392 | 'first': {'id': 1}, 393 | 'second': {'id': 2}, 394 | 'third': {'id': 3} 395 | }} 396 | docs = list(fs.collection('foo').end_at({'id': 2}).stream()) 397 | self.assertEqual({'id': 2}, docs[1].to_dict()) 398 | self.assertEqual(2, len(docs)) 399 | 400 | def test_collection_end_at_order_by(self): 401 | fs = MockFirestore() 402 | fs._data = {'foo': { 403 | 'first': {'id': 1}, 404 | 'second': {'id': 2}, 405 | 'third': {'id': 3} 406 | }} 407 | docs = list(fs.collection('foo').order_by('id').end_at({'id': 2}).stream()) 408 | self.assertEqual({'id': 2}, docs[1].to_dict()) 409 | self.assertEqual(2, len(docs)) 410 | 411 | def test_collection_end_at_doc_snapshot(self): 412 | fs = MockFirestore() 413 | fs._data = {'foo': { 414 | 'first': {'id': 1}, 415 | 'second': {'id': 2}, 416 | 'third': {'id': 3}, 417 | 'fourth': {'id': 4}, 418 | 'fifth': {'id': 5}, 419 | }} 420 | 421 | doc = fs.collection('foo').document('fourth').get() 422 | 423 | docs = list(fs.collection('foo').order_by('id').end_at(doc).stream()) 424 | self.assertEqual(4, len(docs)) 425 | 426 | self.assertEqual({'id': 1}, docs[0].to_dict()) 427 | self.assertEqual({'id': 2}, docs[1].to_dict()) 428 | self.assertEqual({'id': 3}, docs[2].to_dict()) 429 | self.assertEqual({'id': 4}, docs[3].to_dict()) 430 | 431 | def test_collection_limitAndOrderBy(self): 432 | fs = MockFirestore() 433 | fs._data = {'foo': { 434 | 'first': {'order': 2}, 435 | 'second': {'order': 1}, 436 | 'third': {'order': 3} 437 | }} 438 | docs = list(fs.collection('foo').order_by('order').limit(2).stream()) 439 | self.assertEqual({'order': 1}, docs[0].to_dict()) 440 | self.assertEqual({'order': 2}, docs[1].to_dict()) 441 | 442 | def test_collection_listDocuments(self): 443 | fs = MockFirestore() 444 | fs._data = {'foo': { 445 | 'first': {'order': 2}, 446 | 'second': {'order': 1}, 447 | 'third': {'order': 3} 448 | }} 449 | doc_refs = list(fs.collection('foo').list_documents()) 450 | self.assertEqual(3, len(doc_refs)) 451 | for doc_ref in doc_refs: 452 | self.assertIsInstance(doc_ref, DocumentReference) 453 | 454 | def test_collection_stream(self): 455 | fs = MockFirestore() 456 | fs._data = {'foo': { 457 | 'first': {'order': 2}, 458 | 'second': {'order': 1}, 459 | 'third': {'order': 3} 460 | }} 461 | doc_snapshots = list(fs.collection('foo').stream()) 462 | self.assertEqual(3, len(doc_snapshots)) 463 | for doc_snapshot in doc_snapshots: 464 | self.assertIsInstance(doc_snapshot, DocumentSnapshot) 465 | 466 | def test_collection_parent(self): 467 | fs = MockFirestore() 468 | fs._data = {'foo': { 469 | 'first': {'order': 2}, 470 | 'second': {'order': 1}, 471 | 'third': {'order': 3} 472 | }} 473 | doc_snapshots = fs.collection('foo').stream() 474 | for doc_snapshot in doc_snapshots: 475 | doc_reference = doc_snapshot.reference 476 | subcollection = doc_reference.collection('order') 477 | self.assertIs(subcollection.parent, doc_reference) 478 | 479 | def test_collection_addDocument(self): 480 | fs = MockFirestore() 481 | fs._data = {'foo': {}} 482 | doc_id = 'bar' 483 | doc_content = {'id': doc_id, 'xy': 'z'} 484 | timestamp, doc_ref = fs.collection('foo').add(doc_content) 485 | self.assertEqual(doc_content, doc_ref.get().to_dict()) 486 | 487 | doc = fs.collection('foo').document(doc_id).get().to_dict() 488 | self.assertEqual(doc_content, doc) 489 | 490 | with self.assertRaises(AlreadyExists): 491 | fs.collection('foo').add(doc_content) 492 | 493 | def test_collection_useDocumentIdKwarg(self): 494 | fs = MockFirestore() 495 | fs._data = {'foo': { 496 | 'first': {'id': 1} 497 | }} 498 | doc = fs.collection('foo').document(document_id='first').get() 499 | self.assertEqual({'id': 1}, doc.to_dict()) 500 | -------------------------------------------------------------------------------- /tests/test_document_reference.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from google.cloud import firestore 4 | 5 | from mockfirestore import MockFirestore, NotFound 6 | 7 | 8 | class TestDocumentReference(TestCase): 9 | 10 | def test_get_document_by_path(self): 11 | fs = MockFirestore() 12 | fs._data = {'foo': { 13 | 'first': {'id': 1} 14 | }} 15 | doc = fs.document('foo/first').get() 16 | self.assertEqual({'id': 1}, doc.to_dict()) 17 | self.assertEqual('first', doc.id) 18 | 19 | def test_set_document_by_path(self): 20 | fs = MockFirestore() 21 | fs._data = {} 22 | doc_content = {'id': 'bar'} 23 | fs.document('foo/doc1/bar/doc2').set(doc_content) 24 | doc = fs.document('foo/doc1/bar/doc2').get().to_dict() 25 | self.assertEqual(doc_content, doc) 26 | 27 | def test_document_get_returnsDocument(self): 28 | fs = MockFirestore() 29 | fs._data = {'foo': { 30 | 'first': {'id': 1} 31 | }} 32 | doc = fs.collection('foo').document('first').get() 33 | self.assertEqual({'id': 1}, doc.to_dict()) 34 | self.assertEqual('first', doc.id) 35 | 36 | def test_document_get_documentIdEqualsKey(self): 37 | fs = MockFirestore() 38 | fs._data = {'foo': { 39 | 'first': {'id': 1} 40 | }} 41 | doc_ref = fs.collection('foo').document('first') 42 | self.assertEqual('first', doc_ref.id) 43 | 44 | def test_document_get_newDocumentReturnsDefaultId(self): 45 | fs = MockFirestore() 46 | doc_ref = fs.collection('foo').document() 47 | doc = doc_ref.get() 48 | self.assertNotEqual(None, doc_ref.id) 49 | self.assertFalse(doc.exists) 50 | 51 | def test_document_get_documentDoesNotExist(self): 52 | fs = MockFirestore() 53 | fs._data = {'foo': {}} 54 | doc = fs.collection('foo').document('bar').get().to_dict() 55 | self.assertEqual({}, doc) 56 | 57 | def test_get_nestedDocument(self): 58 | fs = MockFirestore() 59 | fs._data = {'top_collection': { 60 | 'top_document': { 61 | 'id': 1, 62 | 'nested_collection': { 63 | 'nested_document': {'id': 1.1} 64 | } 65 | } 66 | }} 67 | doc = fs.collection('top_collection')\ 68 | .document('top_document')\ 69 | .collection('nested_collection')\ 70 | .document('nested_document')\ 71 | .get().to_dict() 72 | 73 | self.assertEqual({'id': 1.1}, doc) 74 | 75 | def test_get_nestedDocument_documentDoesNotExist(self): 76 | fs = MockFirestore() 77 | fs._data = {'top_collection': { 78 | 'top_document': { 79 | 'id': 1, 80 | 'nested_collection': {} 81 | } 82 | }} 83 | doc = fs.collection('top_collection')\ 84 | .document('top_document')\ 85 | .collection('nested_collection')\ 86 | .document('nested_document')\ 87 | .get().to_dict() 88 | 89 | self.assertEqual({}, doc) 90 | 91 | def test_document_set_setsContentOfDocument(self): 92 | fs = MockFirestore() 93 | fs._data = {'foo': {}} 94 | doc_content = {'id': 'bar'} 95 | fs.collection('foo').document('bar').set(doc_content) 96 | doc = fs.collection('foo').document('bar').get().to_dict() 97 | self.assertEqual(doc_content, doc) 98 | 99 | def test_document_set_mergeNewValue(self): 100 | fs = MockFirestore() 101 | fs._data = {'foo': { 102 | 'first': {'id': 1} 103 | }} 104 | fs.collection('foo').document('first').set({'updated': True}, merge=True) 105 | doc = fs.collection('foo').document('first').get().to_dict() 106 | self.assertEqual({'id': 1, 'updated': True}, doc) 107 | 108 | def test_document_set_mergeNewValueForNonExistentDoc(self): 109 | fs = MockFirestore() 110 | fs.collection('foo').document('first').set({'updated': True}, merge=True) 111 | doc = fs.collection('foo').document('first').get().to_dict() 112 | self.assertEqual({'updated': True}, doc) 113 | 114 | def test_document_set_overwriteValue(self): 115 | fs = MockFirestore() 116 | fs._data = {'foo': { 117 | 'first': {'id': 1} 118 | }} 119 | fs.collection('foo').document('first').set({'new_id': 1}, merge=False) 120 | doc = fs.collection('foo').document('first').get().to_dict() 121 | self.assertEqual({'new_id': 1}, doc) 122 | 123 | def test_document_set_isolation(self): 124 | fs = MockFirestore() 125 | fs._data = {'foo': {}} 126 | doc_content = {'id': 'bar'} 127 | fs.collection('foo').document('bar').set(doc_content) 128 | doc_content['id'] = 'new value' 129 | doc = fs.collection('foo').document('bar').get().to_dict() 130 | self.assertEqual({'id': 'bar'}, doc) 131 | 132 | def test_document_update_addNewValue(self): 133 | fs = MockFirestore() 134 | fs._data = {'foo': { 135 | 'first': {'id': 1} 136 | }} 137 | fs.collection('foo').document('first').update({'updated': True}) 138 | doc = fs.collection('foo').document('first').get().to_dict() 139 | self.assertEqual({'id': 1, 'updated': True}, doc) 140 | 141 | def test_document_update_changeExistingValue(self): 142 | fs = MockFirestore() 143 | fs._data = {'foo': { 144 | 'first': {'id': 1} 145 | }} 146 | fs.collection('foo').document('first').update({'id': 2}) 147 | doc = fs.collection('foo').document('first').get().to_dict() 148 | self.assertEqual({'id': 2}, doc) 149 | 150 | def test_document_update_documentDoesNotExist(self): 151 | fs = MockFirestore() 152 | with self.assertRaises(NotFound): 153 | fs.collection('foo').document('nonexistent').update({'id': 2}) 154 | docsnap = fs.collection('foo').document('nonexistent').get() 155 | self.assertFalse(docsnap.exists) 156 | 157 | def test_document_update_isolation(self): 158 | fs = MockFirestore() 159 | fs._data = {'foo': { 160 | 'first': {'nested': {'id': 1}} 161 | }} 162 | update_doc = {'nested': {'id': 2}} 163 | fs.collection('foo').document('first').update(update_doc) 164 | update_doc['nested']['id'] = 3 165 | doc = fs.collection('foo').document('first').get().to_dict() 166 | self.assertEqual({'nested': {'id': 2}}, doc) 167 | 168 | def test_document_update_transformerIncrementBasic(self): 169 | fs = MockFirestore() 170 | fs._data = {'foo': { 171 | 'first': {'count': 1} 172 | }} 173 | fs.collection('foo').document('first').update({'count': firestore.Increment(2)}) 174 | 175 | doc = fs.collection('foo').document('first').get().to_dict() 176 | self.assertEqual(doc, {'count': 3}) 177 | 178 | def test_document_update_transformerIncrementNested(self): 179 | fs = MockFirestore() 180 | fs._data = {'foo': { 181 | 'first': { 182 | 'nested': {'count': 1}, 183 | 'other': {'likes': 0}, 184 | } 185 | }} 186 | fs.collection('foo').document('first').update({ 187 | 'nested': {'count': firestore.Increment(-1)}, 188 | 'other': {'likes': firestore.Increment(1), 'smoked': 'salmon'}, 189 | }) 190 | 191 | doc = fs.collection('foo').document('first').get().to_dict() 192 | self.assertEqual(doc, { 193 | 'nested': {'count': 0}, 194 | 'other': {'likes': 1, 'smoked': 'salmon'} 195 | }) 196 | 197 | def test_document_update_transformerIncrementNonExistent(self): 198 | fs = MockFirestore() 199 | fs._data = {'foo': { 200 | 'first': {'spicy': 'tuna'} 201 | }} 202 | fs.collection('foo').document('first').update({'count': firestore.Increment(1)}) 203 | 204 | doc = fs.collection('foo').document('first').get().to_dict() 205 | self.assertEqual(doc, {'count': 1, 'spicy': 'tuna'}) 206 | 207 | def test_document_delete_documentDoesNotExistAfterDelete(self): 208 | fs = MockFirestore() 209 | fs._data = {'foo': { 210 | 'first': {'id': 1} 211 | }} 212 | fs.collection('foo').document('first').delete() 213 | doc = fs.collection('foo').document('first').get() 214 | self.assertEqual(False, doc.exists) 215 | 216 | def test_document_parent(self): 217 | fs = MockFirestore() 218 | fs._data = {'foo': { 219 | 'first': {'id': 1} 220 | }} 221 | coll = fs.collection('foo') 222 | document = coll.document('first') 223 | self.assertIs(document.parent, coll) 224 | 225 | def test_document_update_transformerArrayUnionBasic(self): 226 | fs = MockFirestore() 227 | fs._data = {"foo": {"first": {"arr": [1, 2]}}} 228 | fs.collection("foo").document("first").update( 229 | {"arr": firestore.ArrayUnion([3, 4])} 230 | ) 231 | doc = fs.collection("foo").document("first").get().to_dict() 232 | self.assertEqual(doc["arr"], [1, 2, 3, 4]) 233 | 234 | def test_document_update_transformerArrayUnionNested(self): 235 | fs = MockFirestore() 236 | fs._data = {'foo': { 237 | 'first': { 238 | 'nested': {'arr': [1]}, 239 | 'other': {'labels': ["a"]}, 240 | } 241 | }} 242 | fs.collection('foo').document('first').update({ 243 | 'nested': {'arr': firestore.ArrayUnion([2])}, 244 | 'other': {'labels': firestore.ArrayUnion(["b"]), 'smoked': 'salmon'}, 245 | }) 246 | 247 | doc = fs.collection('foo').document('first').get().to_dict() 248 | self.assertEqual(doc, { 249 | 'nested': {'arr': [1, 2]}, 250 | 'other': {'labels': ["a", "b"], 'smoked': 'salmon'} 251 | }) 252 | 253 | def test_document_update_transformerArrayUnionNonExistent(self): 254 | fs = MockFirestore() 255 | fs._data = {'foo': { 256 | 'first': {'spicy': 'tuna'} 257 | }} 258 | fs.collection('foo').document('first').update({'arr': firestore.ArrayUnion([1])}) 259 | 260 | doc = fs.collection('foo').document('first').get().to_dict() 261 | self.assertEqual(doc, {'arr': [1], 'spicy': 'tuna'}) 262 | 263 | def test_document_update_nestedFieldDotNotation(self): 264 | fs = MockFirestore() 265 | fs._data = {"foo": {"first": {"nested": {"value": 1, "unchanged": "foo"}}}} 266 | 267 | fs.collection("foo").document("first").update({"nested.value": 2}) 268 | 269 | doc = fs.collection("foo").document("first").get().to_dict() 270 | self.assertEqual(doc, {"nested": {"value": 2, "unchanged": "foo"}}) 271 | 272 | def test_document_update_nestedFieldDotNotationNestedFieldCreation(self): 273 | fs = MockFirestore() 274 | fs._data = {"foo": {"first": {"other": None}}} # non-existent nested field is created 275 | 276 | fs.collection("foo").document("first").update({"nested.value": 2}) 277 | 278 | doc = fs.collection("foo").document("first").get().to_dict() 279 | self.assertEqual(doc, {"nested": {"value": 2}, "other": None}) 280 | 281 | def test_document_update_nestedFieldDotNotationMultipleNested(self): 282 | fs = MockFirestore() 283 | fs._data = {"foo": {"first": {"other": None}}} 284 | 285 | fs.collection("foo").document("first").update({"nested.subnested.value": 42}) 286 | 287 | doc = fs.collection("foo").document("first").get().to_dict() 288 | self.assertEqual(doc, {"nested": {"subnested": {"value": 42}}, "other": None}) 289 | 290 | def test_document_update_nestedFieldDotNotationMultipleNestedWithTransformer(self): 291 | fs = MockFirestore() 292 | fs._data = {"foo": {"first": {"other": None}}} 293 | 294 | fs.collection("foo").document("first").update( 295 | {"nested.subnested.value": firestore.ArrayUnion([1, 3])} 296 | ) 297 | 298 | doc = fs.collection("foo").document("first").get().to_dict() 299 | self.assertEqual(doc, {"nested": {"subnested": {"value": [1, 3]}}, "other": None}) 300 | 301 | 302 | def test_document_update_transformerSentinel(self): 303 | fs = MockFirestore() 304 | fs._data = {'foo': { 305 | 'first': {'spicy': 'tuna'} 306 | }} 307 | fs.collection('foo').document('first').update({"spicy": firestore.DELETE_FIELD}) 308 | 309 | doc = fs.collection("foo").document("first").get().to_dict() 310 | self.assertEqual(doc, {}) 311 | 312 | def test_document_update_transformerArrayRemoveBasic(self): 313 | fs = MockFirestore() 314 | fs._data = {"foo": {"first": {"arr": [1, 2, 3, 4]}}} 315 | fs.collection("foo").document("first").update( 316 | {"arr": firestore.ArrayRemove([3, 4])} 317 | ) 318 | doc = fs.collection("foo").document("first").get().to_dict() 319 | self.assertEqual(doc["arr"], [1, 2]) 320 | 321 | def test_document_update_transformerArrayRemoveNonExistentField(self): 322 | fs = MockFirestore() 323 | fs._data = {"foo": {"first": {"arr": [1, 2, 3, 4]}}} 324 | fs.collection("foo").document("first").update( 325 | {"arr": firestore.ArrayRemove([5])} 326 | ) 327 | doc = fs.collection("foo").document("first").get().to_dict() 328 | self.assertEqual(doc["arr"], [1, 2, 3, 4]) 329 | 330 | def test_document_update_transformerArrayRemoveNonExistentArray(self): 331 | fs = MockFirestore() 332 | fs._data = {"foo": {"first": {"arr": [1, 2, 3, 4]}}} 333 | fs.collection("foo").document("first").update( 334 | {"non_existent_array": firestore.ArrayRemove([1, 2])} 335 | ) 336 | doc = fs.collection("foo").document("first").get().to_dict() 337 | self.assertEqual(doc["arr"], [1, 2, 3, 4]) 338 | 339 | -------------------------------------------------------------------------------- /tests/test_document_snapshot.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from mockfirestore import MockFirestore 4 | 5 | 6 | class TestDocumentSnapshot(TestCase): 7 | def test_documentSnapshot_toDict(self): 8 | fs = MockFirestore() 9 | fs._data = {'foo': { 10 | 'first': {'id': 1} 11 | }} 12 | doc = fs.collection('foo').document('first').get() 13 | self.assertEqual({'id': 1}, doc.to_dict()) 14 | 15 | def test_documentSnapshot_toDict_isolation(self): 16 | fs = MockFirestore() 17 | fs._data = {'foo': { 18 | 'first': {'id': 1} 19 | }} 20 | doc_dict = fs.collection('foo').document('first').get().to_dict() 21 | fs._data['foo']['first']['id'] = 2 22 | self.assertEqual({'id': 1}, doc_dict) 23 | 24 | def test_documentSnapshot_exists(self): 25 | fs = MockFirestore() 26 | fs._data = {'foo': { 27 | 'first': {'id': 1} 28 | }} 29 | doc = fs.collection('foo').document('first').get() 30 | self.assertTrue(doc.exists) 31 | 32 | def test_documentSnapshot_exists_documentDoesNotExist(self): 33 | fs = MockFirestore() 34 | fs._data = {'foo': { 35 | 'first': {'id': 1} 36 | }} 37 | doc = fs.collection('foo').document('second').get() 38 | self.assertFalse(doc.exists) 39 | 40 | def test_documentSnapshot_reference(self): 41 | fs = MockFirestore() 42 | fs._data = {'foo': { 43 | 'first': {'id': 1} 44 | }} 45 | doc_ref = fs.collection('foo').document('second') 46 | doc_snapshot = doc_ref.get() 47 | self.assertIs(doc_ref, doc_snapshot.reference) 48 | 49 | def test_documentSnapshot_id(self): 50 | fs = MockFirestore() 51 | fs._data = {'foo': { 52 | 'first': {'id': 1} 53 | }} 54 | doc = fs.collection('foo').document('first').get() 55 | self.assertIsInstance(doc.id, str) 56 | 57 | def test_documentSnapshot_create_time(self): 58 | fs = MockFirestore() 59 | fs._data = {'foo': { 60 | 'first': {'id': 1} 61 | }} 62 | doc = fs.collection('foo').document('first').get() 63 | self.assertIsNotNone(doc.create_time) 64 | 65 | def test_documentSnapshot_update_time(self): 66 | fs = MockFirestore() 67 | fs._data = {'foo': { 68 | 'first': {'id': 1} 69 | }} 70 | doc = fs.collection('foo').document('first').get() 71 | self.assertIsNotNone(doc.update_time) 72 | 73 | def test_documentSnapshot_read_time(self): 74 | fs = MockFirestore() 75 | fs._data = {'foo': { 76 | 'first': {'id': 1} 77 | }} 78 | doc = fs.collection('foo').document('first').get() 79 | self.assertIsNotNone(doc.read_time) 80 | 81 | def test_documentSnapshot_get_by_existing_field_path(self): 82 | fs = MockFirestore() 83 | fs._data = {'foo': { 84 | 'first': {'id': 1, 'contact': { 85 | 'email': 'email@test.com' 86 | }} 87 | }} 88 | doc = fs.collection('foo').document('first').get() 89 | self.assertEqual(doc.get('contact.email'), 'email@test.com') 90 | 91 | def test_documentSnapshot_get_by_non_existing_field_path(self): 92 | fs = MockFirestore() 93 | fs._data = {'foo': { 94 | 'first': {'id': 1, 'contact': { 95 | 'email': 'email@test.com' 96 | }} 97 | }} 98 | doc = fs.collection('foo').document('first').get() 99 | with self.assertRaises(KeyError): 100 | doc.get('contact.phone') 101 | 102 | def test_documentSnapshot_get_in_an_non_existing_document(self): 103 | fs = MockFirestore() 104 | fs._data = {'foo': { 105 | 'first': {'id': 1, 'contact': { 106 | 'email': 'email@test.com' 107 | }} 108 | }} 109 | doc = fs.collection('foo').document('second').get() 110 | self.assertIsNone(doc.get('contact.email')) 111 | 112 | def test_documentSnapshot_get_returns_a_copy_of_the_data_stored(self): 113 | fs = MockFirestore() 114 | fs._data = {'foo': { 115 | 'first': {'id': 1, 'contact': { 116 | 'email': 'email@test.com' 117 | }} 118 | }} 119 | doc = fs.collection('foo').document('first').get() 120 | self.assertIsNot( 121 | doc.get('contact'),fs._data['foo']['first']['contact'] 122 | ) 123 | -------------------------------------------------------------------------------- /tests/test_mock_client.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from mockfirestore import MockFirestore 4 | 5 | 6 | class TestMockFirestore(TestCase): 7 | def test_client_get_all(self): 8 | fs = MockFirestore() 9 | fs._data = {'foo': { 10 | 'first': {'id': 1}, 11 | 'second': {'id': 2} 12 | }} 13 | doc = fs.collection('foo').document('first') 14 | results = list(fs.get_all([doc])) 15 | returned_doc_snapshot = results[0].to_dict() 16 | expected_doc_snapshot = doc.get().to_dict() 17 | self.assertEqual(returned_doc_snapshot, expected_doc_snapshot) 18 | 19 | def test_client_collections(self): 20 | fs = MockFirestore() 21 | fs._data = { 22 | 'foo': { 23 | 'first': {'id': 1}, 24 | 'second': {'id': 2} 25 | }, 26 | 'bar': {} 27 | } 28 | collections = fs.collections() 29 | expected_collections = fs._data 30 | 31 | self.assertEqual(len(collections), len(expected_collections)) 32 | for collection in collections: 33 | self.assertTrue(collection._path[0] in expected_collections) 34 | -------------------------------------------------------------------------------- /tests/test_timestamp.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import datetime as dt 3 | 4 | from mockfirestore import Timestamp 5 | 6 | 7 | class TestTimestamp(unittest.TestCase): 8 | def test_timestamp(self): 9 | dt_timestamp = dt.now().timestamp() 10 | timestamp = Timestamp(dt_timestamp) 11 | 12 | seconds, nanos = str(dt_timestamp).split('.') 13 | self.assertEqual(seconds, timestamp.seconds) 14 | self.assertEqual(nanos, timestamp.nanos) 15 | 16 | -------------------------------------------------------------------------------- /tests/test_transaction.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from mockfirestore import MockFirestore, Transaction 3 | 4 | 5 | class TestTransaction(TestCase): 6 | def setUp(self) -> None: 7 | self.fs = MockFirestore() 8 | self.fs._data = {'foo': { 9 | 'first': {'id': 1}, 10 | 'second': {'id': 2} 11 | }} 12 | 13 | def test_transaction_getAll(self): 14 | with Transaction(self.fs) as transaction: 15 | transaction._begin() 16 | docs = [self.fs.collection('foo').document(doc_name) 17 | for doc_name in self.fs._data['foo']] 18 | results = list(transaction.get_all(docs)) 19 | returned_docs_snapshots = [result.to_dict() for result in results] 20 | expected_doc_snapshots = [doc.get().to_dict() for doc in docs] 21 | for expected_snapshot in expected_doc_snapshots: 22 | self.assertIn(expected_snapshot, returned_docs_snapshots) 23 | 24 | def test_transaction_getDocument(self): 25 | with Transaction(self.fs) as transaction: 26 | transaction._begin() 27 | doc = self.fs.collection('foo').document('first') 28 | returned_doc = next(transaction.get(doc)) 29 | self.assertEqual(doc.get().to_dict(), returned_doc.to_dict()) 30 | 31 | def test_transaction_getQuery(self): 32 | with Transaction(self.fs) as transaction: 33 | transaction._begin() 34 | query = self.fs.collection('foo').order_by('id') 35 | returned_docs = [doc.to_dict() for doc in transaction.get(query)] 36 | query = self.fs.collection('foo').order_by('id') 37 | expected_docs = [doc.to_dict() for doc in query.stream()] 38 | self.assertEqual(returned_docs, expected_docs) 39 | 40 | def test_transaction_set_setsContentOfDocument(self): 41 | doc_content = {'id': '3'} 42 | doc_ref = self.fs.collection('foo').document('third') 43 | with Transaction(self.fs) as transaction: 44 | transaction._begin() 45 | transaction.set(doc_ref, doc_content) 46 | self.assertEqual(doc_ref.get().to_dict(), doc_content) 47 | 48 | def test_transaction_set_mergeNewValue(self): 49 | doc = self.fs.collection('foo').document('first') 50 | with Transaction(self.fs) as transaction: 51 | transaction._begin() 52 | transaction.set(doc, {'updated': True}, merge=True) 53 | updated_doc = {'id': 1, 'updated': True} 54 | self.assertEqual(doc.get().to_dict(), updated_doc) 55 | 56 | def test_transaction_update_changeExistingValue(self): 57 | doc = self.fs.collection('foo').document('first') 58 | with Transaction(self.fs) as transaction: 59 | transaction._begin() 60 | transaction.update(doc, {'updated': False}) 61 | updated_doc = {'id': 1, 'updated': False} 62 | self.assertEqual(doc.get().to_dict(), updated_doc) 63 | 64 | def test_transaction_delete_documentDoesNotExistAfterDelete(self): 65 | doc = self.fs.collection('foo').document('first') 66 | with Transaction(self.fs) as transaction: 67 | transaction._begin() 68 | transaction.delete(doc) 69 | doc = self.fs.collection('foo').document('first').get() 70 | self.assertEqual(False, doc.exists) 71 | 72 | 73 | 74 | --------------------------------------------------------------------------------