diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 8a9c853a..5b5697c2 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -5,7 +5,7 @@ Thanks for your hard work, please ensure all items are complete before opening.
- [ ] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](../blob/master/DCO1.1.txt)
- [ ] Added tests for code changes _or_ test/build only changes
-- [ ] Updated the change log file (`CHANGES.md`|`CHANGELOG.md`) _or_ test/build only changes
+- [ ] Updated the change log file (`CHANGES.md`) _or_ test/build only changes
- [ ] Completed the PR template below:
## Description
diff --git a/.travis.yml b/.travis.yml
index 8a88780e..9f770aac 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,14 +3,11 @@ sudo: required
language: python
python:
- - "2.7"
- - "3.6"
+ - "3.8"
env:
- - ADMIN_PARTY=true COUCHDB_VERSION=2.1.1
- - ADMIN_PARTY=false COUCHDB_VERSION=2.1.1
- - ADMIN_PARTY=true COUCHDB_VERSION=1.7.1
- - ADMIN_PARTY=false COUCHDB_VERSION=1.7.1
+ - ADMIN_PARTY=true COUCHDB_VERSION=2.3.1
+ - ADMIN_PARTY=false COUCHDB_VERSION=2.3.1
services:
- docker
@@ -30,7 +27,7 @@ before_script:
# command to run tests
script:
- pylint ./src/cloudant
- - nosetests -A 'not db or ((db is "couch" or "couch" in db) and (not couchapi or couchapi <='${COUCHDB_VERSION:0:1}'))' -w ./tests/unit
+ - nosetests -A 'not db or ((db == "couch" or "couch" in db) and (not couchapi or couchapi <='${COUCHDB_VERSION:0:1}'))' -w ./tests/unit
notifications:
email: false
diff --git a/CHANGES.md b/CHANGES.md
index 3ba7cc7f..75402ba5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,6 +1,34 @@
-# Unreleased
+# 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)
- [FIXED] Correctly raise exceptions from `create_database` calls.
+- [FIXED] Fix `DeprecationWarning` from `collections`.
# 2.12.0 (2019-03-28)
@@ -121,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 7501c07e..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 """
@@ -40,7 +41,7 @@ def setupPythonAndTest(pythonVersion, testSuite) {
pip install -r test-requirements.txt
${'simplejson'.equals(testSuite) ? 'pip install simplejson' : ''}
pylint ./src/cloudant
- nosetests -A 'not db or (db is "cloudant" or "cloudant" in db)' -w ./tests/unit --with-xunit
+ nosetests -A 'not db or (db == "cloudant" or "cloudant" in db)' -w ./tests/unit --with-xunit
"""
} finally {
// Load the test results
@@ -61,10 +62,9 @@ stage('Checkout'){
}
stage('Test'){
- def py2 = '2'
def py3 = '3'
def axes = [:]
- [py2, py3].each { version ->
+ [py3].each { version ->
['basic','cookie','iam'].each { auth ->
axes.put("Python${version}-${auth}", {setupPythonAndTest(version, auth)})
}
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 91d3a8d4..3a7d90b0 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.12.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 764be732..c8d4ad26 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -60,9 +60,9 @@
# built documents.
#
# The short X.Y version.
-version = '2.12.1-SNAPSHOT'
+version = '2.15.1-SNAPSHOT'
# The full version, including alpha/beta/rc tags.
-release = '2.12.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 9cd4b65e..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
@@ -284,8 +291,12 @@ when constructing the new ``DesignDocument`` class.
ddoc.add_view('myview','function(doc) { emit(doc.foo, doc.bar); }')
ddoc.save()
-Similarly, to define a partitioned Cloudant Query index you must set the
-``partitioned=True`` optional.
+
+To define a partitioned Cloudant Query index you may set the
+``partitioned=True`` optional, but it is not required as the index will be
+partitioned by default in a partitioned database. Conversely, you must
+set the ``partitioned=False`` optional if you wish to create a global
+(non-partitioned) index in a partitioned database.
.. code-block:: python
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 677df2ed..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.12.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 f7885f49..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.
@@ -19,14 +19,20 @@
import sys
import platform
-from collections import Sequence
import json
from ._2to3 import LONGTYPE, STRTYPE, NONETYPE, UNITYPE, iteritems_
from .error import CloudantArgumentError, CloudantException, CloudantClientException
+try:
+ from collections.abc import Sequence
+except ImportError:
+ from collections import Sequence
+
# Library Constants
+DESIGN_PREFIX = '_design/'
+LOCAL_PREFIX = '_local/'
USER_AGENT = '/'.join([
'python-cloudant',
sys.modules['cloudant'].__version__,
@@ -52,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,),
}
@@ -156,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
@@ -167,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):
@@ -186,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.
@@ -205,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))
@@ -245,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
@@ -293,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 17978468..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
@@ -1100,7 +1118,7 @@ def create_query_index(
design_document_id=None,
index_name=None,
index_type='json',
- partitioned=False,
+ partitioned=None,
**kwargs
):
"""
@@ -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 34594b95..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,12 +45,16 @@ 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:
self.setdefault('options', {'partitioned': True})
+ else:
+ self.setdefault('options', {'partitioned': False})
self._nested_object_names = frozenset(['views', 'indexes', 'lists', 'shows'])
for prop in self._nested_object_names:
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/feed.py b/src/cloudant/feed.py
index ef2c90fc..f038ebfe 100644
--- a/src/cloudant/feed.py
+++ b/src/cloudant/feed.py
@@ -144,7 +144,7 @@ def __next__(self):
"""
Provides Python3 compatibility.
"""
- return self.next()
+ return self.next() # pylint: disable=not-callable
def next(self):
"""
diff --git a/src/cloudant/index.py b/src/cloudant/index.py
index 051c004b..c66c7ac2 100644
--- a/src/cloudant/index.py
+++ b/src/cloudant/index.py
@@ -47,7 +47,7 @@ class Index(object):
:func:`~cloudant.database.CloudantDatabase.create_query_index`.
"""
- def __init__(self, database, design_document_id=None, name=None, partitioned=False, **kwargs):
+ def __init__(self, database, design_document_id=None, name=None, partitioned=None, **kwargs):
self._database = database
self._r_session = self._database.r_session
self._ddoc_id = design_document_id
@@ -154,8 +154,8 @@ def create(self):
self._def_check()
payload['index'] = self._def
- if self._partitioned:
- payload['partitioned'] = True
+ if self._partitioned is not None:
+ payload['partitioned'] = bool(self._partitioned)
headers = {'Content-Type': 'application/json'}
resp = self._r_session.post(
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/test-requirements.txt b/test-requirements.txt
index 88c13ffb..8d95c01d 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -2,5 +2,5 @@ mock==1.3.0
nose
sphinx
sphinx_rtd_theme
-pylint
+pylint==2.5.2
flaky
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 f3ac4e76..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, 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.
@@ -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):
@@ -405,9 +406,10 @@ def test_create_invalid_database_name(self):
"""
dbname = 'invalidDbName_'
self.client.connect()
- with self.assertRaises(CloudantDatabaseException) as cm:
+ with self.assertRaises((CloudantDatabaseException, HTTPError)) as cm:
self.client.create_database(dbname)
- self.assertEqual(cm.exception.status_code, 400)
+ code = cm.exception.status_code if hasattr(cm.exception, 'status_code') else cm.exception.response.status_code
+ self.assertEqual(code, 400)
self.client.disconnect()
@skip_if_not_cookie_auth
@@ -440,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]),
@@ -562,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
@@ -701,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:
@@ -717,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
},
@@ -728,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)))
@@ -743,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
},
@@ -765,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
},
@@ -794,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
},
@@ -807,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)))
@@ -817,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
@@ -834,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
},
@@ -848,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)))
@@ -869,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
@@ -900,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
},
@@ -914,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)))
@@ -933,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
},
@@ -958,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)))
@@ -972,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)
@@ -1412,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 2c595f65..8b3690dd 100644
--- a/tests/unit/database_partition_tests.py
+++ b/tests/unit/database_partition_tests.py
@@ -48,11 +48,22 @@ def test_create_partitioned_design_document(self):
r.raise_for_status()
self.assertTrue(r.json()['options']['partitioned'])
+
+ def test_create_non_partitioned_design_document(self):
+ ddoc_id = 'empty_ddoc'
+
+ ddoc = DesignDocument(self.db, ddoc_id, partitioned=False)
+ ddoc.save()
+
+ r = self.db.r_session.get(ddoc.document_url)
+ r.raise_for_status()
+
+ self.assertFalse(r.json()['options']['partitioned'])
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 + ':'))
@@ -60,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)
@@ -80,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'
@@ -88,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):
@@ -111,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)
@@ -127,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 d57ba654..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-'))
@@ -364,7 +410,7 @@ def test_retrieve_design_document(self):
"""
# Get an empty design document object that does not exist remotely
local_ddoc = self.db.get_design_document('_design/ddoc01')
- self.assertEqual(local_ddoc, {'_id': '_design/ddoc01', 'indexes': {},
+ self.assertEqual(local_ddoc, {'_id': '_design/ddoc01', 'indexes': {}, 'options': {'partitioned': False},
'views': {}, 'lists': {}, 'shows': {}})
# Add the design document to the database
map_func = 'function(doc) {\n emit(doc._id, 1); \n}'
@@ -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 c986e769..86a2b5f1 100644
--- a/tests/unit/design_document_tests.py
+++ b/tests/unit/design_document_tests.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python
-# Copyright (C) 2015, 2018 IBM Corp. All rights reserved.
+# Copyright (C) 2015, 2020 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.
@@ -353,6 +353,7 @@ def test_fetch_map_reduce(self):
self.assertEqual(ddoc_remote, {
'_id': '_design/ddoc001',
'_rev': ddoc['_rev'],
+ 'options': {'partitioned': False},
'lists': {},
'shows': {},
'indexes': {},
@@ -389,7 +390,8 @@ def test_fetch_dbcopy(self):
# before comparison also. Compare the removed values with
# the expected content in each case.
self.assertEqual(db_copy, ddoc['views']['view002'].pop('dbcopy'))
- self.assertEqual({'epi': {'dbcopy': {'view002': db_copy}}}, ddoc_remote.pop('options'))
+ self.assertEqual({'epi': {'dbcopy': {'view002': db_copy}}, 'partitioned': False}, ddoc_remote.pop('options'))
+ self.assertEqual({'partitioned': False}, ddoc.pop('options'))
self.assertEqual(ddoc_remote, ddoc)
self.assertTrue(ddoc_remote['_rev'].startswith('1-'))
self.assertEqual(ddoc_remote, {
@@ -415,7 +417,7 @@ def test_fetch_no_views(self):
ddoc_remote = DesignDocument(self.db, '_design/ddoc001')
ddoc_remote.fetch()
self.assertEqual(set(ddoc_remote.keys()),
- {'_id', '_rev', 'indexes', 'views', 'lists', 'shows'})
+ {'_id', '_rev', 'indexes', 'views', 'options', 'lists', 'shows'})
self.assertEqual(ddoc_remote['_id'], '_design/ddoc001')
self.assertTrue(ddoc_remote['_rev'].startswith('1-'))
self.assertEqual(ddoc_remote['_rev'], ddoc['_rev'])
@@ -432,6 +434,7 @@ def test_fetch_query_views(self):
data = {
'_id': '_design/ddoc001',
'indexes': {},
+ 'options': {'partitioned': False},
'lists': {},
'shows': {},
'language': 'query',
@@ -463,6 +466,7 @@ def test_fetch_text_indexes(self):
data = {
'_id': '_design/ddoc001',
'language': 'query',
+ 'options': {'partitioned': False},
'lists': {},
'shows': {},
'indexes': {'index001':
@@ -499,6 +503,7 @@ def test_fetch_text_indexes_and_query_views(self):
'language': 'query',
'lists': {},
'shows': {},
+ 'options': {'partitioned': False},
'views': {
'view001': {'map': {'fields': {'name': 'asc', 'age': 'asc'}},
'reduce': '_count',
@@ -687,7 +692,7 @@ def test_save_with_no_views(self):
ddoc.save()
# Ensure that locally cached DesignDocument contains an
# empty views dict.
- self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'indexes', 'views', 'lists', 'shows'})
+ self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'indexes', 'options', 'views', 'lists', 'shows'})
self.assertEqual(ddoc['_id'], '_design/ddoc001')
self.assertTrue(ddoc['_rev'].startswith('1-'))
self.assertEqual(ddoc.views, {})
@@ -695,7 +700,7 @@ def test_save_with_no_views(self):
# include a views sub-document.
resp = self.client.r_session.get(ddoc.document_url)
raw_ddoc = response_to_json_dict(resp)
- self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev'})
+ self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev','options'})
self.assertEqual(raw_ddoc['_id'], ddoc['_id'])
self.assertEqual(raw_ddoc['_rev'], ddoc['_rev'])
@@ -776,7 +781,10 @@ def test_get_info(self):
info = ddoc_remote.info()
# Remove variable fields to make equality easier to check
info['view_index'].pop('signature')
- info['view_index'].pop('disk_size')
+ if 'disk_size' in info['view_index']:
+ info['view_index'].pop('disk_size')
+ if 'data_size' in info['view_index']:
+ info['view_index'].pop('data_size')
# Remove Cloudant/Couch 2 fields if present to allow test to pass on Couch 1.6
if 'sizes' in info['view_index']:
info['view_index'].pop('sizes')
@@ -789,8 +797,7 @@ def test_get_info(self):
{'view_index': {'update_seq': 0, 'waiting_clients': 0,
'language': 'javascript',
'purge_seq': 0, 'compact_running': False,
- 'waiting_commit': False, 'updater_running': False,
- 'data_size': 0
+ 'waiting_commit': False, 'updater_running': False
},
'name': name
})
@@ -827,15 +834,18 @@ def test_get_search_info(self):
ddoc_remote = DesignDocument(self.db, '_design/ddoc001')
ddoc_remote.fetch()
+ # Make a request to the search index to ensure it is built
+ self.db.get_search_result('_design/ddoc001', 'search001', query='name:julia*')
+
search_info = ddoc_remote.search_info('search001')
# Check the search index name
self.assertEqual(search_info['name'], '_design/ddoc001/search001', 'The search index name should be correct.')
# 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.')
@@ -856,7 +866,8 @@ def test_get_search_disk_size(self):
ddoc_remote = DesignDocument(self.db, '_design/ddoc001')
ddoc_remote.fetch()
- ddoc_remote.search_info('search001') # trigger index build
+ # Make a request to the search index to ensure it is built
+ self.db.get_search_result('_design/ddoc001', 'search001', query='name:julia*')
search_disk_size = ddoc_remote.search_disk_size('search001')
@@ -1070,6 +1081,7 @@ def test_fetch_search_index(self):
self.assertEqual(ddoc_remote, {
'_id': '_design/ddoc001',
'_rev': ddoc['_rev'],
+ 'options': {'partitioned': False},
'indexes': {
'search001': {'index': search_index},
'search002': {'index': search_index, 'analyzer': 'simple'},
@@ -1094,7 +1106,7 @@ def test_fetch_no_search_index(self):
ddoc_remote = DesignDocument(self.db, '_design/ddoc001')
ddoc_remote.fetch()
self.assertEqual(set(ddoc_remote.keys()),
- {'_id', '_rev', 'indexes', 'views', 'lists', 'shows'})
+ {'_id', '_rev', 'indexes', 'options', 'views', 'lists', 'shows'})
self.assertEqual(ddoc_remote['_id'], '_design/ddoc001')
self.assertTrue(ddoc_remote['_rev'].startswith('1-'))
self.assertEqual(ddoc_remote['_rev'], ddoc['_rev'])
@@ -1177,14 +1189,14 @@ def test_save_with_no_search_indexes(self):
ddoc.save()
# Ensure that locally cached DesignDocument contains an
# empty search indexes and views dict.
- self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'indexes', 'views', 'lists', 'shows'})
+ self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'indexes','options', 'views', 'lists', 'shows'})
self.assertEqual(ddoc['_id'], '_design/ddoc001')
self.assertTrue(ddoc['_rev'].startswith('1-'))
# Ensure that remotely saved design document does not
# include a search indexes sub-document.
resp = self.client.r_session.get(ddoc.document_url)
raw_ddoc = response_to_json_dict(resp)
- self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev'})
+ self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev','options'})
self.assertEqual(raw_ddoc['_id'], ddoc['_id'])
self.assertEqual(raw_ddoc['_rev'], ddoc['_rev'])
@@ -1263,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',
@@ -1407,6 +1419,7 @@ def test_fetch_list_functions(self):
self.assertEqual(ddoc_remote, {
'_id': '_design/ddoc001',
'_rev': ddoc['_rev'],
+ 'options': {'partitioned': False},
'lists': {
'list001': list_func,
'list002': list_func,
@@ -1431,7 +1444,7 @@ def test_fetch_no_list_functions(self):
ddoc_remote = DesignDocument(self.db, '_design/ddoc001')
ddoc_remote.fetch()
self.assertEqual(set(ddoc_remote.keys()),
- {'_id', '_rev', 'indexes', 'views', 'lists', 'shows'})
+ {'_id', '_rev', 'options', 'indexes', 'views', 'lists', 'shows'})
self.assertEqual(ddoc_remote['_id'], '_design/ddoc001')
self.assertTrue(ddoc_remote['_rev'].startswith('1-'))
self.assertEqual(ddoc_remote['_rev'], ddoc['_rev'])
@@ -1446,14 +1459,14 @@ def test_save_with_no_list_functions(self):
ddoc = DesignDocument(self.db, '_design/ddoc001')
ddoc.save()
# Ensure that locally cached DesignDocument contains lists dict
- self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'lists', 'shows', 'indexes', 'views'})
+ self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'lists', 'options', 'shows', 'indexes', 'views'})
self.assertEqual(ddoc['_id'], '_design/ddoc001')
self.assertTrue(ddoc['_rev'].startswith('1-'))
# Ensure that remotely saved design document does not
# include a lists sub-document.
resp = self.client.r_session.get(ddoc.document_url)
raw_ddoc = response_to_json_dict(resp)
- self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev'})
+ self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev','options'})
self.assertEqual(raw_ddoc['_id'], ddoc['_id'])
self.assertEqual(raw_ddoc['_rev'], ddoc['_rev'])
@@ -1541,7 +1554,8 @@ def test_geospatial_index(self):
'indexes': {},
'views': {},
'lists': {},
- 'shows': {}
+ 'shows': {},
+ 'options': {'partitioned': False}
})
# Document with geospatial point
geodoc = Document(self.db, 'doc001')
@@ -1708,6 +1722,7 @@ def test_fetch_show_functions(self):
self.assertEqual(ddoc_remote, {
'_id': '_design/ddoc001',
'_rev': ddoc['_rev'],
+ 'options': {'partitioned': False},
'lists': {},
'shows': {
'show001': show_func,
@@ -1732,7 +1747,7 @@ def test_fetch_no_show_functions(self):
ddoc_remote = DesignDocument(self.db, '_design/ddoc001')
ddoc_remote.fetch()
self.assertEqual(set(ddoc_remote.keys()),
- {'_id', '_rev', 'indexes', 'views', 'lists', 'shows'})
+ {'_id', '_rev', 'indexes', 'options', 'views', 'lists', 'shows'})
self.assertEqual(ddoc_remote['_id'], '_design/ddoc001')
self.assertTrue(ddoc_remote['_rev'].startswith('1-'))
self.assertEqual(ddoc_remote['_rev'], ddoc['_rev'])
@@ -1747,14 +1762,14 @@ def test_save_with_no_show_functions(self):
ddoc = DesignDocument(self.db, '_design/ddoc001')
ddoc.save()
# Ensure that locally cached DesignDocument contains shows dict
- self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'lists', 'shows', 'indexes', 'views'})
+ self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'lists','options', 'shows', 'indexes', 'views'})
self.assertEqual(ddoc['_id'], '_design/ddoc001')
self.assertTrue(ddoc['_rev'].startswith('1-'))
# Ensure that remotely saved design document does not
# include a shows sub-document.
resp = self.client.r_session.get(ddoc.document_url)
raw_ddoc = response_to_json_dict(resp)
- self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev'})
+ self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev','options'})
self.assertEqual(raw_ddoc['_id'], ddoc['_id'])
self.assertEqual(raw_ddoc['_rev'], ddoc['_rev'])
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 59a952d6..f12cf158 100644
--- a/tests/unit/index_tests.py
+++ b/tests/unit/index_tests.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python
-# Copyright (C) 2015, 2018 IBM Corp. All rights reserved.
+# Copyright (C) 2015, 2020 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.
@@ -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):
"""
@@ -390,7 +390,7 @@ def test_index_usage_via_query(self):
self.populate_db_with_documents(100)
result = self.db.get_query_result(fields=['name', 'age'],
selector={'age': {'$eq': 6}}, raw_result=True)
- self.assertTrue(str(result['warning']).startswith("no matching index found"))
+ self.assertTrue(str(result['warning']).lower().startswith("no matching index found"))
@attr(db='cloudant')
class TextIndexTests(UnitTestDbBase):
@@ -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'])
@@ -554,6 +554,21 @@ def test_create_a_search_index_invalid_selector_value(self):
'<{} \'dict\'>'.format('type' if PY2 else 'class')
)
+ def test_create_unpartitioned_query_index(self):
+ """
+ Test that create_query_index works on an unpartitioned database
+ """
+ ddoc = DesignDocument(self.db, document_id="unpartitioned_query_index_ddoc")
+ ddoc["language"] = "query"
+ ddoc.save()
+ index = self.db.create_query_index(
+ design_document_id="_design/unpartitioned_query_index_ddoc",
+ fields=["key"],
+ partitioned=False
+ )
+ index.create()
+ self.assertGreater(len(self.db.get_query_indexes()), 0)
+
def test_search_index_via_query(self):
"""
Test that a created TEXT index will produce expected query results.
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 610d3588..9eb56b56 100644
--- a/tests/unit/replicator_tests.py
+++ b/tests/unit/replicator_tests.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python
-# Copyright (C) 2015, 2018 IBM Corp. All rights reserved.
+# Copyright (C) 2015, 2020 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.
@@ -220,10 +220,13 @@ def test_create_replication(self):
def test_timeout_in_create_replication(self):
"""
Test that a read timeout exception is thrown when creating a
- replicator with a timeout value of 500 ms.
+ replicator with a read timeout value of 5 s.
"""
- # Setup client with a timeout
- self.set_up_client(auto_connect=True, timeout=.5)
+ # Setup client with a read timeout (but the standard connect timeout)
+ # Note that this timeout applies to all connections from this client
+ # setting it too short can cause intermittent failures when responses
+ # are not quick enough. Setting it too long makes the test take longer.
+ self.set_up_client(auto_connect=True, timeout=(30,5))
self.db = self.client[self.test_target_dbname]
self.target_db = self.client[self.test_dbname]
# Construct a replicator with the updated client
@@ -319,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)
@@ -404,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'
diff --git a/tests/unit/unit_t_db_base.py b/tests/unit/unit_t_db_base.py
index 913d7b20..2067af0c 100644
--- a/tests/unit/unit_t_db_base.py
+++ b/tests/unit/unit_t_db_base.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python
-# Copyright (C) 2015, 2019 IBM Corp. All rights reserved.
+# Copyright (C) 2015, 2020 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.
@@ -109,7 +109,7 @@ def setUpClass(cls):
if os.environ.get('DB_USER') is None:
# Get couchdb docker node name
- if os.environ.get('COUCHDB_VERSION') == '2.1.1':
+ if os.environ.get('COUCHDB_VERSION') == '2.3.1':
os.environ['NODENAME'] = requests.get(
'{0}/_membership'.format(os.environ['DB_URL'])).json()['all_nodes'][0]
os.environ['DB_USER_CREATED'] = '1'
@@ -117,7 +117,7 @@ def setUpClass(cls):
unicode_(uuid.uuid4())
)
os.environ['DB_PASSWORD'] = 'password'
- if os.environ.get('COUCHDB_VERSION') == '2.1.1':
+ if os.environ.get('COUCHDB_VERSION') == '2.3.1':
resp = requests.put(
'{0}/_node/{1}/_config/admins/{2}'.format(
os.environ['DB_URL'],
@@ -143,7 +143,7 @@ def tearDownClass(cls):
"""
if (os.environ.get('RUN_CLOUDANT_TESTS') is None and
os.environ.get('DB_USER_CREATED') is not None):
- if os.environ.get('COUCHDB_VERSION') == '2.1.1':
+ if os.environ.get('COUCHDB_VERSION') == '2.3.1':
resp = requests.delete(
'{0}://{1}:{2}@{3}/_node/{4}/_config/admins/{5}'.format(
os.environ['DB_URL'].split('://', 1)[0],