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 [![Build Status](https://travis-ci.org/cloudant/python-cloudant.svg?branch=master)](https://travis-ci.org/cloudant/python-cloudant) [![Readthedocs](https://readthedocs.org/projects/pip/badge/)](http://python-cloudant.readthedocs.io) -[![Compatibility](https://img.shields.io/badge/python-2.7%2C%203.5-blue.svg)](http://python-cloudant.readthedocs.io/en/latest/compatibility.html) +[![Compatibility](https://img.shields.io/badge/python-3.5-blue.svg)](http://python-cloudant.readthedocs.io/en/latest/compatibility.html) [![pypi](https://img.shields.io/pypi/v/cloudant.svg)](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],