diff --git a/.travis.yml b/.travis.yml
index b49557a3..9f770aac 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,8 +8,6 @@ python:
env:
- ADMIN_PARTY=true COUCHDB_VERSION=2.3.1
- ADMIN_PARTY=false COUCHDB_VERSION=2.3.1
- - ADMIN_PARTY=true COUCHDB_VERSION=1.7.2
- - ADMIN_PARTY=false COUCHDB_VERSION=1.7.2
services:
- docker
diff --git a/CHANGES.md b/CHANGES.md
index 6ea317b1..75402ba5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,7 +1,29 @@
# UNRELEASED
+- [DEPRECATED] This library is end-of-life and no longer supported.
+
+# 2.15.0 (2021-08-26)
+- [NEW] Override `dict.get` method for `CouchDatabase` to add `remote` parameter allowing it to
+ retrieve a remote document if specified.
+- [FIXED] Fixed the documentation for `bookmarks`.
+- [FIXED] Also exit `follow_replication` for `failed` state.
+- [FIXED] Fixed result paging for grouped view queries.
+- [FIXED] Incorrect use of username as account name in `Cloudant.bluemix()`.
+- [FIXED] Use custom encoder (if provided) for all view `key` params not just `keys`.
+- [FIXED] Support boolean type for `key`, `endkey`, and `startkey` in view requests.
+- [DEPRECATED] This library is now deprecated and will be EOL on Dec 31 2021.
+- [REMOVED] Removed Python 2 compatibility from the supported environments.
+- [IMPROVED] Documented use of `None` account name and url override for `Cloudant.iam()`.
+- [IMPROVED] - Document IDs and attachment names are now rejected if they could cause an unexpected
+ Cloudant request. We have seen that some applications pass unsantized document IDs to SDK functions
+ (e.g. direct from user requests). In response to this we have updated many functions to reject
+ obviously invalid paths. However, for complete safety applications must still validate that
+ document IDs and attachment names match expected patterns.
+
+# 2.14.0 (2020-08-17)
- [FIXED] Set default value for `partitioned` parameter to false when creating a design document.
- [FIXED] Corrected setting of `partitioned` flag for `create_query_index` requests.
+- [FIXED] Added a workaround for installation on Python 2.
# 2.13.0 (2020-04-16)
@@ -127,7 +149,7 @@
# 2.2.0 (2016-10-20)
-- [NEW] Added auto connect feature to the client constructor.
+- [NEW] Added auto connect feature to the client constructor.
- [FIXED] Requests session is no longer valid after disconnect.
# 2.1.1 (2016-10-03)
diff --git a/Jenkinsfile b/Jenkinsfile
index ddf21dd4..cc2d0ad4 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -1,7 +1,7 @@
def getEnvForSuite(suiteName) {
// Base environment variables
def envVars = [
- "CLOUDANT_ACCOUNT=$DB_USER",
+ "DB_URL=${SDKS_TEST_SERVER_URL}",
"RUN_CLOUDANT_TESTS=1",
"SKIP_DB_UPDATES=1" // Disable pending resolution of case 71610
]
@@ -13,6 +13,7 @@ def getEnvForSuite(suiteName) {
case 'iam':
// Setting IAM_API_KEY forces tests to run using an IAM enabled client.
envVars.add("IAM_API_KEY=$DB_IAM_API_KEY")
+ envVars.add("IAM_TOKEN_URL=$SDKS_TEST_IAM_URL")
break
case 'cookie':
case 'simplejson':
@@ -24,12 +25,12 @@ def getEnvForSuite(suiteName) {
}
def setupPythonAndTest(pythonVersion, testSuite) {
- node {
+ node('sdks-executor') {
// Unstash the source on this node
unstash name: 'source'
// Set up the environment and test
- withCredentials([usernamePassword(credentialsId: 'clientlibs-test', usernameVariable: 'DB_USER', passwordVariable: 'DB_PASSWORD'),
- string(credentialsId: 'clientlibs-test-iam', variable: 'DB_IAM_API_KEY')]) {
+ withCredentials([usernamePassword(credentialsId: 'testServerLegacy', usernameVariable: 'DB_USER', passwordVariable: 'DB_PASSWORD'),
+ string(credentialsId: 'testServerIamApiKey', variable: 'DB_IAM_API_KEY')]) {
withEnv(getEnvForSuite("${testSuite}")) {
try {
sh """
diff --git a/MANIFEST.in b/MANIFEST.in
index 96ebb5e1..7c050d91 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1 +1 @@
-include requirements.txt VERSION
+include requirements.txt VERSION LICENSE
diff --git a/MIGRATION.md b/MIGRATION.md
new file mode 100644
index 00000000..3323e1d8
--- /dev/null
+++ b/MIGRATION.md
@@ -0,0 +1,114 @@
+# Migrating to the `cloudant-python-sdk` library
+This document is to assist in migrating from the `python-cloudant` (module: `cloudant`) to the newly supported [`cloudant-python-sdk`](https://github.com/IBM/cloudant-python-sdk) (module: `ibmcloudant`).
+
+## Initializing the client connection
+There are several ways to create a client connection in `cloudant-python-sdk`:
+1. [Environment variables](https://github.com/IBM/cloudant-python-sdk#authentication-with-environment-variables)
+2. [External configuration file](https://github.com/IBM/cloudant-python-sdk#authentication-with-external-configuration)
+3. [Programmatically](https://github.com/IBM/cloudant-python-sdk#programmatic-authentication)
+
+[See the README](https://github.com/IBM/cloudant-python-sdk#code-examples) for code examples on using environment variables.
+
+## Other differences
+1. The `cloudant-python-sdk` library does not support local dictionary caching of database and document objects.
+1. There are no context managers in `cloudant-python-sdk`. To reproduce the behaviour of the `python-cloudant`
+context managers in `cloudant-python-sdk` users need to explicitly call the specific operations against the
+remote HTTP API. For example, in the case of the document context manager, this would mean doing both a `get_document`
+to fetch and a `put_document` to save.
+1. In `cloudant-python-sdk` View, Search, and Query (aka `_find` endpoint) operation responses contain raw JSON
+content like using `raw_result=True` in `python-cloudant`.
+1. Replay adapters are replaced by the [automatic retries](https://github.com/IBM/ibm-cloud-sdk-common/#automatic-retries) feature for failed requests.
+1. Error handling is not transferable from `python-cloudant` to `cloudant-python-sdk`. For more information go to the [Error handling section](https://cloud.ibm.com/apidocs/cloudant?code=python#error-handling) in our API docs.
+1. Custom HTTP client configurations in `python-cloudant` can be set differently in
+ `cloudant-python-sdk`. For more information go to the
+ [Configuring the HTTP client section](https://github.com/IBM/ibm-cloud-sdk-common/#configuring-the-http-client)
+ in the IBM Cloud SDK Common README.
+
+### Troubleshooting
+1. Authentication errors occur during service instantiation. For example, the code `service =
+ CloudantV1.new_instance(service_name="EXAMPLE")` will fail with `ValueError: At least one of
+ iam_profile_name or iam_profile_id must be specified.` if required environment variables
+ prefixed with `EXAMPLE` are not set.
+1. Server errors occur when running a request against the service. We suggest to
+ check server errors with
+ [`getServerInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getserverinformation)
+ which is the new alternative of `metadata()`.
+
+## Request mapping
+Here's a list of the top 5 most frequently used `python-cloudant` operations and the `cloudant-python-sdk` equivalent API operation documentation link:
+
+| `python-cloudant` operation | `cloudant-python-sdk` API operation documentation link |
+|---------------------------------------|---------------------------------|
+|`Document('db_name', 'docid').fetch()` |[`getDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#getdocument)|
+|`db.get_view_result()` |[`postView`](https://cloud.ibm.com/apidocs/cloudant?code=python#postview)|
+|`db.get_query_result()` |[`postFind`](https://cloud.ibm.com/apidocs/cloudant?code=python#postfind)|
+| `doc.exists()` |[`headDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#headdocument)|
+|`Document('db_name', 'docid').save()` |[`putDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#putdocument)|
+
+
+[A table](#reference-table) with the whole list of operations is provided at the end of this guide.
+
+The `cloudant-python-sdk` library is generated from a more complete API spec and provides a significant number of operations that do not exist in `python-cloudant`. See [the IBM Cloud API Documentation](https://cloud.ibm.com/apidocs/cloudant) to review request parameter and body options, code examples, and additional details for every endpoint.
+
+## Known Issues
+There's an [outline of known issues](https://github.com/IBM/cloudant-python-sdk/blob/master/KNOWN_ISSUES.md) in the `cloudant-python-sdk` repository.
+
+## Reference table
+The table below contains a list of `python-cloudant` functions and the `cloudant-python-sdk` equivalent API operation documentation link. The `cloudant-python-sdk` operation documentation link will contain the new function in a code sample e.g. `getServerInformation` link will contain a code example with `get_server_information()`.
+
+**Note:** There are many API operations included in the new `cloudant-python-sdk` that are not available in the `python-cloudant` library. The [API documentation](https://cloud.ibm.com/apidocs/cloudant?code=python) contains the full list of operations.
+
+| `python-cloudant` function | `cloudant-python-sdk` API operation documentation link |
+|-----------------|---------------------|
+|`metadata()`|[`getServerInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getserverinformation)|
+|`all_dbs()`|[`getAllDbs`](https://cloud.ibm.com/apidocs/cloudant?code=python#getalldbs)|
+|`db_updates()/infinite_db_updates()`|[`getDbUpdates`](https://cloud.ibm.com/apidocs/cloudant?code=python#getdbupdates)|
+|`Replicator.stop_replication()`|[`deleteReplicationDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#deletereplicationdocument)|
+|`Replicator.replication_state()`|[`getReplicationDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#getreplicationdocument)|
+|`Replicator.create_replication()`|[`putReplicationDocument`](https://cloud.ibm.com/apidocs/cloudant?code=#putreplicationdocument)|
+|`Scheduler.get_doc()`|[`getSchedulerDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#getschedulerdocument)|
+|`Scheduler.list_docs()`|[`getSchedulerDocs`](https://cloud.ibm.com/apidocs/cloudant?code=python#getschedulerdocs)|
+|`Scheduler.list_jobs()`|[`getSchedulerJobs`](https://cloud.ibm.com/apidocs/cloudant?code=python#getschedulerjobs)|
+|`session()`|[`getSessionInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getsessioninformation)|
+|`uuids()`|[`getUuids`](https://cloud.ibm.com/apidocs/cloudant?code=python#getuuids)|
+|`db.delete()`|[`deleteDatabase`](https://cloud.ibm.com/apidocs/cloudant?code=python#deletedatabase)|
+|`db.metadata()`|[`getDatabaseInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getdatabaseinformation)|
+|`db.create_document()`|[`postDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#postdocument)|
+|`db.create()`|[`putDatabase`](https://cloud.ibm.com/apidocs/cloudant?code=python#putdatabase)|
+|`db.all_docs()/db.keys()`|[`postAllDocs`](https://cloud.ibm.com/apidocs/cloudant?code=python#postalldocs)|
+|`db.bulk_docs()`|[`postBulkDocs`](https://cloud.ibm.com/apidocs/cloudant?code=python#postbulkdocs)|
+|`db.changes()/db.infinite_changes()`|[`postChanges`](https://cloud.ibm.com/apidocs/cloudant?code=python#postchanges-databases)|
+|`DesignDocument(db, '_design/doc').delete()`|[`deleteDesignDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#deletedesigndocument)|
+|`db.get_design_document()/DesignDocument(db, '_design/doc').fetch()`|[`getDesignDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#getdesigndocument)|
+|`DesignDocument(db, '_design/doc').save()`|[`putDesignDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#putdesigndocument)|
+|`DesignDocument(db, '_design/doc').info()`|[`getDesignDocumentInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getdesigndocumentinformation)|
+|`db.get_search_result()`|[`postSearch`](https://cloud.ibm.com/apidocs/cloudant?code=python#postsearch)|
+|`db.get_view_result()`|[`postView`](https://cloud.ibm.com/apidocs/cloudant?code=python#postview)|
+|`db.list_design_documents()`|[`postDesignDocs`](https://cloud.ibm.com/apidocs/cloudant?code=python#postdesigndocs)|
+|`db.get_query_result()`|[`postFind`](https://cloud.ibm.com/apidocs/cloudant?code=python#postfind)|
+|`db.get_query_indexes()`|[`getIndexesInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getindexesinformation)|
+|`db.create_query_index()`|[`postIndex`](https://cloud.ibm.com/apidocs/cloudant?code=python#postindex)|
+|`db.delete_query_index()`|[`deleteIndex`](https://cloud.ibm.com/apidocs/cloudant?code=python#deleteindex)|
+|`Document(db, '_local/docid').fetch()`|[`getLocalDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#getlocaldocument)|
+|`Document(db, '_local/docid').save()`|[`putLocalDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#putlocaldocument)|
+|`Document(db, '_local/docid').delete()`|[`deleteLocalDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#deletelocaldocument)|
+|`db.missing_revisions()/db.revisions_diff()`|[`postRevsDiff`](https://cloud.ibm.com/apidocs/cloudant?code=python#postrevsdiff)|
+|`db.partition_metadata()`|[`getPartitionInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getpartitioninformation)|
+|`db.partitioned_all_docs()`|[`postPartitionAllDocs`](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionalldocs)|
+|`db.get_partitioned_search_result()`|[`postPartitionSearch`](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionsearch)|
+|`db.get_partitioned_view_result()`|[`postPartitionView`](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionview)|
+|`db.get_partitioned_query_result()`|[`postPartitionFind`](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionfind-partitioned-databases)|
+|`db.get_security_document()/db.security_document()`|[`getSecurity`](https://cloud.ibm.com/apidocs/cloudant?code=python#getsecurity)|
+|`db.share_database()`|[`putSecurity`](https://cloud.ibm.com/apidocs/cloudant?code=python#putsecurity)|
+|`db.shards()`|[`getShardsInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getshardsinformation)|
+|`Document(db, 'docid').delete()`|[`deleteDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#deletedocument)|
+|`Document(db, 'docid').fetch()`|[`getDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#getdocument)|
+|`Document(db, 'docid').exists()`|[`headDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#headdocument)|
+|`Document(db, 'docid').save()`|[`putDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#putdocument)|
+|`Document(db, 'docid').delete_attachment()`|[`deleteAttachment`](https://cloud.ibm.com/apidocs/cloudant?code=python#deleteattachment)|
+|`Document(db, 'docid').get_attachment()`|[`getAttachment`](https://cloud.ibm.com/apidocs/cloudant?code=python#getattachment)|
+|`Document(db, 'docid').put_attachment()`|[`putAttachment`](https://cloud.ibm.com/apidocs/cloudant?code=python#putattachment)|
+|`generate_api_key()`|[`postApiKeys`](https://cloud.ibm.com/apidocs/cloudant?code=python#postapikeys)|
+|`SecurityDocument().save()`|[`putCloudantSecurityConfiguration`](https://cloud.ibm.com/apidocs/cloudant?code=python#putcloudantsecurity)|
+|`cors_configuration()/cors_origin()`|[`getCorsInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getcorsinformation)|
+|`update_cors_configuration()`|[`putCorsConfiguration`](https://cloud.ibm.com/apidocs/cloudant?code=python#putcorsconfiguration)|
diff --git a/README.md b/README.md
index 0cc5edcf..467c9dd3 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,21 @@
+# :warning: NO LONGER MAINTAINED :warning:
+
+**This library is end-of-life and no longer supported.**
+
+This repository will not be updated. The repository will be kept available in read-only mode.
+
+Please see the [Migration Guide](./MIGRATION.md) for advice
+about migrating to our replacement library
+[cloudant-python-sdk](https://github.com/IBM/cloudant-python-sdk).
+
+For FAQs and additional information please refer to the
+[Cloudant blog](https://blog.cloudant.com/2021/06/30/Cloudant-SDK-Transition.html).
+
# Cloudant Python Client
[](https://travis-ci.org/cloudant/python-cloudant)
[](http://python-cloudant.readthedocs.io)
-[](http://python-cloudant.readthedocs.io/en/latest/compatibility.html)
+[](http://python-cloudant.readthedocs.io/en/latest/compatibility.html)
[](https://pypi.python.org/pypi/cloudant)
This is the official Cloudant library for Python.
@@ -17,6 +30,7 @@ This is the official Cloudant library for Python.
* [Using in Other Projects](#using-in-other-projects)
* [License](#license)
* [Issues](#issues)
+* [Migrating to `cloudant-python-sdk` library](#migrating-to-cloudant-python-sdk-library)
## Installation and Usage
@@ -87,3 +101,7 @@ to see if the problem has already been reported. Note that the default search
includes only open issues, but it may already have been closed.
* Cloudant customers should contact Cloudant support for urgent issues.
* When opening a new issue [here in github](../../issues) please complete the template fully.
+
+## Migrating to `cloudant-python-sdk` library
+We have a newly supported Cloudant Python SDK named [cloudant-python-sdk](https://github.com/IBM/cloudant-python-sdk).
+For advice on migrating from this module see [MIGRATION.md](MIGRATION.md).
diff --git a/VERSION b/VERSION
index ca3389d9..3a7d90b0 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.13.1-SNAPSHOT
+2.15.1-SNAPSHOT
diff --git a/docs/compatibility.rst b/docs/compatibility.rst
index 1bb98cce..b756a65e 100644
--- a/docs/compatibility.rst
+++ b/docs/compatibility.rst
@@ -11,5 +11,4 @@ Note that some features are Cloudant specific.
This library has been tested with the following versions of Python
-* `Python™ 2.7 `_
* `Python™ 3.5 `_
diff --git a/docs/conf.py b/docs/conf.py
index ada059aa..c8d4ad26 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -60,9 +60,9 @@
# built documents.
#
# The short X.Y version.
-version = '2.13.1-SNAPSHOT'
+version = '2.15.1-SNAPSHOT'
# The full version, including alpha/beta/rc tags.
-release = '2.13.1-SNAPSHOT'
+release = '2.15.1-SNAPSHOT'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/docs/getting_started.rst b/docs/getting_started.rst
index c92387ca..37c5b92c 100644
--- a/docs/getting_started.rst
+++ b/docs/getting_started.rst
@@ -111,6 +111,13 @@ You can easily connect to your Cloudant account using an IAM API key:
# Authenticate using an IAM API key
client = Cloudant.iam(ACCOUNT_NAME, API_KEY, connect=True)
+If you need to authenticate to a server outside of the `cloudant.com` domain, you can use the `url` parameter:
+
+.. code-block:: python
+
+ # Authenticate using an IAM API key to an account outside of the cloudant.com domain
+ client = Cloudant.iam(None, API_KEY, url='https://private.endpoint.example', connect=True)
+
****************
Resource sharing
diff --git a/docs/index.rst b/docs/index.rst
index 0f642eed..a26ea611 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,14 +1 @@
-python-cloudant documentation
-=============================
-
-This is the official Cloudant client library for Python.
-
-.. toctree::
- :maxdepth: 4
-
- compatibility
- getting_started
- cloudant
-
-* :ref:`genindex`
-
+This library is end-of-life and no longer supported.
diff --git a/setup.py b/setup.py
index e02192cd..a8ac4ef8 100644
--- a/setup.py
+++ b/setup.py
@@ -19,6 +19,7 @@
"""
+from io import open
from os import path
from setuptools import setup, find_packages
@@ -54,8 +55,6 @@
'Topic :: Software Development :: Libraries',
'Development Status :: 5 - Production/Stable',
'Programming Language :: Python',
- 'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5'
]
diff --git a/src/cloudant/__init__.py b/src/cloudant/__init__.py
index 0b162070..8e4d7e33 100644
--- a/src/cloudant/__init__.py
+++ b/src/cloudant/__init__.py
@@ -15,14 +15,18 @@
"""
Cloudant / CouchDB Python client library API package
"""
-__version__ = '2.13.1-SNAPSHOT'
+__version__ = '2.15.1-SNAPSHOT'
# pylint: disable=wrong-import-position
import contextlib
+import warnings
# pylint: disable=wrong-import-position
from .client import Cloudant, CouchDB
from ._common_util import CloudFoundryService
+warnings.warn('The module cloudant is now end-of-life. The replacement is ibmcloudant.',
+ DeprecationWarning)
+
@contextlib.contextmanager
def cloudant(user, passwd, **kwargs):
"""
diff --git a/src/cloudant/_common_util.py b/src/cloudant/_common_util.py
index 6e765c1d..433e09a6 100644
--- a/src/cloudant/_common_util.py
+++ b/src/cloudant/_common_util.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python
-# Copyright (C) 2015, 2019 IBM Corp. All rights reserved.
+# Copyright © 2015, 2021 IBM Corp. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -31,6 +31,8 @@
# Library Constants
+DESIGN_PREFIX = '_design/'
+LOCAL_PREFIX = '_local/'
USER_AGENT = '/'.join([
'python-cloudant',
sys.modules['cloudant'].__version__,
@@ -56,20 +58,20 @@
RESULT_ARG_TYPES = {
'descending': (bool,),
- 'endkey': (int, LONGTYPE, STRTYPE, Sequence,),
+ 'endkey': (int, LONGTYPE, STRTYPE, Sequence, bool,),
'endkey_docid': (STRTYPE,),
'group': (bool,),
'group_level': (int, LONGTYPE, NONETYPE,),
'include_docs': (bool,),
'inclusive_end': (bool,),
- 'key': (int, LONGTYPE, STRTYPE, Sequence,),
+ 'key': (int, LONGTYPE, STRTYPE, Sequence, bool,),
'keys': (list,),
'limit': (int, LONGTYPE, NONETYPE,),
'reduce': (bool,),
'skip': (int, LONGTYPE, NONETYPE,),
'stable': (bool,),
'stale': (STRTYPE,),
- 'startkey': (int, LONGTYPE, STRTYPE, Sequence,),
+ 'startkey': (int, LONGTYPE, STRTYPE, Sequence, bool,),
'startkey_docid': (STRTYPE,),
'update': (STRTYPE,),
}
@@ -160,7 +162,7 @@ def feed_arg_types(feed_type):
return _COUCH_DB_UPDATES_ARG_TYPES
return _CHANGES_ARG_TYPES
-def python_to_couch(options):
+def python_to_couch(options, encoder=None):
"""
Translates query options from python style options into CouchDB/Cloudant
query options. For example ``{'include_docs': True}`` will
@@ -171,13 +173,14 @@ def python_to_couch(options):
:func:`~cloudant.view.View.__call__` callable, both used to retrieve data.
:param dict options: Python style parameters to be translated.
+ :param encoder: Custom encoder, defaults to None
:returns: Dictionary of translated CouchDB/Cloudant query parameters
"""
translation = dict()
for key, val in iteritems_(options):
py_to_couch_validate(key, val)
- translation.update(_py_to_couch_translate(key, val))
+ translation.update(_py_to_couch_translate(key, val, encoder))
return translation
def py_to_couch_validate(key, val):
@@ -190,18 +193,19 @@ def py_to_couch_validate(key, val):
# Validate argument values and ensure that a boolean is not passed in
# if an integer is expected
if (not isinstance(val, RESULT_ARG_TYPES[key]) or
- (type(val) is bool and int in RESULT_ARG_TYPES[key])):
+ (type(val) is bool and bool not in RESULT_ARG_TYPES[key] and
+ int in RESULT_ARG_TYPES[key])):
raise CloudantArgumentError(117, key, RESULT_ARG_TYPES[key])
if key == 'keys':
for key_list_val in val:
if (not isinstance(key_list_val, RESULT_ARG_TYPES['key']) or
- type(key_list_val) is bool):
+ isinstance(key_list_val, bool)):
raise CloudantArgumentError(134, RESULT_ARG_TYPES['key'])
if key == 'stale':
if val not in ('ok', 'update_after'):
raise CloudantArgumentError(135, val)
-def _py_to_couch_translate(key, val):
+def _py_to_couch_translate(key, val, encoder=None):
"""
Performs the conversion of the Python parameter value to its CouchDB
equivalent.
@@ -209,6 +213,8 @@ def _py_to_couch_translate(key, val):
try:
if key in ['keys', 'endkey_docid', 'startkey_docid', 'stale', 'update']:
return {key: val}
+ if key in ['endkey', 'key', 'startkey']:
+ return {key: json.dumps(val, cls=encoder)}
if val is None:
return {key: None}
arg_converter = TYPE_CONVERTERS.get(type(val))
@@ -249,7 +255,7 @@ def get_docs(r_session, url, encoder=None, headers=None, **params):
keys = None
if keys_list is not None:
keys = json.dumps({'keys': keys_list}, cls=encoder)
- f_params = python_to_couch(params)
+ f_params = python_to_couch(params, encoder)
resp = None
if keys is not None:
# If we're using POST we are sending JSON so add the header
@@ -297,6 +303,30 @@ def response_to_json_dict(response, **kwargs):
response.encoding = 'utf-8'
return json.loads(response.text, **kwargs)
+def assert_document_type_id(docid):
+ """
+ Validate the document ID. Raises an error if the ID is an `_` prefixed name
+ that isn't either `_design` or `_local`.
+ :return:
+ """
+ invalid = False
+ if docid.startswith('_'):
+ if docid.startswith(DESIGN_PREFIX) and DESIGN_PREFIX != docid:
+ invalid = False
+ elif docid.startswith(LOCAL_PREFIX) and LOCAL_PREFIX != docid:
+ invalid = False
+ else:
+ invalid = True
+ if invalid:
+ raise CloudantArgumentError(137, docid)
+
+def assert_attachment_name(attname):
+ """
+ Validate the document attachment's name. Raises an error if `_` prefixed name exists.
+ :return:
+ """
+ if attname.startswith('_'):
+ raise CloudantArgumentError(138, attname)
# Classes
diff --git a/src/cloudant/_messages.py b/src/cloudant/_messages.py
index 07a23d62..b428998c 100644
--- a/src/cloudant/_messages.py
+++ b/src/cloudant/_messages.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python
-# Copyright (c) 2016 IBM. All rights reserved.
+# Copyright © 2016, 2021 IBM Corp. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -65,7 +65,9 @@
# Common_util
134: 'Key list element not of expected type: {0}',
135: 'Invalid value for stale option {0} must be ok or update_after.',
- 136: 'Error converting argument {0}: {1}'
+ 136: 'Error converting argument {0}: {1}',
+ 137: 'Invalid document ID: {0}',
+ 138: 'Invalid attachment name: {0}'
}
CLIENT = {
diff --git a/src/cloudant/client.py b/src/cloudant/client.py
index f09d3a69..47c8555c 100755
--- a/src/cloudant/client.py
+++ b/src/cloudant/client.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python
-# Copyright (c) 2015, 2019 IBM Corp. All rights reserved.
+# Copyright (c) 2015, 2021 IBM Corp. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -853,7 +853,7 @@ def bluemix(cls, vcap_services, instance_name=None, service_name=None, **kwargs)
raise CloudantClientException(103)
if hasattr(service, 'iam_api_key'):
- return Cloudant.iam(service.username,
+ return Cloudant.iam(None,
service.iam_api_key,
url=service.url,
**kwargs)
@@ -867,7 +867,7 @@ def iam(cls, account_name, api_key, **kwargs):
"""
Create a Cloudant client that uses IAM authentication.
- :param account_name: Cloudant account name.
+ :param account_name: Cloudant account name; or use None and a url kwarg.
:param api_key: IAM authentication API key.
"""
return cls(None,
diff --git a/src/cloudant/database.py b/src/cloudant/database.py
index 3586c93f..4ad93f90 100644
--- a/src/cloudant/database.py
+++ b/src/cloudant/database.py
@@ -706,6 +706,24 @@ def __getitem__(self, key):
raise KeyError(key)
return doc
+ def get(self, key, remote=False):
+ """
+ Overrides dict's get method. This gets an item from the database or cache
+ like __getitem__, but instead of throwing an exception if the item is not
+ found, it simply returns None.
+
+ :param bool remote: Dictates whether a remote request is made to
+ retrieve the doc, if it is not present in the local cache.
+ Defaults to False.
+ """
+ if remote:
+ try:
+ return self.__getitem__(key)
+ except KeyError:
+ return None
+ else:
+ return super(CouchDatabase, self).get(key)
+
def __contains__(self, key):
"""
Overrides dictionary __contains__ behavior to check if a document
@@ -1242,8 +1260,7 @@ def get_query_result(self, selector, fields=None, raw_result=False,
wrapped in a QueryResult or if the response JSON is returned.
Defaults to False.
:param str bookmark: A string that enables you to specify which page of
- results you require. Only valid for queries using indexes of type
- *text*.
+ results you require.
:param int limit: Maximum number of results returned. Only valid if
used with ``raw_result=True``.
:param int page_size: Sets the page size for result iteration. Default
diff --git a/src/cloudant/design_document.py b/src/cloudant/design_document.py
index d3ea387a..df9fb44d 100644
--- a/src/cloudant/design_document.py
+++ b/src/cloudant/design_document.py
@@ -16,7 +16,8 @@
API module/class for interacting with a design document in a database.
"""
from ._2to3 import iteritems_, url_quote_plus, STRTYPE
-from ._common_util import QUERY_LANGUAGE, codify, response_to_json_dict
+from ._common_util import QUERY_LANGUAGE, codify, response_to_json_dict, \
+ assert_document_type_id, DESIGN_PREFIX
from .document import Document
from .view import View, QueryIndexView
from .error import CloudantArgumentError, CloudantDesignDocumentException
@@ -44,8 +45,10 @@ class DesignDocument(Document):
databases.
"""
def __init__(self, database, document_id=None, partitioned=False):
- if document_id and not document_id.startswith('_design/'):
- document_id = '_design/{0}'.format(document_id)
+ if document_id:
+ assert_document_type_id(document_id)
+ if document_id and not document_id.startswith(DESIGN_PREFIX):
+ document_id = '{0}{1}'.format(DESIGN_PREFIX, document_id)
super(DesignDocument, self).__init__(database, document_id)
if partitioned:
diff --git a/src/cloudant/document.py b/src/cloudant/document.py
index 38082054..96267bc9 100644
--- a/src/cloudant/document.py
+++ b/src/cloudant/document.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python
-# Copyright (C) 2015, 2018 IBM Corp. All rights reserved.
+# Copyright © 2015, 2021 IBM Corp. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
from requests.exceptions import HTTPError
from ._2to3 import url_quote, url_quote_plus
-from ._common_util import response_to_json_dict
+from ._common_util import response_to_json_dict, assert_document_type_id, assert_attachment_name
from .error import CloudantDocumentException
@@ -96,6 +96,15 @@ def document_url(self):
url_quote(self['_id'][8:], safe='')
))
+ # handle _local document url
+ if self['_id'].startswith('_local/'):
+ return '/'.join((
+ self._database_host,
+ url_quote_plus(self._database_name),
+ '_local',
+ url_quote(self['_id'][7:], safe='')
+ ))
+
# handle document url
return '/'.join((
self._database_host,
@@ -113,6 +122,8 @@ def exists(self):
if '_id' not in self or self['_id'] is None:
return False
+ assert_document_type_id(self['_id'])
+
resp = self.r_session.head(self.document_url)
if resp.status_code not in [200, 404]:
resp.raise_for_status()
@@ -161,6 +172,8 @@ def fetch(self):
"""
if self.document_url is None:
raise CloudantDocumentException(101)
+ if '_id' in self:
+ assert_document_type_id(self['_id'])
resp = self.r_session.get(self.document_url)
resp.raise_for_status()
self.clear()
@@ -309,6 +322,8 @@ def delete(self):
if not self.get("_rev"):
raise CloudantDocumentException(103)
+ assert_document_type_id(self['_id'])
+
del_resp = self.r_session.delete(
self.document_url,
params={"rev": self["_rev"]},
@@ -375,7 +390,8 @@ def get_attachment(
"""
# need latest rev
self.fetch()
- attachment_url = '/'.join((self.document_url, attachment))
+ assert_attachment_name(attachment)
+ attachment_url = '/'.join((self.document_url, url_quote(attachment, safe='')))
if headers is None:
headers = {'If-Match': self['_rev']}
else:
@@ -418,6 +434,7 @@ def delete_attachment(self, attachment, headers=None):
"""
# need latest rev
self.fetch()
+ assert_attachment_name(attachment)
attachment_url = '/'.join((self.document_url, attachment))
if headers is None:
headers = {'If-Match': self['_rev']}
@@ -459,6 +476,7 @@ def put_attachment(self, attachment, content_type, data, headers=None):
"""
# need latest rev
self.fetch()
+ assert_attachment_name(attachment)
attachment_url = '/'.join((self.document_url, attachment))
if headers is None:
headers = {
diff --git a/src/cloudant/query.py b/src/cloudant/query.py
index d6a0aebc..2362f80d 100644
--- a/src/cloudant/query.py
+++ b/src/cloudant/query.py
@@ -69,8 +69,7 @@ class Query(dict):
:param CloudantDatabase database: A Cloudant database instance used by the
Query.
:param str bookmark: A string that enables you to specify which page of
- results you require. Only valid for queries using indexes of type
- *text*.
+ results you require.
:param list fields: A list of fields to be returned by the query.
:param int limit: Maximum number of results returned.
:param int r: Read quorum needed for the result. Each document is read from
@@ -141,8 +140,7 @@ def __call__(self, **kwargs):
and set ``raw_result=True`` instead.
:param str bookmark: A string that enables you to specify which page of
- results you require. Only valid for queries using indexes of type
- *text*.
+ results you require.
:param list fields: A list of fields to be returned by the query.
:param int limit: Maximum number of results returned.
:param int r: Read quorum needed for the result. Each document is read
@@ -204,8 +202,7 @@ def custom_result(self, **options):
data = rslt[100:200]
:param str bookmark: A string that enables you to specify which page of
- results you require. Only valid for queries using indexes of type
- *text*.
+ results you require.
:param list fields: A list of fields to be returned by the query.
:param int page_size: Sets the page size for result iteration. Default
is 100.
diff --git a/src/cloudant/replicator.py b/src/cloudant/replicator.py
index e3ec3550..f1b44643 100644
--- a/src/cloudant/replicator.py
+++ b/src/cloudant/replicator.py
@@ -193,7 +193,12 @@ def update_state():
repl_doc, state = update_state()
if repl_doc:
yield repl_doc
- if state is not None and state in ['error', 'completed']:
+ # This is a little awkward, since 2.1 the terminal states are
+ # "failed" and "completed", so those should be the exit states, but
+ # for backwards compatibility with older versions "error" is also
+ # needed. The code has always exited for "error" state even long
+ # after 2.1 was available so that behaviour is retained.
+ if state is not None and state in ['error', 'failed', 'completed']:
return
# Now listen on changes feed for the state
@@ -202,7 +207,8 @@ def update_state():
repl_doc, state = update_state()
if repl_doc is not None:
yield repl_doc
- if state is not None and state in ['error', 'completed']:
+ # See note about these states
+ if state is not None and state in ['error', 'failed', 'completed']:
return
def stop_replication(self, repl_id):
diff --git a/src/cloudant/result.py b/src/cloudant/result.py
index d3dd9a09..319d7268 100644
--- a/src/cloudant/result.py
+++ b/src/cloudant/result.py
@@ -397,9 +397,10 @@ def _iterator(self, response):
# if we are in a view, keys could be duplicate so we
# need to start from the right docid
- if last['id']:
+ last_doc_id = last.get('id')
+ if last_doc_id is not None:
response = self._call(startkey=last['key'],
- startkey_docid=last['id'])
+ startkey_docid=last_doc_id)
# reduce result keys are unique by definition
else:
response = self._call(startkey=last['key'])
@@ -488,8 +489,7 @@ class QueryResult(Result):
:param query: A reference to the query callable that returns
the JSON content result to be wrapped.
:param str bookmark: A string that enables you to specify which page of
- results you require. Only valid for queries using indexes of type
- *text*.
+ results you require.
:param list fields: A list of fields to be returned by the query.
:param int page_size: Sets the page size for result iteration. Default
is 100.
diff --git a/tests/unit/changes_tests.py b/tests/unit/changes_tests.py
index d39ab159..3df0cef3 100644
--- a/tests/unit/changes_tests.py
+++ b/tests/unit/changes_tests.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python
-# Copyright (C) 2016, 2018 IBM Corp. All rights reserved.
+# Copyright (C) 2016, 2021 IBM Corp. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -227,8 +227,7 @@ def test_get_feed_descending(self):
Test getting content back for a descending feed. When testing, the sequence
identifier is in the form of -. Often times
the number prefix sorts as expected when using descending but sometimes the
- number prefix is repeated. In these cases the check is to see if the following
- random character sequence suffix is longer than its predecessor.
+ number prefix is repeated.
"""
self.populate_db_with_documents(50)
feed = Feed(self.db, descending=True)
@@ -245,7 +244,6 @@ def test_get_feed_descending(self):
self.assertTrue(current < last)
except AssertionError:
self.assertEqual(current, last)
- self.assertTrue(len(change['seq']) > len(last_seq))
seq_list.append(change['seq'])
last_seq = change['seq']
self.assertEqual(len(seq_list), 50)
@@ -476,9 +474,9 @@ def test_get_feed_with_custom_filter_query_params(self):
include_docs=False
)
params = feed._translate(feed._options)
- self.assertEquals(params['filter'], 'mailbox/new_mail')
- self.assertEquals(params['foo'], 'bar')
- self.assertEquals(params['include_docs'], 'false')
+ self.assertEqual(params['filter'], 'mailbox/new_mail')
+ self.assertEqual(params['foo'], 'bar')
+ self.assertEqual(params['include_docs'], 'false')
def test_invalid_argument_type(self):
"""
@@ -522,7 +520,7 @@ def test_invalid_style_value(self):
with self.assertRaises(CloudantArgumentError) as cm:
invalid_feed = [x for x in feed]
self.assertEqual(
- str(cm.exception),
+ str(cm.exception),
'Invalid value (foo) for style option. Must be main_only, or all_docs.')
if __name__ == '__main__':
diff --git a/tests/unit/client_tests.py b/tests/unit/client_tests.py
index 2726de33..35139168 100644
--- a/tests/unit/client_tests.py
+++ b/tests/unit/client_tests.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python
-# Copyright (C) 2015, 2020 IBM Corp. All rights reserved.
+# Copyright (C) 2015, 2021 IBM Corp. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@
import sys
import unittest
from time import sleep
+from urllib.parse import urlparse
import mock
import requests
@@ -213,7 +214,7 @@ def test_auto_renew_enabled_with_auto_connect(self):
@skip_if_not_cookie_auth
def test_session(self):
"""
- Test getting session information.
+ Test getting session information.
Session info is None if CouchDB Admin Party mode was selected.
"""
try:
@@ -265,7 +266,7 @@ def test_session_basic(self, m_req):
timeout=None
)
- self.assertEquals(all_dbs, ['animaldb'])
+ self.assertEqual(all_dbs, ['animaldb'])
@mock.patch('cloudant._client_session.Session.request')
def test_session_basic_with_no_credentials(self, m_req):
@@ -332,7 +333,7 @@ def test_change_credentials_basic(self, m_req):
auth=('baz', 'qux'), # uses HTTP Basic Auth
timeout=None
)
- self.assertEquals(all_dbs, ['animaldb'])
+ self.assertEqual(all_dbs, ['animaldb'])
@skip_if_not_cookie_auth
def test_basic_auth_str(self):
@@ -441,7 +442,7 @@ def test_create_with_server_error(self, m_req):
self.assertEqual(cm.exception.status_code, 500)
- self.assertEquals(m_req.call_count, 3)
+ self.assertEqual(m_req.call_count, 3)
m_req.assert_called_with(
'PUT',
'/'.join([self.url, dbname]),
@@ -563,7 +564,7 @@ def test_get_cached_db_object_via_get(self):
self.client.connect()
# Default returns None
self.assertIsNone(self.client.get('no_such_db'))
- # Creates the database remotely and adds it to the
+ # Creates the database remotely and adds it to the
# client database cache
db = self.client.create_database(dbname)
# Locally cached database object is returned
@@ -702,7 +703,7 @@ def test_cloudant_context_helper(self):
Test that the cloudant context helper works as expected.
"""
try:
- with cloudant(self.user, self.pwd, account=self.account) as c:
+ with cloudant(self.user, self.pwd, url=self.url) as c:
self.assertIsInstance(c, Cloudant)
self.assertIsInstance(c.r_session, requests.Session)
except Exception as err:
@@ -718,7 +719,7 @@ def test_cloudant_bluemix_context_helper_with_legacy_creds(self):
'credentials': {
'username': self.user,
'password': self.pwd,
- 'host': '{0}.cloudant.com'.format(self.account),
+ 'host': urlparse(self.url).hostname,
'port': 443,
'url': self.url
},
@@ -729,7 +730,7 @@ def test_cloudant_bluemix_context_helper_with_legacy_creds(self):
with cloudant_bluemix(vcap_services, instance_name=instance_name) as c:
self.assertIsInstance(c, Cloudant)
self.assertIsInstance(c.r_session, requests.Session)
- self.assertEquals(c.session()['userCtx']['name'], self.user)
+ self.assertEqual(c.session()['userCtx']['name'], self.user)
except Exception as err:
self.fail('Exception {0} was raised.'.format(str(err)))
@@ -744,7 +745,7 @@ def test_cloudant_bluemix_context_helper_with_iam(self):
'credentials': {
'apikey': self.iam_api_key,
'username': self.user,
- 'host': '{0}.cloudant.com'.format(self.account),
+ 'host': urlparse(self.url).hostname,
'port': 443,
'url': self.url
},
@@ -766,7 +767,7 @@ def test_cloudant_bluemix_context_helper_raise_error_for_missing_iam_and_creds(s
instance_name = 'Cloudant NoSQL DB-lv'
vcap_services = {'cloudantNoSQLDB': [{
'credentials': {
- 'host': '{0}.cloudant.com'.format(self.account),
+ 'host': urlparse(self.url).hostname,
'port': 443,
'url': self.url
},
@@ -795,7 +796,7 @@ def test_cloudant_bluemix_dedicated_context_helper(self):
'credentials': {
'username': self.user,
'password': self.pwd,
- 'host': '{0}.cloudant.com'.format(self.account),
+ 'host': urlparse(self.url).hostname,
'port': 443,
'url': self.url
},
@@ -808,7 +809,7 @@ def test_cloudant_bluemix_dedicated_context_helper(self):
service_name=service_name) as c:
self.assertIsInstance(c, Cloudant)
self.assertIsInstance(c.r_session, requests.Session)
- self.assertEquals(c.session()['userCtx']['name'], self.user)
+ self.assertEqual(c.session()['userCtx']['name'], self.user)
except Exception as err:
self.fail('Exception {0} was raised.'.format(str(err)))
@@ -818,10 +819,10 @@ def test_constructor_with_account(self):
"""
# Ensure that the client is new
del self.client
- self.client = Cloudant(self.user, self.pwd, account=self.account)
+ self.client = Cloudant('user', 'pass', account='foo')
self.assertEqual(
self.client.server_url,
- 'https://{0}.cloudant.com'.format(self.account)
+ 'https://foo.cloudant.com'
)
@skip_if_not_cookie_auth
@@ -835,7 +836,7 @@ def test_bluemix_constructor_with_legacy_creds(self):
'credentials': {
'username': self.user,
'password': self.pwd,
- 'host': '{0}.cloudant.com'.format(self.account),
+ 'host': urlparse(self.url).hostname,
'port': 443,
'url': self.url
},
@@ -849,7 +850,7 @@ def test_bluemix_constructor_with_legacy_creds(self):
c.connect()
self.assertIsInstance(c, Cloudant)
self.assertIsInstance(c.r_session, requests.Session)
- self.assertEquals(c.session()['userCtx']['name'], self.user)
+ self.assertEqual(c.session()['userCtx']['name'], self.user)
except Exception as err:
self.fail('Exception {0} was raised.'.format(str(err)))
@@ -870,7 +871,7 @@ def test_bluemix_constructor_with_iam(self):
'credentials': {
'apikey': self.iam_api_key,
'username': self.user,
- 'host': '{0}.cloudant.com'.format(self.account),
+ 'host': urlparse(self.url).hostname,
'port': 443
},
'name': instance_name
@@ -901,7 +902,7 @@ def test_bluemix_constructor_specify_instance_name(self):
'credentials': {
'username': self.user,
'password': self.pwd,
- 'host': '{0}.cloudant.com'.format(self.account),
+ 'host': urlparse(self.url).hostname,
'port': 443,
'url': self.url
},
@@ -915,7 +916,7 @@ def test_bluemix_constructor_specify_instance_name(self):
c.connect()
self.assertIsInstance(c, Cloudant)
self.assertIsInstance(c.r_session, requests.Session)
- self.assertEquals(c.session()['userCtx']['name'], self.user)
+ self.assertEqual(c.session()['userCtx']['name'], self.user)
except Exception as err:
self.fail('Exception {0} was raised.'.format(str(err)))
@@ -934,7 +935,7 @@ def test_bluemix_constructor_with_multiple_services(self):
{
'credentials': {
'apikey': '1234api',
- 'host': '{0}.cloudant.com'.format(self.account),
+ 'host': urlparse(self.url).hostname,
'port': 443,
'url': self.url
},
@@ -959,7 +960,7 @@ def test_bluemix_constructor_with_multiple_services(self):
c.connect()
self.assertIsInstance(c, Cloudant)
self.assertIsInstance(c.r_session, requests.Session)
- self.assertEquals(c.session()['userCtx']['name'], self.user)
+ self.assertEqual(c.session()['userCtx']['name'], self.user)
except Exception as err:
self.fail('Exception {0} was raised.'.format(str(err)))
@@ -973,10 +974,11 @@ def test_connect_headers(self):
"""
try:
self.client.connect()
- self.assertEqual(
- self.client.r_session.headers['X-Cloudant-User'],
- self.account
- )
+ if (self.account):
+ self.assertEqual(
+ self.client.r_session.headers['X-Cloudant-User'],
+ self.account
+ )
agent = self.client.r_session.headers.get('User-Agent')
ua_parts = agent.split('/')
self.assertEqual(len(ua_parts), 6)
@@ -1413,4 +1415,4 @@ def test_update_cors_configuration(self):
self.client.disconnect()
if __name__ == '__main__':
- unittest.main()
\ No newline at end of file
+ unittest.main()
diff --git a/tests/unit/database_partition_tests.py b/tests/unit/database_partition_tests.py
index 52b76e0d..8b3690dd 100644
--- a/tests/unit/database_partition_tests.py
+++ b/tests/unit/database_partition_tests.py
@@ -63,7 +63,7 @@ def test_create_non_partitioned_design_document(self):
def test_partitioned_all_docs(self):
for partition_key in self.populate_db_with_partitioned_documents(5, 25):
docs = self.db.partitioned_all_docs(partition_key)
- self.assertEquals(len(docs['rows']), 25)
+ self.assertEqual(len(docs['rows']), 25)
for doc in docs['rows']:
self.assertTrue(doc['id'].startswith(partition_key + ':'))
@@ -71,8 +71,8 @@ def test_partitioned_all_docs(self):
def test_partition_metadata(self):
for partition_key in self.populate_db_with_partitioned_documents(5, 25):
meta = self.db.partition_metadata(partition_key)
- self.assertEquals(meta['partition'], partition_key)
- self.assertEquals(meta['doc_count'], 25)
+ self.assertEqual(meta['partition'], partition_key)
+ self.assertEqual(meta['doc_count'], 25)
def test_partitioned_search(self):
ddoc = DesignDocument(self.db, 'partitioned_search', partitioned=True)
@@ -91,7 +91,7 @@ def test_partitioned_search(self):
print(result)
self.assertTrue(result['id'].startswith(partition_key + ':'))
i += 1
- self.assertEquals(i, 10)
+ self.assertEqual(i, 10)
def test_get_partitioned_index(self):
index_name = 'test_partitioned_index'
@@ -99,16 +99,16 @@ def test_get_partitioned_index(self):
self.db.create_query_index(index_name=index_name, fields=['foo'])
results = self.db.get_query_indexes()
- self.assertEquals(len(results), 2)
+ self.assertEqual(len(results), 2)
index_all_docs = results[0]
- self.assertEquals(index_all_docs.name, '_all_docs')
- self.assertEquals(type(index_all_docs), SpecialIndex)
+ self.assertEqual(index_all_docs.name, '_all_docs')
+ self.assertEqual(type(index_all_docs), SpecialIndex)
self.assertFalse(index_all_docs.partitioned)
index_partitioned = results[1]
- self.assertEquals(index_partitioned.name, index_name)
- self.assertEquals(type(index_partitioned), Index)
+ self.assertEqual(index_partitioned.name, index_name)
+ self.assertEqual(type(index_partitioned), Index)
self.assertTrue(index_partitioned.partitioned)
def test_partitioned_query(self):
@@ -122,7 +122,7 @@ def test_partitioned_query(self):
for result in results:
self.assertTrue(result['_id'].startswith(partition_key + ':'))
i += 1
- self.assertEquals(i, 10)
+ self.assertEqual(i, 10)
def test_partitioned_view(self):
ddoc = DesignDocument(self.db, 'partitioned_view', partitioned=True)
@@ -138,4 +138,4 @@ def test_partitioned_view(self):
self.assertTrue(
result['id'].startswith(partition_key + ':'))
i += 1
- self.assertEquals(i, 10)
+ self.assertEqual(i, 10)
diff --git a/tests/unit/database_tests.py b/tests/unit/database_tests.py
index f054dd62..ae9898c7 100644
--- a/tests/unit/database_tests.py
+++ b/tests/unit/database_tests.py
@@ -256,6 +256,8 @@ def test_create_document_with_id(self):
data = {'_id': 'julia06', 'name': 'julia', 'age': 6}
doc = self.db.create_document(data)
self.assertEqual(self.db['julia06'], doc)
+ self.assertEqual(self.db.get('julia06'), doc)
+ self.assertEqual(self.db.get('julia06', remote=True), doc)
self.assertEqual(doc['_id'], data['_id'])
self.assertTrue(doc['_rev'].startswith('1-'))
self.assertEqual(doc['name'], data['name'])
@@ -271,6 +273,42 @@ def test_create_document_with_id(self):
'Document with id julia06 already exists.'
)
+ def test_get_non_existing_document_from_remote(self):
+ """
+ Test dict's get on non existing document from remote.
+ """
+ doc = self.db.get('non-existing', remote=True)
+ self.assertIsNone(doc)
+
+ def test_get_non_existing_document_from_cache(self):
+ """
+ Test dict's get on non existing document from cache.
+ """
+ doc = self.db.get('non-existing')
+ self.assertIsNone(doc)
+
+ def test_get_document_from_cache(self):
+ """
+ Test dict's get on a document from cache.
+ """
+ doc = Document(self.db, document_id='julia06')
+ self.db['julia06'] = doc
+ self.assertEqual(self.db.get('julia06'), doc)
+ # doc is fetched from the local dict preferentially to remote even with remote=True
+ self.assertEqual(self.db.get('julia06', remote=True), doc)
+ self.assertEqual(self.db['julia06'], doc)
+
+ def test_get_document_from_remote(self):
+ """
+ Test dict's get on a document from remote.
+ """
+ data = {'_id': 'julia06','name': 'julia06', 'age': 6}
+ doc = self.db.create_document(data)
+ self.db.clear()
+ self.assertIsNone(self.db.get('julia06'))
+ self.assertEqual(self.db.get('julia06', remote=True), doc)
+ self.assertEqual(self.db['julia06'], doc)
+
def test_create_document_that_already_exists(self):
"""
Test creating a document that already exists
@@ -278,6 +316,8 @@ def test_create_document_that_already_exists(self):
data = {'_id': 'julia'}
doc = self.db.create_document(data)
self.assertEqual(self.db['julia'], doc)
+ self.assertEqual(self.db.get('julia'), doc)
+ self.assertEqual(self.db.get('julia', remote=True), doc)
self.assertTrue(doc['_rev'].startswith('1-'))
# attempt to recreate document
self.db.create_document(data, throw_on_exists=False)
@@ -289,6 +329,8 @@ def test_create_document_without_id(self):
data = {'name': 'julia', 'age': 6}
doc = self.db.create_document(data)
self.assertEqual(self.db[doc['_id']], doc)
+ self.assertEqual(self.db.get(doc['_id']), doc)
+ self.assertEqual(self.db.get(doc['_id'], remote=True), doc)
self.assertTrue(doc['_rev'].startswith('1-'))
self.assertEqual(doc['name'], data['name'])
self.assertEqual(doc['age'], data['age'])
@@ -302,6 +344,8 @@ def test_create_design_document(self):
data = {'_id': '_design/julia06', 'name': 'julia', 'age': 6}
doc = self.db.create_document(data)
self.assertEqual(self.db['_design/julia06'], doc)
+ self.assertEqual(self.db.get('_design/julia06'), doc)
+ self.assertEqual(self.db.get('_design/julia06', remote=True), doc)
self.assertEqual(doc['_id'], data['_id'])
self.assertTrue(doc['_rev'].startswith('1-'))
self.assertEqual(doc['name'], data['name'])
@@ -316,6 +360,8 @@ def test_create_empty_document(self):
"""
empty_doc = self.db.new_document()
self.assertEqual(self.db[empty_doc['_id']], empty_doc)
+ self.assertEqual(self.db.get(empty_doc['_id']), empty_doc)
+ self.assertEqual(self.db.get(empty_doc['_id'], remote=True), empty_doc)
self.assertTrue(all(x in ['_id', '_rev'] for x in empty_doc.keys()))
self.assertTrue(empty_doc['_rev'].startswith('1-'))
@@ -408,6 +454,34 @@ def test_retrieve_view_results(self):
self.assertIsInstance(rslt, Result)
self.assertEqual(rslt[:1], rslt['julia099'])
+ def test_retrieve_grouped_view_result_with_page_size(self):
+ """
+ Test retrieving Result wrapped output from a design document grouped view
+ that uses a custom page size
+
+ The view used here along with group=True will generate rows of
+ data where each key will be grouped into groups of 2. Such as:
+ {'key': 0, 'value': 2},
+ {'key': 1, 'value': 2},
+ ...
+ """
+ map_func = 'function(doc) {\n emit(Math.floor(doc.age / 2), 1); \n}'
+ data = {'_id': '_design/ddoc01','views': {'view01': {"map": map_func, "reduce": "_count"}}}
+ self.db.create_document(data)
+ self.populate_db_with_documents(5)
+
+ rslt = self.db.get_view_result(
+ '_design/ddoc01',
+ 'view01',
+ group=True,
+ page_size=1)
+ self.assertIsInstance(rslt, Result)
+ i = 0
+ for row in rslt:
+ self.assertIsNotNone(row)
+ self.assertEqual(row['key'], i)
+ i += 1
+
def test_retrieve_raw_view_results(self):
"""
Test retrieving raw output from a design document view
@@ -484,6 +558,15 @@ def test_all_docs_get_with_long_type(self):
data = self.db.all_docs(limit=1, skip=LONG_NUMBER)
self.assertEqual(len(data.get('rows')), 1)
+ def test_all_docs_get_uses_custom_encoder(self):
+ """
+ Test that all_docs uses the custom encoder.
+ """
+ self.set_up_client(auto_connect=True, encoder="AEncoder")
+ database = self.client[self.test_dbname]
+ with self.assertRaises(CloudantArgumentError):
+ database.all_docs(endkey=['foo', 10])
+
def test_custom_result_context_manager(self):
"""
Test using the database custom result context manager
@@ -802,7 +885,7 @@ def test_get_set_revision_limit(self, m_req):
# Get new revisions limit.
self.assertEqual(self.db.get_revision_limit(), 1234)
- self.assertEquals(m_req.call_count, 3)
+ self.assertEqual(m_req.call_count, 3)
@attr(db='couch')
def test_view_clean_up(self):
@@ -1001,19 +1084,19 @@ def test_create_json_index(self):
ddoc = self.db[index.design_document_id]
- self.assertEquals(ddoc['_id'], index.design_document_id)
+ self.assertEqual(ddoc['_id'], index.design_document_id)
self.assertTrue(ddoc['_rev'].startswith('1-'))
- self.assertEquals(ddoc['indexes'], {})
- self.assertEquals(ddoc['language'], 'query')
- self.assertEquals(ddoc['lists'], {})
- self.assertEquals(ddoc['shows'], {})
+ self.assertEqual(ddoc['indexes'], {})
+ self.assertEqual(ddoc['language'], 'query')
+ self.assertEqual(ddoc['lists'], {})
+ self.assertEqual(ddoc['shows'], {})
index = ddoc['views'][index.name]
- self.assertEquals(index['map']['fields']['age'], 'asc')
- self.assertEquals(index['map']['fields']['name'], 'asc')
- self.assertEquals(index['options']['def']['fields'], ['name', 'age'])
- self.assertEquals(index['reduce'], '_count')
+ self.assertEqual(index['map']['fields']['age'], 'asc')
+ self.assertEqual(index['map']['fields']['name'], 'asc')
+ self.assertEqual(index['options']['def']['fields'], ['name', 'age'])
+ self.assertEqual(index['reduce'], '_count')
@attr(couchapi=2)
def test_delete_json_index(self):
@@ -1286,22 +1369,22 @@ def test_create_text_index(self):
ddoc = self.db[index.design_document_id]
- self.assertEquals(ddoc['_id'], index.design_document_id)
+ self.assertEqual(ddoc['_id'], index.design_document_id)
self.assertTrue(ddoc['_rev'].startswith('1-'))
- self.assertEquals(ddoc['language'], 'query')
- self.assertEquals(ddoc['lists'], {})
- self.assertEquals(ddoc['shows'], {})
- self.assertEquals(ddoc['views'], {})
+ self.assertEqual(ddoc['language'], 'query')
+ self.assertEqual(ddoc['lists'], {})
+ self.assertEqual(ddoc['shows'], {})
+ self.assertEqual(ddoc['views'], {})
text_index = ddoc['indexes'][index.name]
- self.assertEquals(text_index['analyzer']['default'], 'keyword')
- self.assertEquals(text_index['analyzer']['fields']['$default'], 'standard')
- self.assertEquals(text_index['analyzer']['name'], 'perfield')
- self.assertEquals(text_index['index']['default_analyzer'], 'keyword')
- self.assertEquals(text_index['index']['default_field'], {})
- self.assertEquals(text_index['index']['fields'], [{'name': 'name', 'type': 'string'}, {'name': 'age', 'type': 'number'}])
- self.assertEquals(text_index['index']['selector'], {})
+ self.assertEqual(text_index['analyzer']['default'], 'keyword')
+ self.assertEqual(text_index['analyzer']['fields']['$default'], 'standard')
+ self.assertEqual(text_index['analyzer']['name'], 'perfield')
+ self.assertEqual(text_index['index']['default_analyzer'], 'keyword')
+ self.assertEqual(text_index['index']['default_field'], {})
+ self.assertEqual(text_index['index']['fields'], [{'name': 'name', 'type': 'string'}, {'name': 'age', 'type': 'number'}])
+ self.assertEqual(text_index['index']['selector'], {})
self.assertTrue(text_index['index']['index_array_lengths'])
def test_create_all_fields_text_index(self):
@@ -1313,22 +1396,22 @@ def test_create_all_fields_text_index(self):
ddoc = self.db[index.design_document_id]
- self.assertEquals(ddoc['_id'], index.design_document_id)
+ self.assertEqual(ddoc['_id'], index.design_document_id)
self.assertTrue(ddoc['_rev'].startswith('1-'))
- self.assertEquals(ddoc['language'], 'query')
- self.assertEquals(ddoc['lists'], {})
- self.assertEquals(ddoc['shows'], {})
- self.assertEquals(ddoc['views'], {})
+ self.assertEqual(ddoc['language'], 'query')
+ self.assertEqual(ddoc['lists'], {})
+ self.assertEqual(ddoc['shows'], {})
+ self.assertEqual(ddoc['views'], {})
index = ddoc['indexes'][index.name]
- self.assertEquals(index['analyzer']['default'], 'keyword')
- self.assertEquals(index['analyzer']['fields'], {'$default': 'standard'})
- self.assertEquals(index['analyzer']['name'], 'perfield')
- self.assertEquals(index['index']['default_analyzer'], 'keyword')
- self.assertEquals(index['index']['default_field'], {})
- self.assertEquals(index['index']['fields'], 'all_fields')
- self.assertEquals(index['index']['selector'], {})
+ self.assertEqual(index['analyzer']['default'], 'keyword')
+ self.assertEqual(index['analyzer']['fields'], {'$default': 'standard'})
+ self.assertEqual(index['analyzer']['name'], 'perfield')
+ self.assertEqual(index['index']['default_analyzer'], 'keyword')
+ self.assertEqual(index['index']['default_field'], {})
+ self.assertEqual(index['index']['fields'], 'all_fields')
+ self.assertEqual(index['index']['selector'], {})
self.assertTrue(index['index']['index_array_lengths'])
def test_create_multiple_indexes_one_ddoc(self):
@@ -1353,27 +1436,27 @@ def test_create_multiple_indexes_one_ddoc(self):
ddoc = self.db['_design/ddoc001']
- self.assertEquals(ddoc['_id'], index.design_document_id)
+ self.assertEqual(ddoc['_id'], index.design_document_id)
self.assertTrue(ddoc['_rev'].startswith('2-'))
- self.assertEquals(ddoc['language'], 'query')
- self.assertEquals(ddoc['lists'], {})
- self.assertEquals(ddoc['shows'], {})
+ self.assertEqual(ddoc['language'], 'query')
+ self.assertEqual(ddoc['lists'], {})
+ self.assertEqual(ddoc['shows'], {})
json_index = ddoc['views']['json-index-001']
- self.assertEquals(json_index['map']['fields']['age'], 'asc')
- self.assertEquals(json_index['map']['fields']['name'], 'asc')
- self.assertEquals(json_index['options']['def']['fields'], ['name', 'age'])
- self.assertEquals(json_index['reduce'], '_count')
+ self.assertEqual(json_index['map']['fields']['age'], 'asc')
+ self.assertEqual(json_index['map']['fields']['name'], 'asc')
+ self.assertEqual(json_index['options']['def']['fields'], ['name', 'age'])
+ self.assertEqual(json_index['reduce'], '_count')
text_index = ddoc['indexes']['text-index-001']
- self.assertEquals(text_index['analyzer']['default'], 'keyword')
- self.assertEquals(text_index['analyzer']['fields']['$default'], 'standard')
- self.assertEquals(text_index['analyzer']['name'], 'perfield')
- self.assertEquals(text_index['index']['default_analyzer'], 'keyword')
- self.assertEquals(text_index['index']['default_field'], {})
- self.assertEquals(text_index['index']['fields'], [{'name': 'name', 'type': 'string'}, {'name': 'age', 'type': 'number'}])
- self.assertEquals(text_index['index']['selector'], {})
+ self.assertEqual(text_index['analyzer']['default'], 'keyword')
+ self.assertEqual(text_index['analyzer']['fields']['$default'], 'standard')
+ self.assertEqual(text_index['analyzer']['name'], 'perfield')
+ self.assertEqual(text_index['index']['default_analyzer'], 'keyword')
+ self.assertEqual(text_index['index']['default_field'], {})
+ self.assertEqual(text_index['index']['fields'], [{'name': 'name', 'type': 'string'}, {'name': 'age', 'type': 'number'}])
+ self.assertEqual(text_index['index']['selector'], {})
self.assertTrue(text_index['index']['index_array_lengths'])
def test_create_query_index_failure(self):
@@ -1430,28 +1513,28 @@ def test_get_query_indexes_raw(self):
indexes = self.db.get_query_indexes(raw_result=True)
- self.assertEquals(indexes['total_rows'], 3)
+ self.assertEqual(indexes['total_rows'], 3)
all_docs_index = indexes['indexes'][0]
- self.assertEquals(all_docs_index['ddoc'], None)
- self.assertEquals(all_docs_index['def']['fields'], [{'_id': 'asc'}])
- self.assertEquals(all_docs_index['name'], '_all_docs')
- self.assertEquals(all_docs_index['type'], 'special')
+ self.assertEqual(all_docs_index['ddoc'], None)
+ self.assertEqual(all_docs_index['def']['fields'], [{'_id': 'asc'}])
+ self.assertEqual(all_docs_index['name'], '_all_docs')
+ self.assertEqual(all_docs_index['type'], 'special')
json_index = indexes['indexes'][1]
- self.assertEquals(json_index['ddoc'], '_design/ddoc001')
- self.assertEquals(json_index['def']['fields'], [{'name': 'asc'}, {'age': 'asc'}])
- self.assertEquals(json_index['name'], 'json-idx-001')
- self.assertEquals(json_index['type'], 'json')
+ self.assertEqual(json_index['ddoc'], '_design/ddoc001')
+ self.assertEqual(json_index['def']['fields'], [{'name': 'asc'}, {'age': 'asc'}])
+ self.assertEqual(json_index['name'], 'json-idx-001')
+ self.assertEqual(json_index['type'], 'json')
text_index = indexes['indexes'][2]
- self.assertEquals(text_index['ddoc'], '_design/ddoc001')
- self.assertEquals(text_index['def']['default_analyzer'], 'keyword')
- self.assertEquals(text_index['def']['default_field'], {})
- self.assertEquals(text_index['def']['fields'], [])
- self.assertEquals(text_index['def']['selector'], {})
- self.assertEquals(text_index['name'], 'text-idx-001')
- self.assertEquals(text_index['type'], 'text')
+ self.assertEqual(text_index['ddoc'], '_design/ddoc001')
+ self.assertEqual(text_index['def']['default_analyzer'], 'keyword')
+ self.assertEqual(text_index['def']['default_field'], {})
+ self.assertEqual(text_index['def']['fields'], [])
+ self.assertEqual(text_index['def']['selector'], {})
+ self.assertEqual(text_index['name'], 'text-idx-001')
+ self.assertEqual(text_index['type'], 'text')
self.assertTrue(text_index['def']['index_array_lengths'])
def test_get_query_indexes(self):
diff --git a/tests/unit/design_document_tests.py b/tests/unit/design_document_tests.py
index 52352bcc..86a2b5f1 100644
--- a/tests/unit/design_document_tests.py
+++ b/tests/unit/design_document_tests.py
@@ -843,9 +843,9 @@ def test_get_search_info(self):
# Validate the metadata
search_index_metadata = search_info['search_index']
self.assertIsNotNone(search_index_metadata)
- self.assertEquals(search_index_metadata['doc_del_count'], 0, 'There should be no deleted docs.')
+ self.assertEqual(search_index_metadata['doc_del_count'], 0, 'There should be no deleted docs.')
self.assertTrue(search_index_metadata['doc_count'] <= 100, 'There should be 100 or fewer docs.')
- self.assertEquals(search_index_metadata['committed_seq'], 0, 'The committed_seq should be 0.')
+ self.assertEqual(search_index_metadata['committed_seq'], 0, 'The committed_seq should be 0.')
self.assertTrue(search_index_metadata['pending_seq'] <= 101, 'The pending_seq should be 101 or fewer.')
self.assertTrue(search_index_metadata['disk_size'] >0, 'The disk_size should be greater than 0.')
@@ -1275,7 +1275,7 @@ def test_rewrite_rule(self):
doc = Document(self.db, 'rewrite_doc')
doc.save()
resp = self.client.r_session.get('/'.join([ddoc.document_url, '_rewrite']))
- self.assertEquals(
+ self.assertEqual(
response_to_json_dict(resp),
{
'_id': 'rewrite_doc',
diff --git a/tests/unit/document_tests.py b/tests/unit/document_tests.py
index 61fa9fac..09cb09c4 100644
--- a/tests/unit/document_tests.py
+++ b/tests/unit/document_tests.py
@@ -944,14 +944,14 @@ def object_hook(self, obj):
raw_doc = self.db.all_docs(include_docs=True)['rows'][0]['doc']
- self.assertEquals(raw_doc['name'], 'julia')
- self.assertEquals(raw_doc['dt']['_type'], 'datetime')
- self.assertEquals(raw_doc['dt']['value'], '2018-07-09T15:11:10')
+ self.assertEqual(raw_doc['name'], 'julia')
+ self.assertEqual(raw_doc['dt']['_type'], 'datetime')
+ self.assertEqual(raw_doc['dt']['value'], '2018-07-09T15:11:10')
doc2 = Document(self.db, doc['_id'], decoder=DTDecoder)
doc2.fetch()
- self.assertEquals(doc2['dt'], doc['dt'])
+ self.assertEqual(doc2['dt'], doc['dt'])
if __name__ == '__main__':
unittest.main()
diff --git a/tests/unit/document_validation_tests.py b/tests/unit/document_validation_tests.py
new file mode 100644
index 00000000..021d8d6f
--- /dev/null
+++ b/tests/unit/document_validation_tests.py
@@ -0,0 +1,909 @@
+#!/usr/bin/env python
+# Copyright © 2021 IBM Corp. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+from enum import Enum
+from unittest.mock import Mock, patch
+
+from mock import create_autospec
+
+import requests
+from urllib.parse import urlparse
+
+from cloudant import database
+from cloudant.design_document import DesignDocument
+from cloudant.document import Document
+from cloudant.error import CloudantArgumentError
+
+class ValidationExceptionMsg(Enum):
+ DOC = 'Invalid document ID:'
+ ATTACHMENT = 'Invalid attachment name:'
+
+class Expect(Enum):
+ VALIDATION_EXCEPTION_DOCID = CloudantArgumentError(137, '')
+ VALIDATION_EXCEPTION_ATT = CloudantArgumentError(138, '')
+ RESPONSE_404 = 404
+ RESPONSE_200 = 200
+ RESPONSE_201 = 201
+
+
+class ValidationTests(unittest.TestCase):
+ """
+ Document validation unit tests
+ """
+ def setUp(self):
+ self.doc_r_session_patcher = patch('cloudant.document.Document.r_session')
+ self.requests_get_patcher = patch('requests.get')
+
+ self.addCleanup(patch.stopall)
+
+ self.doc_r_session_mock = self.doc_r_session_patcher.start()
+ self.requests_get_mock = self.requests_get_patcher.start()
+
+ self.db = create_autospec(database)
+ self.db.client = Mock()
+ self.db.client.server_url = 'http://mocked.url.com'
+ self.db.database_url = 'http://mocked.url.com/my_db'
+ self.db.database_name = 'mydb'
+
+ def teardown(self):
+ self.addCleanup(patch.stopall)
+ del self.db
+ del self.doc_r_session_patcher
+ del self.requests_get_patcher
+ del self.doc_r_session_mock
+ del self.requests_get_mock
+
+ # GET and HEAD _all_docs
+ # EXPECTED: validation failure
+ def test_get_invalid_all_docs(self):
+ """
+ Test GET/HEAD request for invalid '_all_docs' document ID
+ """
+ self.get_document_variants('_all_docs', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # GET and HEAD _design/foo
+ # EXPECTED: 200
+ def test_get_valid_ddoc(self):
+ """
+ Test GET/HEAD request for valid '_design/foo' document ID
+ """
+ self.get_document_variants('_design/foo', Expect.RESPONSE_200.value, path_segment_count=3)
+ self.get_document_variants('_design/foo', Expect.RESPONSE_200.value, True, path_segment_count=3)
+
+ # GET and HEAD _design
+ # EXPECTED: Validation exception
+ def test_get_invalid_design(self):
+ """
+ Test GET/HEAD request for invalid '_design' document ID
+ """
+ self.get_document_variants('_design', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.get_document_variants('_design', Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+
+ # GET and HEAD /_design/foo with a slash
+ # EXPECTED: 404
+ def test_get_missing_ddoc_with_slash(self):
+ """
+ Test GET/HEAD request for missing '/_design/foo' document ID
+ """
+ self.get_document_variants('/_design/foo', Expect.RESPONSE_404.value, path_segment_count=2)
+
+ # GET and HEAD _design/foo/_view/bar
+ # EXPECTED: 404
+ def test_get_invalid_view(self):
+ """
+ Test GET/HEAD request for missing '_design/foo' document ID
+ """
+ self.get_document_variants('_design/foo/_view/bar', Expect.RESPONSE_404.value, path_segment_count=3)
+ self.get_document_variants('_design/foo/_view/bar', Expect.RESPONSE_404.value, True, path_segment_count=3)
+
+ # GET and HEAD _design/foo/_info
+ # EXPECTED: 404
+ def test_get_invalid_view_info(self):
+ """
+ Test GET/HEAD request for missing '_design/foo/_info' document ID
+ """
+ self.get_document_variants('_design/foo/_info', Expect.RESPONSE_404.value, path_segment_count=3)
+ self.get_document_variants('_design/foo/_info', Expect.RESPONSE_404.value, True, path_segment_count=3)
+
+ # GET and HEAD _design/foo/_search/bar
+ # EXPECTED: 404
+ def test_get_invalid_search(self):
+ """
+ Test GET/HEAD request for missing '_design/foo/_search/bar' document ID
+ """
+ self.get_document_variants('_design/foo/_search/bar', Expect.RESPONSE_404.value, path_segment_count=3)
+ self.get_document_variants('_design/foo/_search/bar', Expect.RESPONSE_404.value, True, path_segment_count=3)
+
+ # GET and HEAD _design/foo/_search_info/bar
+ # EXPECTED: 404
+ def test_get_invalid_search_info(self):
+ """
+ Test GET/HEAD request for missing '_design/foo/_search_info/bar' document ID
+ """
+ self.get_document_variants('_design/foo/_search_info/bar', Expect.RESPONSE_404.value, path_segment_count=3)
+ self.get_document_variants('_design/foo/_search_info/bar', Expect.RESPONSE_404.value, True, path_segment_count=3)
+
+ # GET and HEAD _design/foo/_geo/bar
+ # EXPECTED: 404
+ def test_get_missing_geo(self):
+ """
+ Test GET/HEAD request for missing '_design/foo/_geo/bar' document ID
+ """
+ self.get_document_variants('_design/foo/_geo/bar', Expect.RESPONSE_404.value, path_segment_count=3)
+ self.get_document_variants('_design/foo/_geo/bar', Expect.RESPONSE_404.value, True, path_segment_count=3)
+ # with a parameter
+ self.get_document_variants('_design/foo/_geo/bar?bbox=-50.52,-4.46,54.59,1.45', Expect.RESPONSE_404.value,
+ path_segment_count=3)
+ self.get_document_variants('_design/foo/_geo/bar?bbox=-50.52,-4.46,54.59,1.45', Expect.RESPONSE_404.value, True,
+ path_segment_count=3)
+
+ # GET and HEAD _design/foo/_geo_info/bar
+ # EXPECTED: 404
+ def test_get_missing_geo_info(self):
+ """
+ Test GET/HEAD request for missing '_design/foo/_geo_info/bar' document ID
+ """
+ self.get_document_variants('_design/foo/_geo_info/bar', Expect.RESPONSE_404.value, path_segment_count=3)
+ self.get_document_variants('_design/foo/_geo_info/bar', Expect.RESPONSE_404.value, True, path_segment_count=3)
+
+ # GET and HEAD _local/foo
+ # EXPECTED: 200
+ def test_get_local_doc(self):
+ """
+ Test GET/HEAD request for valid '_local/foo' document ID
+ """
+ self.get_document_variants('_local/foo', Expect.RESPONSE_200.value, path_segment_count=3)
+
+ # GET and HEAD _local
+ # EXPECTED: Validation exception
+ def test_get_invalid_local(self):
+ """
+ Test GET/HEAD request for invalid '_local' document ID
+ """
+ self.get_document_variants('_local', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # GET and HEAD _local_docs
+ # EXPECTED: Validation exception
+ def test_get_invalid_local_docs(self):
+ """
+ Test GET/HEAD request for invalid '_local_docs' document ID
+ """
+ self.get_document_variants('_local_docs', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # GET and HEAD _design_docs
+ # EXPECTED: Validation exception
+ def test_get_invalid_design_docs(self):
+ """
+ Test GET/HEAD request for invalid '_design_docs' document ID
+ """
+ self.get_document_variants('_design_docs', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # GET and HEAD _changes
+ # EXPECTED: Validation exception
+ def test_get_invalid_changes(self):
+ """
+ Test GET/HEAD request for invalid '_changes' document ID
+ """
+ self.get_document_variants('_changes', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # GET and HEAD _ensure_full_commit
+ # EXPECTED: Validation exception
+ def test_get_invalid_ensure_full_commit(self):
+ """
+ Test GET/HEAD request for invalid '_ensure_full_commit' document ID
+ """
+ self.get_document_variants('_ensure_full_commit', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # GET and HEAD _index
+ # EXPECTED: Validation exception
+ def test_get_invalid_index(self):
+ """
+ Test GET/HEAD request for invalid '_index' document ID
+ """
+ self.get_document_variants('_index', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # GET and HEAD _revs_limit
+ # EXPECTED: Validation exception
+ def test_get_invalid_revs_limit(self):
+ """
+ Test GET/HEAD request for invalid '_revs_limit' document ID
+ """
+ self.get_document_variants('_revs_limit', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # GET and HEAD _security
+ # EXPECTED: Validation exception
+ def test_get_invalid_security(self):
+ """
+ Test GET/HEAD request for invalid '_security' document ID
+ """
+ self.get_document_variants('_security', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # GET and HEAD _shards
+ # EXPECTED: Validation exception
+ def test_get_invalid_shards(self):
+ """
+ Test GET/HEAD request for invalid '_shards' document ID
+ """
+ self.get_document_variants('_shards', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # DELETE _index/_design/foo/json/bar
+ # EXPECTED: Validation exception
+ def test_delete_invalid_index(self):
+ """
+ Test DELETE request for invalid '_index/_design/foo/json/bar' document ID
+ """
+ self.delete_document_variants('_index/_design/foo/json/bar', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # DELETE _design/foo
+ # EXPECTED: 200
+ def test_delete_valid_ddoc(self):
+ """
+ Test DELETE request for valid '_design/foo' document ID
+ """
+ self.delete_document_variants('_design/foo', Expect.RESPONSE_200.value, path_segment_count=3)
+
+ # DELETE _design
+ # EXPECTED: Validation exception
+ def test_delete_invalid_ddoc(self):
+ """
+ Test DELETE request for invalid '_design' document ID
+ """
+ # no trailing '/' on _design prefix
+ self.delete_document_variants('_design', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # DELETE _local/foo
+ # EXPECTED: 200
+ def test_delete_valid_local_doc(self):
+ """
+ Test DELETE request for valid '_local/foo' document ID
+ """
+ self.delete_document_variants('_local/foo', Expect.RESPONSE_200.value, path_segment_count=3)
+
+ # DELETE _local
+ # EXPECTED: Validation exception
+ def test_delete_invalid_local(self):
+ """
+ Test DELETE request for invalid '_local' document ID
+ """
+ # no trailing '/' on _local prefix
+ self.delete_document_variants('_local', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # PUT _design/foo
+ # EXPECTED: 201
+ def test_put_valid_ddoc(self):
+ """
+ Test PUT request for valid '_design/foo' document ID
+ """
+ self.put_document_variants('_design/foo', Expect.RESPONSE_201.value, path_segment_count=3)
+
+ # PUT _design
+ # EXPECTED: Validation exception
+ def test_put_invalid_ddoc(self):
+ """
+ Test PUT request for invalid '_design' document ID
+ """
+ self.put_document_variants('_design', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # PUT _local/foo
+ # EXPECTED: 201
+ def test_put_valid_local_doc(self):
+ """
+ Test PUT request for valid '_local/foo' document ID
+ """
+ self.put_document_variants('_local/foo', Expect.RESPONSE_201.value, path_segment_count=3)
+
+ # PUT _local
+ # EXPECTED: Validation exception
+ def test_put_invalid_local_doc(self):
+ """
+ Test PUT request for invalid '_local' document ID
+ """
+ self.put_document_variants('_local', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # PUT _revs_limit
+ # EXPECTED: Validation exception
+ def test_put_invalid_revs_limit(self):
+ """
+ Test PUT request for invalid '_revs_limit' document ID
+ """
+ self.put_document_variants('_revs_limit', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # PUT _security
+ # EXPECTED: Validation exception
+ def test_put_invalid_security(self):
+ """
+ Test PUT request for invalid '_security' document ID
+ """
+ self.put_document_variants('_security', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # GET _design/foo/bar
+ # EXPECTED: 200
+ def test_get_valid_ddoc_attachment(self):
+ """
+ Test PUT request for valid '_design/foo/bar' document ID
+ """
+ self.get_doc_attachment_variants('_design/foo', 'bar', Expect.RESPONSE_200.value, True, path_segment_count=4)
+
+ # PUT _design/foo/bar
+ # EXPECTED: 201
+ def test_put_valid_ddoc_attachment(self):
+ """
+ Test PUT request for valid '_design/foo/bar' document ID
+ """
+ self.put_doc_attachment_variants('_design/foo', 'bar', Expect.RESPONSE_201.value, True, path_segment_count=4)
+
+ # DELETE _design/foo/bar
+ # EXPECTED: 200
+ def test_delete_valid_ddoc_attachment(self):
+ """
+ Test DELETE request for valid '_design/foo/bar' document ID
+ """
+ self.delete_doc_attachment_variants('_design/foo', 'bar', Expect.RESPONSE_200.value, True, path_segment_count=4)
+
+ # GET _design/foo
+ # EXPECTED: Validation exception
+ def test_get_invalid_ddoc_attachment(self):
+ """
+ Test GET request for invalid '_design/foo' document ID
+ """
+ # with ddoc option enabled
+ self.get_doc_attachment_variants('_design', 'foo', Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+ self.get_doc_attachment_variants('_design', 'foo', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # PUT _design/foo
+ # EXPECTED: Validation exception
+ def test_put_invalid_ddoc_attachment(self):
+ """
+ Test PUT request for invalid '_design/foo' document ID
+ """
+ # with ddoc option enabled
+ self.put_doc_attachment_variants('_design', 'foo', Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+ self.put_doc_attachment_variants('_design', 'foo', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # DELETE _design/foo
+ # EXPECTED: Validation exception
+ def test_delete_invalid_ddoc_attachment(self):
+ """
+ Test DELETE request for invalid '_design/foo' document ID
+ """
+ # with ddoc option enabled
+ self.delete_doc_attachment_variants('_design', 'foo', Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+ self.delete_doc_attachment_variants('_design', 'foo', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # DELETE _index/_design/foo/json/bar
+ # EXPECTED: Validation exception
+ def test_delete_index_via_attachment(self):
+ """
+ Test DELETE requests for invalid '_index/_design/foo/json/bar'
+ """
+ self.delete_doc_attachment_variants('_index', '_design/foo/json/bar', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.delete_doc_attachment_variants('_index', '_design/foo/json/bar',
+ Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+ self.delete_doc_attachment_variants('_index/_design', 'foo/json/bar', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.delete_doc_attachment_variants('_index/_design', 'foo/json/bar',
+ Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+ self.delete_doc_attachment_variants('_index/_design/foo', 'json/bar', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.delete_doc_attachment_variants('_index/_design/foo', 'json/bar',
+ Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+ self.delete_doc_attachment_variants('_index/_design/foo/json', 'bar', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.delete_doc_attachment_variants('_index/_design/foo/json', 'bar',
+ Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+
+ # GET _design/foo/_view/bar
+ def test_get_view_via_ddoc_attachment(self):
+ """
+ Test GET requests for '_design/foo/_view/bar'
+ """
+ # EXPECTED: 404
+ self.get_doc_attachment_variants('_design/foo/_view', 'bar', Expect.RESPONSE_404.value, path_segment_count=4)
+ self.get_doc_attachment_variants('_design/foo/_view', 'bar', Expect.RESPONSE_404.value, True, path_segment_count=4)
+ self.get_doc_attachment_variants('_design/foo', '/_view/bar', Expect.RESPONSE_404.value, path_segment_count=4)
+ self.get_doc_attachment_variants('_design/foo', '/_view/bar', Expect.RESPONSE_404.value, True, path_segment_count=4)
+ # EXPECTED: Validation exception
+ self.get_doc_attachment_variants('_design/foo', '_view/bar', Expect.VALIDATION_EXCEPTION_ATT.value)
+ self.get_doc_attachment_variants('_design/foo', '_view/bar', Expect.VALIDATION_EXCEPTION_ATT.value, True)
+ self.get_doc_attachment_variants('_design', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.get_doc_attachment_variants('_design', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+ self.get_doc_attachment_variants('_design/', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.get_doc_attachment_variants('_design/', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+
+ # PUT _design/foo/_view/bar
+ def test_put_view_via_ddoc_attachment(self):
+ """
+ Test PUT requests for '_design/foo/_view/bar'
+ """
+ # EXPECTED: Validation exception
+ self.put_doc_attachment_variants('_design/foo', '_view/bar', Expect.VALIDATION_EXCEPTION_ATT.value)
+ self.put_doc_attachment_variants('_design/foo', '_view/bar', Expect.VALIDATION_EXCEPTION_ATT.value, True)
+ self.put_doc_attachment_variants('_design', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.put_doc_attachment_variants('_design', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+ self.put_doc_attachment_variants('_design/', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.put_doc_attachment_variants('_design/', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+
+ # DELETE _design/foo/_view/bar
+ def test_delete_view_via_ddoc_attachment(self):
+ """
+ Test DELETE requests for '_design/foo/_view/bar'
+ """
+ # EXPECTED: Validation exception
+ self.delete_doc_attachment_variants('_design/foo', '_view/bar', Expect.VALIDATION_EXCEPTION_ATT.value)
+ self.delete_doc_attachment_variants('_design/foo', '_view/bar', Expect.VALIDATION_EXCEPTION_ATT.value, True)
+ self.delete_doc_attachment_variants('_design', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.delete_doc_attachment_variants('_design', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+ self.delete_doc_attachment_variants('_design/', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.delete_doc_attachment_variants('_design/', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+
+ # GET _design/foo/_info
+ def test_get_view_info_via_ddoc_attachment(self):
+ """
+ Test GET requests for '_design/foo/_info'
+ """
+ # EXPECTED: Validation exception
+ self.get_doc_attachment_variants('_design/foo', '_info', Expect.VALIDATION_EXCEPTION_ATT.value)
+ self.get_doc_attachment_variants('_design/foo', '_info', Expect.VALIDATION_EXCEPTION_ATT.value, True)
+ self.get_doc_attachment_variants('_design', 'foo/_info', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.get_doc_attachment_variants('_design', 'foo/_info', Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+ self.get_doc_attachment_variants('_design/', 'foo/_info', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.get_doc_attachment_variants('_design/', 'foo/_info', Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+
+ # GET _design/foo/_search/bar
+ def test_get_search_via_ddoc_attachment(self):
+ """
+ Test GET requests for '_design/foo/_search/bar'
+ """
+ # EXPECTED: 404
+ self.get_doc_attachment_variants('_design/foo/_search', 'bar', Expect.RESPONSE_404.value, path_segment_count=4)
+ self.get_doc_attachment_variants('_design/foo/_search', 'bar', Expect.RESPONSE_404.value, True,
+ path_segment_count=4)
+ self.get_doc_attachment_variants('_design/foo/_search', 'bar?q=*.*', Expect.RESPONSE_404.value,
+ path_segment_count=4)
+ self.get_doc_attachment_variants('_design/foo/_search', 'bar?q=*.*', Expect.RESPONSE_404.value, True,
+ path_segment_count=4)
+ # EXPECTED: Validation exception
+ self.get_doc_attachment_variants('_design/foo', '_search/bar', Expect.VALIDATION_EXCEPTION_ATT.value)
+ self.get_doc_attachment_variants('_design/foo', '_search/bar', Expect.VALIDATION_EXCEPTION_ATT.value, True)
+ self.get_doc_attachment_variants('_design', 'foo/_search/bar', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.get_doc_attachment_variants('_design', 'foo/_search/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+ self.get_doc_attachment_variants('_design/', 'foo/_search/bar', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.get_doc_attachment_variants('_design/', 'foo/_search/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True)
+
+ # GET _design/foo/_search_info/bar
+ def test_get_search_info_via_ddoc_attachment(self):
+ """
+ Test GET requests for '_design/foo/_search_info/bar'
+ """
+ # EXPECTED: 404
+ self.get_doc_attachment_variants('_design/foo/_search_info', 'bar', Expect.RESPONSE_404.value,
+ path_segment_count=4)
+ self.get_doc_attachment_variants('_design/foo/_search_info', 'bar', Expect.RESPONSE_404.value, True,
+ path_segment_count=4)
+ # EXPECTED: Validation exception
+ self.get_doc_attachment_variants('_design/foo', '_search_info/bar', Expect.VALIDATION_EXCEPTION_ATT.value)
+ self.get_doc_attachment_variants('_design/foo', '_search_info/bar', Expect.VALIDATION_EXCEPTION_ATT.value, True)
+
+ # GET _design/foo/_geo/bar
+ def test_get_geo_via_ddoc_attachment(self):
+ """
+ Test GET requests for '_design/foo/_geo/bar'
+ """
+ # EXPECTED: 404
+ self.get_doc_attachment_variants('_design/foo/_geo', 'bar', Expect.RESPONSE_404.value, path_segment_count=4)
+ self.get_doc_attachment_variants('_design/foo/_geo', 'bar', Expect.RESPONSE_404.value, True,
+ path_segment_count=4)
+ self.get_doc_attachment_variants('_design/foo/_geo', 'bar?bbox=-50.52,-4.46,54.59,1.45',
+ Expect.RESPONSE_404.value, path_segment_count=4)
+ self.get_doc_attachment_variants('_design/foo/_geo', 'bar?bbox=-50.52,-4.46,54.59,1.45',
+ Expect.RESPONSE_404.value, True, path_segment_count=4)
+ # EXPECTED: Validation exception
+ self.get_doc_attachment_variants('_design/foo', '_geo/bar', Expect.VALIDATION_EXCEPTION_ATT.value)
+ self.get_doc_attachment_variants('_design/foo', '_geo/bar', Expect.VALIDATION_EXCEPTION_ATT.value, True)
+
+ # GET _design/foo/_geo_info/bar
+ def test_get_geo_info_via_ddoc_attachment(self):
+ """
+ Test GET requests for '_design/foo/_geo_info/bar'
+ """
+ # EXPECTED: 404
+ self.get_doc_attachment_variants('_design/foo/_geo_info', 'bar', Expect.RESPONSE_404.value,
+ path_segment_count=4)
+ self.get_doc_attachment_variants('_design/foo/_geo_info', 'bar', Expect.RESPONSE_404.value, True,
+ path_segment_count=4)
+ # EXPECTED: Validation exception
+ self.get_doc_attachment_variants('_design/foo', '_geo_info/bar', Expect.VALIDATION_EXCEPTION_ATT.value)
+ self.get_doc_attachment_variants('_design/foo', '_geo_info/bar', Expect.VALIDATION_EXCEPTION_ATT.value, True)
+
+ # GET _partition/foo
+ # EXPECTED: Validation exception
+ def test_get_invalid_partition_info(self):
+ """
+ Test GET requests for '_partition/foo'
+ """
+ self.get_document_variants('_partition/foo', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # GET _partition/foo
+ # EXPECTED: Validation exception
+ def test_get_invalid_partition_info_via_attachment(self):
+ """
+ Test GET requests for '_partition/foo'
+ """
+ self.get_doc_attachment_variants('_partition', 'foo', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # GET _partition/foo/_all_docs
+ # EXPECTED: Validation exception
+ def test_get_partition_info(self):
+ """
+ Test GET requests for '_partition/foo/_all_docs'
+ """
+ self.get_document_variants('_partition/foo/_all_docs', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ # GET _partition/foo/_all_docs
+ # EXPECTED: Validation exception
+ def test_get_invalid_partition_all_docs_via_attachment(self):
+ """
+ Test GET requests for '_partition/foo/_all_docs'
+ """
+ self.get_doc_attachment_variants('_partition', 'foo/_all_docs', Expect.VALIDATION_EXCEPTION_DOCID.value)
+ self.get_doc_attachment_variants('_partition/foo', '_all_docs', Expect.VALIDATION_EXCEPTION_DOCID.value)
+
+ """UTIL FUNCTIONS"""
+ def mocked_get_requests(self, rev=None, override_status_code=None):
+ """
+ Create a mock GET request for documents with the expected status code
+ :param rev: the doc's revision (default None)
+ :param override_status_code: override the status code for handling
+ inner `fetch` request call within `get_attachment`
+ :return: mocked Response object
+ """
+ resp_mock = create_autospec(requests.Response)
+ if override_status_code is not None:
+ resp_mock.status_code = override_status_code
+ else:
+ resp_mock.status_code = self.expected_enum
+ if (resp_mock.status_code == 200 or resp_mock.status_code == 201
+ and self.doc_id is not None):
+ if rev is not None:
+ resp_mock.text = f"""{{"_id": "{self.doc_id}", "_rev": "{rev}"}}"""
+ else:
+ resp_mock.text = f"""{{"_id": "{self.doc_id}", "_rev": "1-abc"}}"""
+ elif resp_mock.status_code == 404:
+ resp_mock.raise_for_status.side_effect = requests.exceptions.HTTPError
+ resp_mock.encoding = None
+
+ return resp_mock
+
+ def mocked_get_att_requests(self):
+ """
+ Create a mock GET request for attachments with the expected status code
+ """
+ self.expected_att_content = f"""this is a text attachment"""
+ # first fetch doc call with rev
+ fetch_mock = self.mocked_get_requests(rev=None, override_status_code=200)
+ # second get to attachment
+ resp_mock = create_autospec(requests.Response)
+ resp_mock.status_code = self.expected_enum
+ if self.expected_enum == 200 and self.doc_id is not None and self.att_name is not None:
+ resp_mock.text = self.expected_att_content
+ if self.expected_enum == 404:
+ resp_mock.raise_for_status.side_effect = requests.exceptions.HTTPError
+ self.doc_r_session_mock.get.side_effect = [fetch_mock, resp_mock]
+
+ def mocked_head_requests(self, override_status_code=None):
+ """
+ Create a mock HEAD request for documents and attachments with the expected status code
+ """
+ resp_mock = create_autospec(requests.Response)
+ if override_status_code is not None:
+ resp_mock.status_code = override_status_code
+ else:
+ resp_mock.status_code = self.expected_enum
+ self.doc_r_session_mock.head = Mock(return_value=resp_mock)
+
+ def mocked_delete_requests(self):
+ """
+ Create a mock DELETE request for documents with the expected status code
+ """
+ resp_mock = create_autospec(requests.Response)
+ resp_mock.status_code = self.expected_enum
+ if self.expected_enum == 201 and self.doc_id is not None:
+ resp_mock.text = f"""{{"id": "{self.doc_id}", "rev": "2-abc", "ok": true}}"""
+ self.doc_r_session_mock.delete = Mock(return_value=resp_mock)
+
+ def mocked_delete_att_requests(self):
+ """
+ Create a mock DELETE request for attachments with the expected status code
+ """
+ # first `fetch` document call with rev
+ self.doc_r_session_mock.get = Mock(return_value=self.mocked_get_requests(rev=None, override_status_code=200))
+ # second delete to attachment
+ resp_mock = create_autospec(requests.Response)
+ resp_mock.status_code = self.expected_enum
+ resp_mock.encoding = None
+ if self.expected_enum == 200 and self.doc_id is not None and self.att_name is not None:
+ resp_mock.text = f"""{{"id": "{self.doc_id}", "rev": "2-abc", "ok": true}}"""
+ elif self.expected_enum == 404:
+ resp_mock.raise_for_status.side_effect = requests.exceptions.HTTPError
+
+ self.doc_r_session_mock.delete = Mock(return_value=resp_mock)
+
+ def mocked_put_doc_requests(self):
+ """
+ Create a mock PUT request for documents with the expected status code
+ """
+ # mock 'doc.exists' request call within 'doc.save' function
+ self.mocked_head_requests(200)
+ resp_mock = create_autospec(requests.Response)
+ resp_mock.status_code = self.expected_enum
+ resp_mock.encoding = None
+ if self.expected_enum == 201 and self.doc_id is not None:
+ resp_mock.text = f"""{{"id": "{self.doc_id}", "rev": "1-abc", "ok": true}}"""
+ if self.expected_enum == 404:
+ resp_mock.raise_for_status.side_effect = requests.exceptions.HTTPError
+ self.doc_r_session_mock.put = Mock(return_value=resp_mock)
+
+ def mocked_put_att_requests(self):
+ """
+ Create a mock PUT request for attachments with the expected status code
+ """
+ # first `fetch` document call within `put_attachment`
+ fetch_mock = self.mocked_get_requests(rev=None, override_status_code=200)
+ # create Response object for PUT attachment
+ resp_mock = create_autospec(requests.Response)
+ resp_mock.status_code = self.expected_enum
+ resp_mock.encoding = None
+ if self.expected_enum == 201 and self.doc_id is not None:
+ resp_mock.text = f"""{{"id": "{self.doc_id}", "rev": "2-def", "ok": true}}"""
+ if self.expected_enum == 404:
+ resp_mock.raise_for_status.side_effect = requests.exceptions.HTTPError
+ # final fetch doc call
+ second_fetch_mock = self.mocked_get_requests(rev='2-def', override_status_code=200)
+ self.doc_r_session_mock.get.side_effect = [fetch_mock, second_fetch_mock]
+ self.doc_r_session_mock.put = Mock(return_value=resp_mock)
+
+ def get_document_variants(self, doc_id, expected_enum, is_ddoc=False,
+ path_segment_count=None):
+ """
+ Function to setup mock requests and execute GET/HEAD document requests
+ """
+ self.doc_id = doc_id
+ self.expected_enum = expected_enum
+ self.is_ddoc = is_ddoc
+ self.mocked_head_requests()
+ self.head_document()
+ self.doc_r_session_mock.get.return_value = self.mocked_get_requests()
+ self.fetch_document()
+ self.assert_path_segments(self.doc_r_session_mock.get.call_args_list, path_segment_count)
+
+ def get_doc_attachment_variants(self, doc_id, att_name, expected_enum, is_ddoc=False,
+ path_segment_count=None):
+ """
+ Function to setup mock requests and execute GET attachment requests
+ """
+ self.att_name = att_name
+ self.doc_id = doc_id
+ self.expected_enum = expected_enum
+ self.is_ddoc = is_ddoc
+ self.mocked_get_att_requests()
+ self.get_doc_attachment()
+ self.assert_path_segments(self.doc_r_session_mock.get.call_args_list, path_segment_count)
+
+ def put_document_variants(self, doc_id, expected_enum, is_ddoc=False,
+ path_segment_count=None):
+ """
+ Function to setup mock requests and execute PUT document requests
+ """
+ self.doc_id = doc_id
+ self.expected_enum = expected_enum
+ self.is_ddoc = is_ddoc
+ self.mocked_put_doc_requests()
+ self.put_document()
+ self.assert_path_segments(self.doc_r_session_mock.put.call_args_list, path_segment_count)
+
+ def put_doc_attachment_variants(self, doc_id, att_name, expected_enum, is_ddoc=False,
+ path_segment_count=None):
+ """
+ Function to setup mock requests and execute PUT attachment requests
+ """
+ self.att_name = att_name
+ self.doc_id = doc_id
+ self.expected_enum = expected_enum
+ self.is_ddoc = is_ddoc
+ self.mocked_put_att_requests()
+ self.put_doc_attachment()
+ self.assert_path_segments(self.doc_r_session_mock.put.call_args_list, path_segment_count)
+
+ def delete_document_variants(self, doc_id, expected_enum, is_ddoc=False,
+ path_segment_count=None):
+ """
+ Function to setup mock requests and execute DELETE document requests
+ """
+ self.doc_id = doc_id
+ self.expected_enum = expected_enum
+ self.is_ddoc = is_ddoc
+ self.mocked_delete_requests()
+ self.delete_document()
+ self.assert_path_segments(self.doc_r_session_mock.delete.call_args_list, path_segment_count)
+
+ def delete_doc_attachment_variants(self, doc_id, attname, expected_enum, is_ddoc=False,
+ path_segment_count=None):
+ """
+ Function to setup mock requests and execute DELETE attachment requests
+ """
+ self.doc_id = doc_id
+ self.att_name = attname
+ self.expected_enum = expected_enum
+ self.is_ddoc = is_ddoc
+ self.mocked_delete_att_requests()
+ self.delete_doc_attachment()
+ self.assert_path_segments(self.doc_r_session_mock.delete.call_args_list, path_segment_count)
+
+ """HTTP REQUEST FUNCTIONS"""
+ def head_document(self):
+ try:
+ resp = self.create_doc(self.doc_id, self.is_ddoc).exists()
+ if self.expected_enum == 200 or self.expected_enum == 201:
+ self.assertTrue(resp)
+ elif self.expected_enum == 404:
+ self.assertFalse(resp)
+ except CloudantArgumentError as cae:
+ self.assert_exception_msg(cae)
+
+ def delete_document(self):
+ try:
+ doc = self.create_doc(self.doc_id, self.is_ddoc)
+ doc['_rev'] = '1-abc'
+ doc.delete()
+ self.assertTrue(isinstance(self.expected_enum, int),
+ f"""Expected value {self.expected_enum} is not an int status code.""")
+ self.assertTrue(self.expected_enum < 400,
+ f"""Expected value {self.expected_enum} is not a successful status code.""")
+ self.assertEqual(self.doc_id, doc['_id'])
+ self.assertFalse('rev' in doc)
+ except CloudantArgumentError as cae:
+ self.assert_exception_msg(cae)
+ except requests.exceptions.HTTPError as err:
+ self.assertTrue(id(self.expected_enum), id(err))
+
+ def fetch_document(self):
+ try:
+ doc = self.create_doc(self.doc_id, self.is_ddoc)
+ doc.fetch()
+ self.assertTrue(isinstance(self.expected_enum, int),
+ f"""Expected value {self.expected_enum} is not an int status code.""")
+ self.assertTrue(self.expected_enum < 400,
+ f"""Expected value {self.expected_enum} is not a successful status code.""")
+ self.assertEqual(self.doc_id, doc['_id'])
+ self.assertIsNotNone(doc['_rev'])
+ except CloudantArgumentError as cae:
+ self.assert_exception_msg(cae)
+ except requests.exceptions.HTTPError as err:
+ self.assertTrue(id(self.expected_enum), id(err))
+
+ def put_document(self):
+ try:
+ doc = self.create_doc(self.doc_id, self.is_ddoc)
+ doc.save()
+ self.assertTrue(isinstance(self.expected_enum, int),
+ f"""Expected value {self.expected_enum} is not an int status code.""")
+ self.assertTrue(self.expected_enum < 400,
+ f"""Expected value {self.expected_enum} is not a successful status code.""")
+ self.assertEqual(self.doc_id, doc['_id'])
+ self.assertIsNotNone(doc['_rev'])
+ except CloudantArgumentError as cae:
+ self.assert_exception_msg(cae)
+ except requests.exceptions.HTTPError as err:
+ self.assertTrue(id(self.expected_enum), id(err))
+
+ def delete_doc_attachment(self):
+ try:
+ doc = self.create_doc(self.doc_id, self.is_ddoc)
+ doc['_rev'] = '1-abc'
+ resp = doc.delete_attachment(self.att_name)
+ self.assertTrue(isinstance(self.expected_enum, int),
+ f"""Expected value {self.expected_enum} is not an int status code.""")
+ self.assertTrue(self.expected_enum < 400,
+ f"""Expected value {self.expected_enum} is not a successful status code.""")
+ self.assertEqual(self.doc_id, doc['_id'])
+ self.assertEqual(self.doc_id, resp['id'])
+ self.assertEqual(doc['_rev'], resp['rev'])
+ except CloudantArgumentError as cae:
+ self.assert_exception_msg(cae)
+ except requests.exceptions.HTTPError as err:
+ self.assertTrue(id(self.expected_enum), id(err))
+
+ def get_doc_attachment(self):
+ try:
+ doc = self.create_doc(self.doc_id, self.is_ddoc)
+ resp_att = doc.get_attachment(self.att_name, attachment_type='text')
+ self.assertTrue(isinstance(self.expected_enum, int),
+ f"""Expected value {self.expected_enum} is not an int status code.""")
+ self.assertTrue(self.expected_enum < 400,
+ f"""Expected value {self.expected_enum} is not a successful status code.""")
+ self.assertEqual(self.doc_id, doc['_id'])
+ self.assertIsNotNone(resp_att)
+ self.assertEqual(resp_att, self.expected_att_content)
+ except CloudantArgumentError as cae:
+ self.assert_exception_msg(cae)
+ except requests.exceptions.HTTPError as err:
+ self.assertTrue(id(self.expected_enum), id(err))
+
+ def put_doc_attachment(self):
+ try:
+ doc = self.create_doc(self.doc_id, self.is_ddoc)
+ resp_att = doc.put_attachment(self.att_name, content_type='utf-8', data='test')
+ self.assertIsNotNone(resp_att)
+ self.assertTrue(isinstance(self.expected_enum, int),
+ f"""Expected value {self.expected_enum} is not an int status code.""")
+ self.assertTrue(self.expected_enum < 400,
+ f"""Expected value {self.expected_enum} is not a successful status code.""")
+ self.assertEqual(self.doc_id, resp_att['id'])
+ self.assertEqual(resp_att['id'], doc['_id'])
+ self.assertEqual(doc['_rev'], resp_att['rev'])
+ self.assertEqual(resp_att['ok'], True)
+ except CloudantArgumentError as cae:
+ self.assert_exception_msg(cae)
+ except requests.exceptions.HTTPError as err:
+ self.assertTrue(id(self.expected_enum), id(err))
+
+ """HELPER FUNCTIONS"""
+ def create_doc(self, doc_id=None, is_ddoc=False):
+ """
+ Function to create and return a Document or DesignDocument object.
+ """
+ if is_ddoc:
+ if doc_id is not None:
+ doc = DesignDocument(self.db, doc_id)
+ else:
+ doc = DesignDocument(self.db)
+ elif doc_id is not None:
+ doc = Document(self.db, doc_id)
+ else:
+ doc = Document(self.db)
+ self.assertIsNone(doc.get('_rev'))
+ return doc
+
+ def assert_exception_msg(self, cae):
+ """
+ Function to assert whether the exception message is for an invalid
+ document ID or an attachment name.
+ """
+ self.assertTrue(id(self.expected_enum), id(cae))
+ # Check that actual exception message starts with the expected msg
+ if str(cae).startswith(str(self.expected_enum)):
+ # Figure out which exception msg to assert against
+ if str(cae).startswith(ValidationExceptionMsg.ATTACHMENT.value):
+ self.assertEqual(str(cae), f"""{ValidationExceptionMsg.ATTACHMENT.value} {self.att_name}""")
+ elif str(cae).startswith(ValidationExceptionMsg.DOC.value):
+ self.assertEqual(str(cae), f"""{ValidationExceptionMsg.DOC.value} {self.doc_id}""")
+ else:
+ self.fail('Expected CloudantArgumentError message should equal actual error message.')
+
+ def assert_path_segments(self, actual_call_args_list, exp_segment_count):
+ """
+ Function to assert the number of path segments from a mock request
+ """
+ # If there's no segment count, verify that the test case expects an argument error
+ if exp_segment_count is None:
+ self.assertTrue(isinstance(self.expected_enum, CloudantArgumentError), 'Path segment count should exist '
+ 'when testing against valid '
+ 'document or attachment names.')
+ else:
+ # get latest call in list
+ url, headers = actual_call_args_list[len(actual_call_args_list) - 1]
+ # there should only be one mocked url
+ self.assertEqual(len(url), 1)
+ # parse path of url and remove first / path segment
+ path = urlparse(url[0]).path[1:]
+ actual_segment_count = len(path.split('/'))
+ self.assertEqual(actual_segment_count, exp_segment_count)
diff --git a/tests/unit/iam_auth_tests.py b/tests/unit/iam_auth_tests.py
index f36af4e0..3e0c7cf2 100644
--- a/tests/unit/iam_auth_tests.py
+++ b/tests/unit/iam_auth_tests.py
@@ -88,12 +88,12 @@ def _mock_cookie(expires_secs=300):
def test_iam_set_credentials(self):
iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984')
- self.assertEquals(iam._api_key, MOCK_API_KEY)
+ self.assertEqual(iam._api_key, MOCK_API_KEY)
new_api_key = 'some_new_api_key'
iam.set_credentials(None, new_api_key)
- self.assertEquals(iam._api_key, new_api_key)
+ self.assertEqual(iam._api_key, new_api_key)
@mock.patch('cloudant._client_session.ClientSession.request')
def test_iam_get_access_token(self, m_req):
diff --git a/tests/unit/index_tests.py b/tests/unit/index_tests.py
index 29a3d51c..f12cf158 100644
--- a/tests/unit/index_tests.py
+++ b/tests/unit/index_tests.py
@@ -161,21 +161,21 @@ def test_create_an_index_using_ddoc_index_name(self):
with DesignDocument(self.db, index.design_document_id) as ddoc:
self.assertIsInstance(ddoc.get_view(index.name), QueryIndexView)
- self.assertEquals(ddoc['_id'], index.design_document_id)
+ self.assertEqual(ddoc['_id'], index.design_document_id)
self.assertTrue(ddoc['_rev'].startswith('1-'))
- self.assertEquals(ddoc['indexes'], {})
- self.assertEquals(ddoc['language'], 'query')
- self.assertEquals(ddoc['lists'], {})
- self.assertEquals(ddoc['shows'], {})
+ self.assertEqual(ddoc['indexes'], {})
+ self.assertEqual(ddoc['language'], 'query')
+ self.assertEqual(ddoc['lists'], {})
+ self.assertEqual(ddoc['shows'], {})
self.assertListEqual(list(ddoc['views'].keys()), ['index001'])
view = ddoc['views'][index.name]
- self.assertEquals(view['map']['fields']['age'], 'asc')
- self.assertEquals(view['map']['fields']['name'], 'asc')
- self.assertEquals(view['options']['def']['fields'], ['name', 'age'])
- self.assertEquals(view['reduce'], '_count')
+ self.assertEqual(view['map']['fields']['age'], 'asc')
+ self.assertEqual(view['map']['fields']['name'], 'asc')
+ self.assertEqual(view['options']['def']['fields'], ['name', 'age'])
+ self.assertEqual(view['reduce'], '_count')
def test_create_an_index_without_ddoc_index_name(self):
"""
@@ -189,21 +189,21 @@ def test_create_an_index_without_ddoc_index_name(self):
with DesignDocument(self.db, index.design_document_id) as ddoc:
self.assertIsInstance(ddoc.get_view(index.name), QueryIndexView)
- self.assertEquals(ddoc['_id'], index.design_document_id)
+ self.assertEqual(ddoc['_id'], index.design_document_id)
self.assertTrue(ddoc['_rev'].startswith('1-'))
- self.assertEquals(ddoc['indexes'], {})
- self.assertEquals(ddoc['language'], 'query')
- self.assertEquals(ddoc['lists'], {})
- self.assertEquals(ddoc['shows'], {})
+ self.assertEqual(ddoc['indexes'], {})
+ self.assertEqual(ddoc['language'], 'query')
+ self.assertEqual(ddoc['lists'], {})
+ self.assertEqual(ddoc['shows'], {})
self.assertListEqual(list(ddoc['views'].keys()), [index.name])
view = ddoc['views'][index.name]
- self.assertEquals(view['map']['fields']['age'], 'asc')
- self.assertEquals(view['map']['fields']['name'], 'asc')
- self.assertEquals(view['options']['def']['fields'], ['name', 'age'])
- self.assertEquals(view['reduce'], '_count')
+ self.assertEqual(view['map']['fields']['age'], 'asc')
+ self.assertEqual(view['map']['fields']['name'], 'asc')
+ self.assertEqual(view['options']['def']['fields'], ['name', 'age'])
+ self.assertEqual(view['reduce'], '_count')
def test_create_an_index_with_empty_ddoc_index_name(self):
"""
@@ -217,21 +217,21 @@ def test_create_an_index_with_empty_ddoc_index_name(self):
with DesignDocument(self.db, index.design_document_id) as ddoc:
self.assertIsInstance(ddoc.get_view(index.name), QueryIndexView)
- self.assertEquals(ddoc['_id'], index.design_document_id)
+ self.assertEqual(ddoc['_id'], index.design_document_id)
self.assertTrue(ddoc['_rev'].startswith('1-'))
- self.assertEquals(ddoc['indexes'], {})
- self.assertEquals(ddoc['language'], 'query')
- self.assertEquals(ddoc['lists'], {})
- self.assertEquals(ddoc['shows'], {})
+ self.assertEqual(ddoc['indexes'], {})
+ self.assertEqual(ddoc['language'], 'query')
+ self.assertEqual(ddoc['lists'], {})
+ self.assertEqual(ddoc['shows'], {})
self.assertListEqual(list(ddoc['views'].keys()), [index.name])
view = ddoc['views'][index.name]
- self.assertEquals(view['map']['fields']['age'], 'asc')
- self.assertEquals(view['map']['fields']['name'], 'asc')
- self.assertEquals(view['options']['def']['fields'], ['name', 'age'])
- self.assertEquals(view['reduce'], '_count')
+ self.assertEqual(view['map']['fields']['age'], 'asc')
+ self.assertEqual(view['map']['fields']['name'], 'asc')
+ self.assertEqual(view['options']['def']['fields'], ['name', 'age'])
+ self.assertEqual(view['reduce'], '_count')
def test_create_an_index_using_design_prefix(self):
"""
@@ -245,21 +245,21 @@ def test_create_an_index_using_design_prefix(self):
with DesignDocument(self.db, index.design_document_id) as ddoc:
self.assertIsInstance(ddoc.get_view(index.name), QueryIndexView)
- self.assertEquals(ddoc['_id'], index.design_document_id)
+ self.assertEqual(ddoc['_id'], index.design_document_id)
self.assertTrue(ddoc['_rev'].startswith('1-'))
- self.assertEquals(ddoc['indexes'], {})
- self.assertEquals(ddoc['language'], 'query')
- self.assertEquals(ddoc['lists'], {})
- self.assertEquals(ddoc['shows'], {})
+ self.assertEqual(ddoc['indexes'], {})
+ self.assertEqual(ddoc['language'], 'query')
+ self.assertEqual(ddoc['lists'], {})
+ self.assertEqual(ddoc['shows'], {})
self.assertListEqual(list(ddoc['views'].keys()), [index.name])
view = ddoc['views'][index.name]
- self.assertEquals(view['map']['fields']['age'], 'asc')
- self.assertEquals(view['map']['fields']['name'], 'asc')
- self.assertEquals(view['options']['def']['fields'], ['name', 'age'])
- self.assertEquals(view['reduce'], '_count')
+ self.assertEqual(view['map']['fields']['age'], 'asc')
+ self.assertEqual(view['map']['fields']['name'], 'asc')
+ self.assertEqual(view['options']['def']['fields'], ['name', 'age'])
+ self.assertEqual(view['reduce'], '_count')
def test_create_uses_custom_encoder(self):
"""
@@ -446,22 +446,22 @@ def test_create_a_search_index_no_kwargs(self):
self.assertEqual(index.design_document_id, '_design/ddoc001')
self.assertEqual(index.name, 'index001')
with DesignDocument(self.db, index.design_document_id) as ddoc:
- self.assertEquals(ddoc['_id'], index.design_document_id)
+ self.assertEqual(ddoc['_id'], index.design_document_id)
self.assertTrue(ddoc['_rev'].startswith('1-'))
- self.assertEquals(ddoc['language'], 'query')
- self.assertEquals(ddoc['lists'], {})
- self.assertEquals(ddoc['shows'], {})
- self.assertEquals(ddoc['views'], {})
+ self.assertEqual(ddoc['language'], 'query')
+ self.assertEqual(ddoc['lists'], {})
+ self.assertEqual(ddoc['shows'], {})
+ self.assertEqual(ddoc['views'], {})
index = ddoc['indexes']['index001']
- self.assertEquals(index['analyzer']['default'], 'keyword')
- self.assertEquals(index['analyzer']['fields']['$default'], 'standard')
- self.assertEquals(index['analyzer']['name'], 'perfield')
- self.assertEquals(index['index']['default_analyzer'], 'keyword')
- self.assertEquals(index['index']['default_field'], {})
- self.assertEquals(index['index']['fields'], 'all_fields')
- self.assertEquals(index['index']['selector'], {})
+ self.assertEqual(index['analyzer']['default'], 'keyword')
+ self.assertEqual(index['analyzer']['fields']['$default'], 'standard')
+ self.assertEqual(index['analyzer']['name'], 'perfield')
+ self.assertEqual(index['index']['default_analyzer'], 'keyword')
+ self.assertEqual(index['index']['default_field'], {})
+ self.assertEqual(index['index']['fields'], 'all_fields')
+ self.assertEqual(index['index']['selector'], {})
self.assertTrue(index['index']['index_array_lengths'])
def test_create_a_search_index_with_kwargs(self):
@@ -480,22 +480,22 @@ def test_create_a_search_index_with_kwargs(self):
self.assertEqual(index.design_document_id, '_design/ddoc001')
self.assertEqual(index.name, 'index001')
with DesignDocument(self.db, index.design_document_id) as ddoc:
- self.assertEquals(ddoc['_id'], index.design_document_id)
+ self.assertEqual(ddoc['_id'], index.design_document_id)
self.assertTrue(ddoc['_rev'].startswith('1-'))
- self.assertEquals(ddoc['language'], 'query')
- self.assertEquals(ddoc['lists'], {})
- self.assertEquals(ddoc['shows'], {})
- self.assertEquals(ddoc['views'], {})
+ self.assertEqual(ddoc['language'], 'query')
+ self.assertEqual(ddoc['lists'], {})
+ self.assertEqual(ddoc['shows'], {})
+ self.assertEqual(ddoc['views'], {})
index = ddoc['indexes']['index001']
- self.assertEquals(index['analyzer']['default'], 'keyword')
- self.assertEquals(index['analyzer']['fields']['$default'], 'german')
- self.assertEquals(index['analyzer']['name'], 'perfield')
- self.assertEquals(index['index']['default_analyzer'], 'keyword')
- self.assertEquals(index['index']['default_field']['analyzer'], 'german')
- self.assertEquals(index['index']['fields'], [{'name': 'name', 'type': 'string'}, {'name': 'age', 'type': 'number'}])
- self.assertEquals(index['index']['selector'], {})
+ self.assertEqual(index['analyzer']['default'], 'keyword')
+ self.assertEqual(index['analyzer']['fields']['$default'], 'german')
+ self.assertEqual(index['analyzer']['name'], 'perfield')
+ self.assertEqual(index['index']['default_analyzer'], 'keyword')
+ self.assertEqual(index['index']['default_field']['analyzer'], 'german')
+ self.assertEqual(index['index']['fields'], [{'name': 'name', 'type': 'string'}, {'name': 'age', 'type': 'number'}])
+ self.assertEqual(index['index']['selector'], {})
self.assertTrue(index['index']['default_field']['enabled'])
self.assertTrue(index['index']['index_array_lengths'])
diff --git a/tests/unit/param_translation_tests.py b/tests/unit/param_translation_tests.py
index 4da0d05d..fda3c002 100644
--- a/tests/unit/param_translation_tests.py
+++ b/tests/unit/param_translation_tests.py
@@ -36,7 +36,7 @@ def test_valid_descending(self):
{'descending': 'true'}
)
self.assertEqual(
- python_to_couch({'descending': False}),
+ python_to_couch({'descending': False}),
{'descending': 'false'}
)
@@ -44,9 +44,9 @@ def test_valid_endkey(self):
"""
Test endkey translation is successful.
"""
- self.assertEqual(python_to_couch({'endkey': 10}), {'endkey': 10})
+ self.assertEqual(python_to_couch({'endkey': 10}), {'endkey': '10'})
# Test with long type
- self.assertEqual(python_to_couch({'endkey': LONG_NUMBER}), {'endkey': LONG_NUMBER})
+ self.assertEqual(python_to_couch({'endkey': LONG_NUMBER}), {'endkey': str(LONG_NUMBER)})
self.assertEqual(
python_to_couch({'endkey': 'foo'}),
{'endkey': '"foo"'}
@@ -55,6 +55,10 @@ def test_valid_endkey(self):
python_to_couch({'endkey': ['foo', 10]}),
{'endkey': '["foo", 10]'}
)
+ self.assertEqual(
+ python_to_couch({'endkey': True}),
+ {'endkey': 'true'}
+ )
def test_valid_endkey_docid(self):
"""
@@ -120,14 +124,18 @@ def test_valid_key(self):
"""
Test key translation is successful.
"""
- self.assertEqual(python_to_couch({'key': 10}), {'key': 10})
+ self.assertEqual(python_to_couch({'key': 10}), {'key': '10'})
# Test with long type
- self.assertEqual(python_to_couch({'key': LONG_NUMBER}), {'key': LONG_NUMBER})
+ self.assertEqual(python_to_couch({'key': LONG_NUMBER}), {'key': str(LONG_NUMBER)})
self.assertEqual(python_to_couch({'key': 'foo'}), {'key': '"foo"'})
self.assertEqual(
python_to_couch({'key': ['foo', 10]}),
{'key': '["foo", 10]'}
)
+ self.assertEqual(
+ python_to_couch({'key': True}),
+ {'key': 'true'}
+ )
def test_valid_keys(self):
"""
@@ -194,9 +202,9 @@ def test_valid_startkey(self):
"""
Test startkey translation is successful.
"""
- self.assertEqual(python_to_couch({'startkey': 10}), {'startkey': 10})
+ self.assertEqual(python_to_couch({'startkey': 10}), {'startkey': '10'})
# Test with long type
- self.assertEqual(python_to_couch({'startkey': LONG_NUMBER}), {'startkey': LONG_NUMBER})
+ self.assertEqual(python_to_couch({'startkey': LONG_NUMBER}), {'startkey': str(LONG_NUMBER)})
self.assertEqual(
python_to_couch({'startkey': 'foo'}),
{'startkey': '"foo"'}
@@ -205,6 +213,10 @@ def test_valid_startkey(self):
python_to_couch({'startkey': ['foo', 10]}),
{'startkey': '["foo", 10]'}
)
+ self.assertEqual(
+ python_to_couch({'startkey': True}),
+ {'startkey': 'true'}
+ )
def test_valid_startkey_docid(self):
"""
@@ -247,7 +259,7 @@ def test_invalid_endkey(self):
"""
msg = 'Argument endkey not instance of expected type:'
with self.assertRaises(CloudantArgumentError) as cm:
- python_to_couch({'endkey': True})
+ python_to_couch({'endkey': {'foo': 'bar'}})
self.assertTrue(str(cm.exception).startswith(msg))
def test_invalid_endkey_docid(self):
@@ -302,7 +314,7 @@ def test_invalid_key(self):
"""
msg = 'Argument key not instance of expected type:'
with self.assertRaises(CloudantArgumentError) as cm:
- python_to_couch({'key': True})
+ python_to_couch({'key': {'foo': 'bar'}})
self.assertTrue(str(cm.exception).startswith(msg))
def test_invalid_keys_not_list(self):
@@ -372,7 +384,7 @@ def test_invalid_startkey(self):
"""
msg = 'Argument startkey not instance of expected type:'
with self.assertRaises(CloudantArgumentError) as cm:
- python_to_couch({'startkey': True})
+ python_to_couch({'startkey': {'foo': 'bar'}})
self.assertTrue(str(cm.exception).startswith(msg))
def test_invalid_startkey_docid(self):
diff --git a/tests/unit/replicator_mock_tests.py b/tests/unit/replicator_mock_tests.py
index 96589f9e..f49b04d4 100644
--- a/tests/unit/replicator_mock_tests.py
+++ b/tests/unit/replicator_mock_tests.py
@@ -86,9 +86,9 @@ def test_using_admin_party_source_and_target(self):
rep.create_replication(src, tgt, repl_id=self.repl_id)
kcall = m_replicator.create_document.call_args_list
- self.assertEquals(len(kcall), 1)
+ self.assertEqual(len(kcall), 1)
args, kwargs = kcall[0]
- self.assertEquals(len(args), 1)
+ self.assertEqual(len(args), 1)
expected_doc = {
'_id': self.repl_id,
@@ -118,9 +118,9 @@ def test_using_basic_auth_source_and_target(self):
src, tgt, repl_id=self.repl_id, user_ctx=self.user_ctx)
kcall = m_replicator.create_document.call_args_list
- self.assertEquals(len(kcall), 1)
+ self.assertEqual(len(kcall), 1)
args, kwargs = kcall[0]
- self.assertEquals(len(args), 1)
+ self.assertEqual(len(args), 1)
expected_doc = {
'_id': self.repl_id,
@@ -154,9 +154,9 @@ def test_using_iam_auth_source_and_target(self):
src, tgt, repl_id=self.repl_id, user_ctx=self.user_ctx)
kcall = m_replicator.create_document.call_args_list
- self.assertEquals(len(kcall), 1)
+ self.assertEqual(len(kcall), 1)
args, kwargs = kcall[0]
- self.assertEquals(len(args), 1)
+ self.assertEqual(len(args), 1)
expected_doc = {
'_id': self.repl_id,
diff --git a/tests/unit/replicator_tests.py b/tests/unit/replicator_tests.py
index 28d78fab..9eb56b56 100644
--- a/tests/unit/replicator_tests.py
+++ b/tests/unit/replicator_tests.py
@@ -322,12 +322,14 @@ def test_retrieve_replication_state(self):
)
self.replication_ids.append(repl_id)
repl_state = None
- valid_states = ['completed', 'error', 'triggered', 'running', None]
+ # note triggered is for versions prior to 2.1
+ valid_states = ['completed', 'error', 'initializing', 'triggered', 'pending', 'running', 'failed', 'crashing', None]
finished = False
+ # Wait for 5 minutes or a terminal replication state
for _ in range(300):
repl_state = self.replicator.replication_state(repl_id)
self.assertTrue(repl_state in valid_states)
- if repl_state in ('error', 'completed'):
+ if repl_state in ('error', 'failed', 'completed'):
finished = True
break
time.sleep(1)
@@ -407,7 +409,8 @@ def test_follow_replication(self):
repl_id
)
self.replication_ids.append(repl_id)
- valid_states = ('completed', 'error', 'triggered', 'running', None)
+ # note triggered is for versions prior to 2.1
+ valid_states = ['completed', 'error', 'initializing', 'triggered', 'pending', 'running', 'failed', 'crashing', None]
repl_states = []
if 'scheduler' in self.client.features():
state_key = 'state'