diff --git a/contentstack/asset.py b/contentstack/asset.py index ee09a06..9b48f90 100644 --- a/contentstack/asset.py +++ b/contentstack/asset.py @@ -17,6 +17,7 @@ def __init__(self, http_instance, uid=None, logger=None): self.__uid = uid if self.__uid is None or self.__uid.strip() == 0: raise KeyError(ErrorMessages.INVALID_UID) + self.uid = uid self.base_url = f'{self.http_instance.endpoint}/assets/{self.__uid}' if 'environment' in self.http_instance.headers: self.asset_params['environment'] = self.http_instance.headers['environment'] diff --git a/contentstack/entry.py b/contentstack/entry.py index 06c5faa..3c20edf 100644 --- a/contentstack/entry.py +++ b/contentstack/entry.py @@ -54,6 +54,25 @@ def environment(self, environment): self.http_instance.headers['environment'] = environment return self + def remove_environment(self): + """Removes environment from the request headers + :return: Entry, so we can chain the call + ------------------------------- + Example:: + + >>> import contentstack + >>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment') + >>> content_type = stack.content_type('content_type_uid') + >>> entry = content_type.entry(uid='entry_uid') + >>> entry = entry.environment('test') + >>> entry = entry.remove_environment() + >>> result = entry.fetch() + ------------------------------- + """ + if 'environment' in self.http_instance.headers: + self.http_instance.headers.pop('environment') + return self + def version(self, version): """When no version is specified, it returns the latest version To retrieve a specific version, specify the version number under this parameter. @@ -96,6 +115,9 @@ def param(self, key, value): """ if None in (key, value) and not isinstance(key, str): raise ValueError(ErrorMessages.INVALID_KEY_VALUE_ARGS) + # Convert non-string values to strings + if not isinstance(value, str): + value = str(value) self.entry_param[key] = value return self diff --git a/tests/test_assets.py b/tests/test_assets.py index 3492247..f2ae65c 100644 --- a/tests/test_assets.py +++ b/tests/test_assets.py @@ -211,5 +211,284 @@ def test_25_include_metadata(self): self.assertTrue( self.asset_query.asset_query_params.__contains__('include_metadata')) + def test_26_where_with_include_count_and_pagination(self): + """Test combination of where, include_count, skip, and limit for assets""" + query = (self.asset_query + .where("title", QueryOperation.EQUALS, fields=IMAGE) + .include_count() + .skip(2) + .limit(5)) + self.assertEqual({"title": IMAGE}, query.parameters) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("2", query.query_params["skip"]) + self.assertEqual("5", query.query_params["limit"]) + + def test_27_where_with_order_by_and_pagination(self): + """Test combination of where, order_by, skip, and limit for assets""" + query = (self.asset_query + .where("file_size", QueryOperation.IS_GREATER_THAN, fields=1000) + .order_by_ascending("file_size") + .skip(0) + .limit(10)) + self.assertEqual({"file_size": {"$gt": 1000}}, query.parameters) + self.assertEqual("file_size", query.query_params["asc"]) + self.assertEqual("0", query.query_params["skip"]) + self.assertEqual("10", query.query_params["limit"]) + + def test_28_multiple_where_conditions_with_all_base_methods(self): + """Test multiple where conditions combined with all BaseQuery methods for assets""" + query = (self.asset_query + .where("title", QueryOperation.EQUALS, fields=IMAGE) + .where("file_size", QueryOperation.IS_LESS_THAN, fields=10000) + .where("content_type", QueryOperation.INCLUDES, fields=["image/jpeg", "image/png"]) + .include_count() + .skip(5) + .limit(20) + .order_by_descending("created_at") + .param("locale", "en-us")) + + # Verify parameters + self.assertEqual(3, len(query.parameters)) + self.assertEqual(IMAGE, query.parameters["title"]) + self.assertEqual({"$lt": 10000}, query.parameters["file_size"]) + self.assertEqual({"$in": ["image/jpeg", "image/png"]}, query.parameters["content_type"]) + + # Verify query_params + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("5", query.query_params["skip"]) + self.assertEqual("20", query.query_params["limit"]) + self.assertEqual("created_at", query.query_params["desc"]) + self.assertEqual("en-us", query.query_params["locale"]) + + def test_29_where_with_all_query_operations_combined(self): + """Test where with all QueryOperation types combined for assets""" + query = (self.asset_query + .where("title", QueryOperation.EQUALS, fields=IMAGE) + .where("file_size", QueryOperation.NOT_EQUALS, fields=0) + .where("tags", QueryOperation.INCLUDES, fields=["tag1"]) + .where("excluded", QueryOperation.EXCLUDES, fields=["tag2"]) + .where("min_size", QueryOperation.IS_GREATER_THAN, fields=100) + .where("max_size", QueryOperation.IS_LESS_THAN, fields=1000000) + .where("width", QueryOperation.IS_GREATER_THAN_OR_EQUAL, fields=100) + .where("height", QueryOperation.IS_LESS_THAN_OR_EQUAL, fields=2000) + .where("has_metadata", QueryOperation.EXISTS, fields=True) + .where("filename", QueryOperation.MATCHES, fields=".*\\.jpg$")) + + self.assertEqual(10, len(query.parameters)) + self.assertEqual(IMAGE, query.parameters["title"]) + self.assertEqual({"$ne": 0}, query.parameters["file_size"]) + self.assertEqual({"$in": ["tag1"]}, query.parameters["tags"]) + self.assertEqual({"$nin": ["tag2"]}, query.parameters["excluded"]) + self.assertEqual({"$gt": 100}, query.parameters["min_size"]) + self.assertEqual({"$lt": 1000000}, query.parameters["max_size"]) + self.assertEqual({"$gte": 100}, query.parameters["width"]) + self.assertEqual({"$lte": 2000}, query.parameters["height"]) + self.assertEqual({"$exists": True}, query.parameters["has_metadata"]) + self.assertEqual({"$regex": ".*\\.jpg$"}, query.parameters["filename"]) + + def test_30_asset_specific_methods_with_base_query_methods(self): + """Test AssetQuery specific methods combined with BaseQuery methods""" + query = (self.asset_query + .where("title", QueryOperation.EQUALS, fields=IMAGE) + .environment("dev") + .version("1") + .include_dimension() + .relative_url() + .include_count() + .skip(0) + .limit(10) + .order_by_ascending("title")) + + self.assertEqual({"title": IMAGE}, query.parameters) + self.assertEqual("dev", query.http_instance.headers["environment"]) + self.assertEqual("1", query.asset_query_params["version"]) + self.assertEqual("true", query.asset_query_params["include_dimension"]) + self.assertEqual("true", query.asset_query_params["relative_urls"]) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("0", query.query_params["skip"]) + self.assertEqual("10", query.query_params["limit"]) + self.assertEqual("title", query.query_params["asc"]) + + def test_31_include_fallback_with_where_and_base_methods(self): + """Test include_fallback combined with where and BaseQuery methods""" + query = (self.asset_query + .where("title", QueryOperation.EQUALS, fields=IMAGE) + .include_fallback() + .include_count() + .skip(5) + .limit(15) + .order_by_ascending("title")) + + self.assertEqual({"title": IMAGE}, query.parameters) + self.assertEqual("true", query.asset_query_params["include_fallback"]) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("5", query.query_params["skip"]) + self.assertEqual("15", query.query_params["limit"]) + self.assertEqual("title", query.query_params["asc"]) + + def test_32_include_metadata_with_where_and_base_methods(self): + """Test include_metadata combined with where and BaseQuery methods""" + query = (self.asset_query + .where("file_size", QueryOperation.IS_GREATER_THAN, fields=1000) + .include_metadata() + .include_count() + .skip(10) + .limit(20) + .order_by_descending("file_size")) + + self.assertEqual({"file_size": {"$gt": 1000}}, query.parameters) + self.assertEqual("true", query.asset_query_params["include_metadata"]) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("10", query.query_params["skip"]) + self.assertEqual("20", query.query_params["limit"]) + self.assertEqual("file_size", query.query_params["desc"]) + + def test_33_locale_with_where_and_pagination(self): + """Test locale combined with where and pagination for assets""" + query = (self.asset_query + .locale('en-us') + .where("title", QueryOperation.EQUALS, fields=IMAGE) + .include_count() + .skip(0) + .limit(10)) + + self.assertEqual("en-us", query.asset_query_params["locale"]) + self.assertEqual({"title": IMAGE}, query.parameters) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("0", query.query_params["skip"]) + self.assertEqual("10", query.query_params["limit"]) + + def test_34_include_branch_with_where_and_base_methods(self): + """Test include_branch combined with where and BaseQuery methods""" + query = (self.asset_query + .where("title", QueryOperation.INCLUDES, fields=[IMAGE, "other.jpg"]) + .include_branch() + .include_count() + .skip(0) + .limit(10)) + + self.assertEqual({"title": {"$in": [IMAGE, "other.jpg"]}}, query.parameters) + self.assertEqual("true", query.asset_query_params["include_branch"]) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("0", query.query_params["skip"]) + self.assertEqual("10", query.query_params["limit"]) + + def test_35_complex_combination_all_asset_and_base_methods(self): + """Test complex combination of all AssetQuery and BaseQuery methods""" + query = (self.asset_query + .where("title", QueryOperation.EQUALS, fields=IMAGE) + .where("file_size", QueryOperation.IS_GREATER_THAN, fields=1000) + .where("content_type", QueryOperation.INCLUDES, fields=["image/jpeg", "image/png"]) + .environment("production") + .version("2") + .include_dimension() + .relative_url() + .include_fallback() + .include_metadata() + .include_branch() + .locale("en-us") + .include_count() + .skip(10) + .limit(50) + .order_by_descending("created_at") + .param("custom_param", "custom_value")) + + # Verify parameters + self.assertEqual(3, len(query.parameters)) + self.assertEqual(IMAGE, query.parameters["title"]) + self.assertEqual({"$gt": 1000}, query.parameters["file_size"]) + self.assertEqual({"$in": ["image/jpeg", "image/png"]}, query.parameters["content_type"]) + + # Verify asset_query_params + self.assertEqual("production", query.http_instance.headers["environment"]) + self.assertEqual("2", query.asset_query_params["version"]) + self.assertEqual("true", query.asset_query_params["include_dimension"]) + self.assertEqual("true", query.asset_query_params["relative_urls"]) + self.assertEqual("true", query.asset_query_params["include_fallback"]) + self.assertEqual("true", query.asset_query_params["include_metadata"]) + self.assertEqual("true", query.asset_query_params["include_branch"]) + self.assertEqual("en-us", query.asset_query_params["locale"]) + + # Verify query_params + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("10", query.query_params["skip"]) + self.assertEqual("50", query.query_params["limit"]) + self.assertEqual("created_at", query.query_params["desc"]) + self.assertEqual("custom_value", query.query_params["custom_param"]) + + def test_36_add_params_with_where_and_other_methods(self): + """Test add_params combined with where and other methods for assets""" + query = (self.asset_query + .where("title", QueryOperation.EQUALS, fields=IMAGE) + .add_params({"locale": "en-us", "include_count": "true"}) + .skip(5) + .limit(10)) + + self.assertEqual({"title": IMAGE}, query.parameters) + self.assertEqual("en-us", query.query_params["locale"]) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("5", query.query_params["skip"]) + self.assertEqual("10", query.query_params["limit"]) + + def test_37_remove_param_after_combination(self): + """Test remove_param after building a complex asset query""" + query = (self.asset_query + .where("title", QueryOperation.EQUALS, fields=IMAGE) + .include_count() + .skip(10) + .limit(20) + .param("key1", "value1") + .param("key2", "value2") + .remove_param("key1")) + + self.assertEqual({"title": IMAGE}, query.parameters) + self.assertNotIn("key1", query.query_params) + self.assertEqual("value2", query.query_params["key2"]) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("10", query.query_params["skip"]) + self.assertEqual("20", query.query_params["limit"]) + + def test_38_order_by_ascending_then_descending_coexist(self): + """Test that order_by_ascending and order_by_descending can coexist for assets""" + query = (self.asset_query + .where("title", QueryOperation.EQUALS, fields=IMAGE) + .order_by_ascending("title") + .order_by_descending("file_size")) + + self.assertEqual({"title": IMAGE}, query.parameters) + # Both asc and desc can coexist (they use different keys) + self.assertEqual("title", query.query_params["asc"]) + self.assertEqual("file_size", query.query_params["desc"]) + + def test_39_multiple_where_conditions_with_complex_operations(self): + """Test multiple where conditions with complex operations and all BaseQuery methods for assets""" + query = (self.asset_query + .where("title", QueryOperation.EQUALS, fields=IMAGE) + .where("file_size", QueryOperation.IS_GREATER_THAN, fields=1000) + .where("file_size", QueryOperation.IS_LESS_THAN, fields=100000) + .where("tags", QueryOperation.INCLUDES, fields=["image", "photo"]) + .where("excluded_tags", QueryOperation.EXCLUDES, fields=["archive"]) + .include_count() + .skip(0) + .limit(100) + .order_by_descending("file_size") + .param("locale", "en-us") + .include_fallback()) + + # Verify all where conditions are present + self.assertEqual(IMAGE, query.parameters["title"]) + # Note: file_size is overwritten by the second where call - last call wins + self.assertEqual({"$lt": 100000}, query.parameters["file_size"]) + self.assertEqual({"$in": ["image", "photo"]}, query.parameters["tags"]) + self.assertEqual({"$nin": ["archive"]}, query.parameters["excluded_tags"]) + + # Verify all query_params + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("0", query.query_params["skip"]) + self.assertEqual("100", query.query_params["limit"]) + self.assertEqual("file_size", query.query_params["desc"]) + self.assertEqual("en-us", query.query_params["locale"]) + self.assertEqual("true", query.asset_query_params["include_fallback"]) + # if __name__ == '__main__': # unittest.main() diff --git a/tests/test_basequery.py b/tests/test_basequery.py new file mode 100644 index 0000000..8c561ae --- /dev/null +++ b/tests/test_basequery.py @@ -0,0 +1,410 @@ +""" +Comprehensive unit tests for BaseQuery class with combination tests +""" +import unittest +import logging +from contentstack.basequery import BaseQuery, QueryOperation + + +class TestBaseQuery(unittest.TestCase): + """Test cases for BaseQuery class""" + + def setUp(self): + """Set up test fixtures""" + self.query = BaseQuery() + + # ========== Individual Method Tests ========== + + def test_01_where_equals_single_value(self): + """Test where method with EQUALS operation and single value""" + query = self.query.where("title", QueryOperation.EQUALS, fields="Apple") + self.assertEqual({"title": "Apple"}, query.parameters) + + def test_02_where_equals_list_single_item(self): + """Test where method with EQUALS operation and list with single item""" + query = self.query.where("title", QueryOperation.EQUALS, fields=["Apple"]) + self.assertEqual({"title": "Apple"}, query.parameters) + + def test_03_where_not_equals(self): + """Test where method with NOT_EQUALS operation""" + query = self.query.where("price", QueryOperation.NOT_EQUALS, fields=100) + self.assertEqual({"price": {"$ne": 100}}, query.parameters) + + def test_04_where_includes(self): + """Test where method with INCLUDES operation""" + query = self.query.where("tags", QueryOperation.INCLUDES, fields=["tag1", "tag2"]) + self.assertEqual({"tags": {"$in": ["tag1", "tag2"]}}, query.parameters) + + def test_05_where_excludes(self): + """Test where method with EXCLUDES operation""" + query = self.query.where("tags", QueryOperation.EXCLUDES, fields=["tag1", "tag2"]) + self.assertEqual({"tags": {"$nin": ["tag1", "tag2"]}}, query.parameters) + + def test_06_where_is_less_than(self): + """Test where method with IS_LESS_THAN operation""" + query = self.query.where("price", QueryOperation.IS_LESS_THAN, fields=100) + self.assertEqual({"price": {"$lt": 100}}, query.parameters) + + def test_07_where_is_less_than_or_equal(self): + """Test where method with IS_LESS_THAN_OR_EQUAL operation""" + query = self.query.where("price", QueryOperation.IS_LESS_THAN_OR_EQUAL, fields=100) + self.assertEqual({"price": {"$lte": 100}}, query.parameters) + + def test_08_where_is_greater_than(self): + """Test where method with IS_GREATER_THAN operation""" + query = self.query.where("price", QueryOperation.IS_GREATER_THAN, fields=100) + self.assertEqual({"price": {"$gt": 100}}, query.parameters) + + def test_09_where_is_greater_than_or_equal(self): + """Test where method with IS_GREATER_THAN_OR_EQUAL operation""" + query = self.query.where("price", QueryOperation.IS_GREATER_THAN_OR_EQUAL, fields=100) + self.assertEqual({"price": {"$gte": 100}}, query.parameters) + + def test_10_where_exists(self): + """Test where method with EXISTS operation""" + query = self.query.where("field", QueryOperation.EXISTS, fields=True) + self.assertEqual({"field": {"$exists": True}}, query.parameters) + + def test_11_where_matches(self): + """Test where method with MATCHES operation""" + query = self.query.where("title", QueryOperation.MATCHES, fields="pattern") + self.assertEqual({"title": {"$regex": "pattern"}}, query.parameters) + + def test_12_where_multiple_conditions(self): + """Test multiple where conditions""" + query = (self.query + .where("title", QueryOperation.EQUALS, fields="Apple") + .where("price", QueryOperation.IS_GREATER_THAN, fields=100)) + self.assertEqual({"title": "Apple", "price": {"$gt": 100}}, query.parameters) + + def test_13_include_count(self): + """Test include_count method""" + query = self.query.include_count() + self.assertEqual("true", query.query_params["include_count"]) + + def test_14_skip(self): + """Test skip method""" + query = self.query.skip(10) + self.assertEqual("10", query.query_params["skip"]) + + def test_15_limit(self): + """Test limit method""" + query = self.query.limit(20) + self.assertEqual("20", query.query_params["limit"]) + + def test_16_order_by_ascending(self): + """Test order_by_ascending method""" + query = self.query.order_by_ascending("title") + self.assertEqual("title", query.query_params["asc"]) + + def test_17_order_by_descending(self): + """Test order_by_descending method""" + query = self.query.order_by_descending("price") + self.assertEqual("price", query.query_params["desc"]) + + def test_18_param(self): + """Test param method""" + query = self.query.param("key1", "value1") + self.assertEqual("value1", query.query_params["key1"]) + + def test_19_add_params(self): + """Test add_params method""" + query = self.query.add_params({"key1": "value1", "key2": "value2"}) + self.assertEqual("value1", query.query_params["key1"]) + self.assertEqual("value2", query.query_params["key2"]) + + def test_20_query(self): + """Test query method""" + query = self.query.query("field1", "value1") + self.assertEqual("value1", query.parameters["field1"]) + + def test_21_remove_param(self): + """Test remove_param method""" + query = self.query.param("key1", "value1").remove_param("key1") + self.assertNotIn("key1", query.query_params) + + def test_22_remove_param_nonexistent(self): + """Test remove_param with non-existent key""" + query = self.query.remove_param("nonexistent") + self.assertEqual({}, query.query_params) + + # ========== Combination Tests ========== + + def test_23_where_with_include_count(self): + """Test combination of where and include_count""" + query = (self.query + .where("title", QueryOperation.EQUALS, fields="Apple") + .include_count()) + self.assertEqual({"title": "Apple"}, query.parameters) + self.assertEqual("true", query.query_params["include_count"]) + + def test_24_where_with_skip_and_limit(self): + """Test combination of where, skip, and limit""" + query = (self.query + .where("price", QueryOperation.IS_GREATER_THAN, fields=100) + .skip(5) + .limit(10)) + self.assertEqual({"price": {"$gt": 100}}, query.parameters) + self.assertEqual("5", query.query_params["skip"]) + self.assertEqual("10", query.query_params["limit"]) + + def test_25_where_with_order_by_ascending(self): + """Test combination of where and order_by_ascending""" + query = (self.query + .where("category", QueryOperation.INCLUDES, fields=["electronics"]) + .order_by_ascending("price")) + self.assertEqual({"category": {"$in": ["electronics"]}}, query.parameters) + self.assertEqual("price", query.query_params["asc"]) + + def test_26_where_with_order_by_descending(self): + """Test combination of where and order_by_descending""" + query = (self.query + .where("status", QueryOperation.EQUALS, fields="active") + .order_by_descending("created_at")) + self.assertEqual({"status": "active"}, query.parameters) + self.assertEqual("created_at", query.query_params["desc"]) + + def test_27_include_count_with_skip_limit(self): + """Test combination of include_count, skip, and limit""" + query = (self.query + .include_count() + .skip(10) + .limit(25)) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("10", query.query_params["skip"]) + self.assertEqual("25", query.query_params["limit"]) + + def test_28_where_with_param(self): + """Test combination of where and param""" + query = (self.query + .where("title", QueryOperation.EQUALS, fields="Apple") + .param("locale", "en-us")) + self.assertEqual({"title": "Apple"}, query.parameters) + self.assertEqual("en-us", query.query_params["locale"]) + + def test_29_where_with_add_params(self): + """Test combination of where and add_params""" + query = (self.query + .where("price", QueryOperation.IS_LESS_THAN, fields=1000) + .add_params({"locale": "en-us", "include_count": "true"})) + self.assertEqual({"price": {"$lt": 1000}}, query.parameters) + self.assertEqual("en-us", query.query_params["locale"]) + self.assertEqual("true", query.query_params["include_count"]) + + def test_30_where_with_query_method(self): + """Test combination of where and query""" + query = (self.query + .where("title", QueryOperation.EQUALS, fields="Apple") + .query("field1", "value1")) + self.assertEqual({"title": "Apple", "field1": "value1"}, query.parameters) + + def test_31_complex_combination_all_methods(self): + """Test complex combination of all BaseQuery methods""" + query = (self.query + .where("title", QueryOperation.EQUALS, fields="Apple") + .where("price", QueryOperation.IS_GREATER_THAN, fields=100) + .include_count() + .skip(5) + .limit(20) + .order_by_ascending("price") + .param("locale", "en-us") + .query("category", "electronics")) + + # Verify parameters + self.assertEqual({"title": "Apple", "price": {"$gt": 100}, "category": "electronics"}, + query.parameters) + + # Verify query_params + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("5", query.query_params["skip"]) + self.assertEqual("20", query.query_params["limit"]) + self.assertEqual("price", query.query_params["asc"]) + self.assertEqual("en-us", query.query_params["locale"]) + + def test_32_multiple_where_conditions_all_operations(self): + """Test multiple where conditions with all query operations""" + query = (self.query + .where("title", QueryOperation.EQUALS, fields="Apple") + .where("price", QueryOperation.NOT_EQUALS, fields=0) + .where("tags", QueryOperation.INCLUDES, fields=["tag1", "tag2"]) + .where("excluded_tags", QueryOperation.EXCLUDES, fields=["tag3"]) + .where("min_price", QueryOperation.IS_GREATER_THAN, fields=10) + .where("max_price", QueryOperation.IS_LESS_THAN, fields=1000) + .where("age", QueryOperation.IS_GREATER_THAN_OR_EQUAL, fields=18) + .where("score", QueryOperation.IS_LESS_THAN_OR_EQUAL, fields=100) + .where("field_exists", QueryOperation.EXISTS, fields=True) + .where("pattern", QueryOperation.MATCHES, fields="regex.*")) + + expected_params = { + "title": "Apple", + "price": {"$ne": 0}, + "tags": {"$in": ["tag1", "tag2"]}, + "excluded_tags": {"$nin": ["tag3"]}, + "min_price": {"$gt": 10}, + "max_price": {"$lt": 1000}, + "age": {"$gte": 18}, + "score": {"$lte": 100}, + "field_exists": {"$exists": True}, + "pattern": {"$regex": "regex.*"} + } + self.assertEqual(expected_params, query.parameters) + + def test_33_remove_param_after_combination(self): + """Test remove_param after building a complex query""" + query = (self.query + .include_count() + .skip(10) + .limit(20) + .param("key1", "value1") + .param("key2", "value2") + .remove_param("key1")) + + self.assertNotIn("key1", query.query_params) + self.assertEqual("value2", query.query_params["key2"]) + self.assertEqual("10", query.query_params["skip"]) + self.assertEqual("20", query.query_params["limit"]) + + def test_34_order_by_ascending_and_descending_coexist(self): + """Test that order_by_ascending and order_by_descending can coexist""" + query = (self.query + .order_by_ascending("title") + .order_by_descending("price")) + + # Both asc and desc can coexist (they use different keys) + self.assertEqual("title", query.query_params["asc"]) + self.assertEqual("price", query.query_params["desc"]) + + def test_35_multiple_params_and_add_params(self): + """Test combination of param and add_params""" + query = (self.query + .param("key1", "value1") + .param("key2", "value2") + .add_params({"key3": "value3", "key4": "value4"})) + + self.assertEqual("value1", query.query_params["key1"]) + self.assertEqual("value2", query.query_params["key2"]) + self.assertEqual("value3", query.query_params["key3"]) + self.assertEqual("value4", query.query_params["key4"]) + + def test_36_where_with_all_query_operations_and_pagination(self): + """Test where with all operations combined with pagination""" + query = (self.query + .where("title", QueryOperation.EQUALS, fields="Test") + .where("price", QueryOperation.IS_GREATER_THAN, fields=50) + .where("tags", QueryOperation.INCLUDES, fields=["tag1"]) + .include_count() + .skip(0) + .limit(50) + .order_by_descending("created_at")) + + self.assertEqual(3, len(query.parameters)) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("0", query.query_params["skip"]) + self.assertEqual("50", query.query_params["limit"]) + self.assertEqual("created_at", query.query_params["desc"]) + + # ========== Edge Cases and Error Handling ========== + + def test_37_where_with_none_field_uid(self): + """Test where with None field_uid (should not add to parameters)""" + query = self.query.where(None, QueryOperation.EQUALS, fields="value") + self.assertEqual({}, query.parameters) + + def test_38_where_with_none_operation(self): + """Test where with None operation (should not add to parameters)""" + query = self.query.where("field", None, fields="value") + self.assertEqual({}, query.parameters) + + def test_39_param_with_none_key_raises_error(self): + """Test param with None key raises KeyError""" + with self.assertRaises(KeyError): + self.query.param(None, "value") + + def test_40_param_with_none_value_raises_error(self): + """Test param with None value raises KeyError""" + with self.assertRaises(KeyError): + self.query.param("key", None) + + def test_41_query_with_none_key_raises_error(self): + """Test query with None key raises KeyError""" + with self.assertRaises(KeyError): + self.query.query(None, "value") + + def test_42_query_with_none_value_raises_error(self): + """Test query with None value raises KeyError""" + with self.assertRaises(KeyError): + self.query.query("key", None) + + def test_43_remove_param_with_none_raises_error(self): + """Test remove_param with None key raises ValueError""" + with self.assertRaises(ValueError): + self.query.remove_param(None) + + def test_44_where_equals_with_empty_list(self): + """Test where EQUALS with empty list""" + query = self.query.where("field", QueryOperation.EQUALS, fields=[]) + self.assertEqual({"field": []}, query.parameters) + + def test_45_where_equals_with_list_multiple_items(self): + """Test where EQUALS with list containing multiple items""" + query = self.query.where("field", QueryOperation.EQUALS, fields=["item1", "item2"]) + self.assertEqual({"field": ["item1", "item2"]}, query.parameters) + + def test_46_skip_with_zero(self): + """Test skip with zero value""" + query = self.query.skip(0) + self.assertEqual("0", query.query_params["skip"]) + + def test_47_limit_with_zero(self): + """Test limit with zero value""" + query = self.query.limit(0) + self.assertEqual("0", query.query_params["limit"]) + + def test_48_limit_with_large_number(self): + """Test limit with large number""" + query = self.query.limit(10000) + self.assertEqual("10000", query.query_params["limit"]) + + def test_49_add_params_overwrites_existing(self): + """Test that add_params overwrites existing params""" + query = (self.query + .param("key1", "value1") + .add_params({"key1": "new_value1", "key2": "value2"})) + + self.assertEqual("new_value1", query.query_params["key1"]) + self.assertEqual("value2", query.query_params["key2"]) + + def test_50_query_overwrites_existing_parameter(self): + """Test that query overwrites existing parameter""" + query = (self.query + .query("field1", "value1") + .query("field1", "value2")) + + self.assertEqual("value2", query.parameters["field1"]) + + def test_51_where_overwrites_existing_parameter(self): + """Test that where overwrites existing parameter""" + query = (self.query + .where("title", QueryOperation.EQUALS, fields="Apple") + .where("title", QueryOperation.EQUALS, fields="Orange")) + + self.assertEqual("Orange", query.parameters["title"]) + + def test_52_chaining_returns_self(self): + """Test that all methods return self for chaining""" + query = self.query + self.assertIs(query, query.where("field", QueryOperation.EQUALS, fields="value")) + self.assertIs(query, query.include_count()) + self.assertIs(query, query.skip(10)) + self.assertIs(query, query.limit(20)) + self.assertIs(query, query.order_by_ascending("field")) + self.assertIs(query, query.order_by_descending("field")) + self.assertIs(query, query.param("key", "value")) + self.assertIs(query, query.add_params({})) + self.assertIs(query, query.query("key", "value")) + self.assertIs(query, query.remove_param("key")) + + +if __name__ == '__main__': + unittest.main() + diff --git a/tests/test_entry.py b/tests/test_entry.py index e910a58..e2f2462 100644 --- a/tests/test_entry.py +++ b/tests/test_entry.py @@ -154,9 +154,215 @@ def test_25_content_type_entry_variants_with_has_hash_variant(self): content_type = self.stack.content_type('faq').entry(FAQ_UID) entry = content_type.variants([VARIANT_UID]).fetch() self.assertIn('variants', entry['entry']['publish_details']) - - + # ========== Additional Test Cases ========== + + def test_26_entry_method_chaining_locale_version(self): + """Test entry method chaining with locale and version""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .locale('en-us') + .version(1)) + entry.fetch() + self.assertEqual('en-us', entry.entry_queryable_param['locale']) + self.assertEqual(1, entry.entry_param['version']) + + def test_27_entry_method_chaining_environment_locale(self): + """Test entry method chaining with environment and locale""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .environment('test') + .locale('en-us')) + entry.fetch() + self.assertEqual('test', entry.http_instance.headers['environment']) + self.assertEqual('en-us', entry.entry_queryable_param['locale']) + + def test_28_entry_only_multiple_fields(self): + """Test entry only with multiple field calls""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .only('field1') + .only('field2')) + entry.fetch() + # Note: Multiple only calls may overwrite or append + self.assertIn('only[BASE][]', entry.entry_param) + + def test_29_entry_excepts_multiple_fields(self): + """Test entry excepts with multiple field calls""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .excepts('field1') + .excepts('field2')) + entry.fetch() + # Note: Multiple excepts calls may overwrite or append + self.assertIn('except[BASE][]', entry.entry_param) + + def test_30_entry_include_fallback_with_locale(self): + """Test entry include_fallback combined with locale""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .locale('en-gb') + .include_fallback()) + entry.fetch() + self.assertEqual('en-gb', entry.entry_queryable_param['locale']) + self.assertIn('include_fallback', entry.entry_param) + + def test_31_entry_include_metadata_with_version(self): + """Test entry include_metadata combined with version""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .version(2) + .include_metadata()) + entry.fetch() + self.assertEqual(2, entry.entry_param['version']) + self.assertEqual('true', entry.entry_queryable_param['include_metadata']) + + def test_32_entry_include_content_type_with_locale(self): + """Test entry include_content_type combined with locale""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .locale('en-us') + .include_content_type()) + entry.fetch() + self.assertEqual('en-us', entry.entry_queryable_param['locale']) + self.assertIn('include_content_type', entry.entry_queryable_param) + + def test_33_entry_include_reference_content_type_uid_with_version(self): + """Test entry include_reference_content_type_uid combined with version""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .version(1) + .include_reference_content_type_uid()) + entry.fetch() + self.assertEqual(1, entry.entry_param['version']) + self.assertIn('include_reference_content_type_uid', entry.entry_queryable_param) + + def test_34_entry_add_param_multiple_times(self): + """Test entry add_param called multiple times""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .add_param('key1', 'value1') + .add_param('key2', 'value2')) + entry.fetch() + self.assertEqual('value1', entry.entry_queryable_param['key1']) + self.assertEqual('value2', entry.entry_queryable_param['key2']) + + def test_35_entry_complex_method_chaining(self): + """Test entry with complex method chaining""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .environment('test') + .locale('en-us') + .version(1) + .include_fallback() + .include_metadata() + .include_content_type() + .add_param('custom', 'value')) + entry.fetch() + self.assertEqual('test', entry.http_instance.headers['environment']) + self.assertEqual('en-us', entry.entry_queryable_param['locale']) + self.assertEqual(1, entry.entry_param['version']) + self.assertIn('include_fallback', entry.entry_param) + self.assertIn('include_metadata', entry.entry_queryable_param) + self.assertIn('include_content_type', entry.entry_queryable_param) + self.assertEqual('value', entry.entry_queryable_param['custom']) + + def test_36_entry_include_embedded_items_with_locale(self): + """Test entry include_embedded_items combined with locale""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .locale('en-us') + .include_embedded_items()) + entry.fetch() + self.assertEqual('en-us', entry.entry_queryable_param['locale']) + self.assertIn('include_embedded_items[]', entry.entry_param) + def test_37_entry_param_with_different_values(self): + """Test entry param method with different value types""" + entry = self.stack.content_type('faq').entry(FAQ_UID) + entry.param('string_param', 'string_value') + entry.param('int_param', 123) + entry.fetch() + self.assertEqual('string_value', entry.entry_param['string_param']) + self.assertEqual('123', entry.entry_param['int_param']) # Converted to string + + def test_38_entry_only_and_excepts_together(self): + """Test entry with both only and excepts""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .only('field1') + .excepts('field2')) + entry.fetch() + self.assertIn('only[BASE][]', entry.entry_param) + self.assertIn('except[BASE][]', entry.entry_param) + + def test_39_entry_include_reference_with_multiple_fields(self): + """Test entry include_reference with multiple fields""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .include_reference(['field1', 'field2', 'field3'])) + result = entry.fetch() + self.assertIsNotNone(result) + + def test_40_entry_variants_with_params(self): + """Test entry variants with params""" + content_type = self.stack.content_type('faq') + entry = content_type.entry(FAQ_UID).variants(VARIANT_UID, params={'locale': 'en-us'}) + result = entry.fetch() + self.assertIn('variants', result['entry']['publish_details']) + + def test_41_entry_variants_multiple_uids(self): + """Test entry variants with multiple variant UIDs""" + content_type = self.stack.content_type('faq') + entry = content_type.entry(FAQ_UID).variants([VARIANT_UID, VARIANT_UID]) + result = entry.fetch() + self.assertIn('variants', result['entry']['publish_details']) + + def test_42_entry_environment_removal(self): + """Test entry remove_environment method""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .environment('test') + .remove_environment()) + self.assertNotIn('environment', entry.http_instance.headers) + + def test_43_entry_version_zero(self): + """Test entry version with zero value""" + entry = self.stack.content_type('faq').entry(FAQ_UID).version(0) + entry.fetch() + self.assertEqual(0, entry.entry_param['version']) + + def test_44_entry_locale_empty_string(self): + """Test entry locale with empty string""" + entry = self.stack.content_type('faq').entry(FAQ_UID).locale('') + entry.fetch() + self.assertEqual('', entry.entry_queryable_param['locale']) + + def test_45_entry_include_reference_empty_list(self): + """Test entry include_reference with empty list""" + entry = self.stack.content_type('faq').entry(FAQ_UID).include_reference([]) + result = entry.fetch() + self.assertIsNotNone(result) + + def test_46_entry_all_queryable_methods_combined(self): + """Test entry with all EntryQueryable methods combined""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .locale('en-us') + .only('field1') + .excepts('field2') + .include_reference(['ref1', 'ref2']) + .include_content_type() + .include_reference_content_type_uid() + .add_param('custom', 'value')) + entry.fetch() + self.assertEqual('en-us', entry.entry_queryable_param['locale']) + self.assertIn('only[BASE][]', entry.entry_param) + self.assertIn('except[BASE][]', entry.entry_param) + self.assertIn('include_content_type', entry.entry_queryable_param) + self.assertIn('include_reference_content_type_uid', entry.entry_queryable_param) + self.assertEqual('value', entry.entry_queryable_param['custom']) + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_global_fields.py b/tests/test_global_fields.py index d8f682b..26947b0 100644 --- a/tests/test_global_fields.py +++ b/tests/test_global_fields.py @@ -95,4 +95,58 @@ def info(self, msg): pass gf = GlobalField(dummy_http, "uid", logger=dummy) assert gf.logger is dummy + # ========== Additional Test Cases for GlobalField Methods ========== + + def test_fetch_with_valid_uid(self, dummy_http): + """Test fetch method with valid global_field_uid""" + # This test requires a real http_instance, so we'll test the structure + gf = GlobalField(dummy_http, "test_global_field_uid") + assert gf._GlobalField__global_field_uid == "test_global_field_uid" + assert gf.local_param == {} + + def test_fetch_with_none_uid_raises_error(self, dummy_http): + """Test fetch method with None global_field_uid raises KeyError""" + gf = GlobalField(dummy_http, None) + with pytest.raises(KeyError): + gf.fetch() + + def test_find_with_params(self, dummy_http): + """Test find method with parameters""" + gf = GlobalField(dummy_http, None) + # This test requires a real http_instance, so we'll test the structure + assert gf.local_param == {} + # The find method should accept params + # Note: This would need a real http_instance to fully test + + def test_find_without_params(self, dummy_http): + """Test find method without parameters""" + gf = GlobalField(dummy_http, None) + assert gf.local_param == {} + # The find method should work without params + # Note: This would need a real http_instance to fully test + + def test_find_with_none_params(self, dummy_http): + """Test find method with None params""" + gf = GlobalField(dummy_http, None) + assert gf.local_param == {} + # The find method should handle None params + # Note: This would need a real http_instance to fully test + + def test_local_param_initialization(self, dummy_http): + """Test that local_param is initialized as empty dict""" + gf = GlobalField(dummy_http, "test_uid") + assert isinstance(gf.local_param, dict) + assert len(gf.local_param) == 0 + + def test_global_field_uid_storage(self, dummy_http): + """Test that global_field_uid is stored correctly""" + test_uid = "global_field_12345" + gf = GlobalField(dummy_http, test_uid) + assert gf._GlobalField__global_field_uid == test_uid + + def test_http_instance_storage(self, dummy_http): + """Test that http_instance is stored correctly""" + gf = GlobalField(dummy_http, "test_uid") + assert gf.http_instance is dummy_http + \ No newline at end of file diff --git a/tests/test_query.py b/tests/test_query.py index 5c1f056..5508088 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -143,3 +143,310 @@ def test_21_include_metadata(self): entry = self.query.locale('en-gb').include_metadata().find() entries = entry['entries'] self.assertEqual(0, len(entries)) + + # ========== Combination Tests for BaseQuery Methods ========== + + def test_22_where_with_include_count_and_pagination(self): + """Test combination of where, include_count, skip, and limit""" + query = (self.query3 + .where("title", QueryOperation.EQUALS, fields=self.const_value) + .include_count() + .skip(5) + .limit(10)) + self.assertEqual({"title": self.const_value}, query.parameters) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("5", query.query_params["skip"]) + self.assertEqual("10", query.query_params["limit"]) + + def test_23_where_with_order_by_and_pagination(self): + """Test combination of where, order_by, skip, and limit""" + query = (self.query3 + .where("price", QueryOperation.IS_GREATER_THAN, fields=100) + .order_by_ascending("price") + .skip(0) + .limit(20)) + self.assertEqual({"price": {"$gt": 100}}, query.parameters) + self.assertEqual("price", query.query_params["asc"]) + self.assertEqual("0", query.query_params["skip"]) + self.assertEqual("20", query.query_params["limit"]) + + def test_24_multiple_where_conditions_with_all_base_methods(self): + """Test multiple where conditions combined with all BaseQuery methods""" + query = (self.query3 + .where("title", QueryOperation.EQUALS, fields=self.const_value) + .where("price", QueryOperation.IS_LESS_THAN, fields=1000) + .where("tags", QueryOperation.INCLUDES, fields=["tag1", "tag2"]) + .include_count() + .skip(10) + .limit(50) + .order_by_descending("created_at") + .param("locale", "en-us")) + + # Verify parameters + self.assertEqual(3, len(query.parameters)) + self.assertEqual(self.const_value, query.parameters["title"]) + self.assertEqual({"$lt": 1000}, query.parameters["price"]) + self.assertEqual({"$in": ["tag1", "tag2"]}, query.parameters["tags"]) + + # Verify query_params + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("10", query.query_params["skip"]) + self.assertEqual("50", query.query_params["limit"]) + self.assertEqual("created_at", query.query_params["desc"]) + self.assertEqual("en-us", query.query_params["locale"]) + + def test_25_where_with_all_query_operations_combined(self): + """Test where with all QueryOperation types combined""" + query = (self.query3 + .where("title", QueryOperation.EQUALS, fields="Test") + .where("price", QueryOperation.NOT_EQUALS, fields=0) + .where("tags", QueryOperation.INCLUDES, fields=["tag1"]) + .where("excluded", QueryOperation.EXCLUDES, fields=["tag2"]) + .where("min_price", QueryOperation.IS_GREATER_THAN, fields=10) + .where("max_price", QueryOperation.IS_LESS_THAN, fields=1000) + .where("age", QueryOperation.IS_GREATER_THAN_OR_EQUAL, fields=18) + .where("score", QueryOperation.IS_LESS_THAN_OR_EQUAL, fields=100) + .where("exists", QueryOperation.EXISTS, fields=True) + .where("pattern", QueryOperation.MATCHES, fields="regex.*")) + + self.assertEqual(10, len(query.parameters)) + self.assertEqual("Test", query.parameters["title"]) + self.assertEqual({"$ne": 0}, query.parameters["price"]) + self.assertEqual({"$in": ["tag1"]}, query.parameters["tags"]) + self.assertEqual({"$nin": ["tag2"]}, query.parameters["excluded"]) + self.assertEqual({"$gt": 10}, query.parameters["min_price"]) + self.assertEqual({"$lt": 1000}, query.parameters["max_price"]) + self.assertEqual({"$gte": 18}, query.parameters["age"]) + self.assertEqual({"$lte": 100}, query.parameters["score"]) + self.assertEqual({"$exists": True}, query.parameters["exists"]) + self.assertEqual({"$regex": "regex.*"}, query.parameters["pattern"]) + + def test_26_tags_with_where_and_pagination(self): + """Test combination of tags, where, and pagination""" + query = (self.query + .tags('black', 'gold', 'silver') + .where("price", QueryOperation.IS_GREATER_THAN, fields=50) + .skip(5) + .limit(15)) + self.assertEqual("black,gold,silver", query.query_params["tags"]) + self.assertEqual({"price": {"$gt": 50}}, query.parameters) + self.assertEqual("5", query.query_params["skip"]) + self.assertEqual("15", query.query_params["limit"]) + + def test_27_search_with_where_and_base_methods(self): + """Test combination of search, where, and BaseQuery methods""" + query = (self.query + .search('search_keyword') + .where("status", QueryOperation.EQUALS, fields="active") + .include_count() + .order_by_ascending("title")) + self.assertEqual("search_keyword", query.query_params["typeahead"]) + self.assertEqual({"status": "active"}, query.parameters) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("title", query.query_params["asc"]) + + def test_28_where_in_with_base_query_methods(self): + """Test where_in combined with BaseQuery methods""" + query1 = self.query3.where("title", QueryOperation.EQUALS, fields=self.const_value) + query = (self.query + .where_in("brand", query1) + .include_count() + .skip(0) + .limit(10) + .order_by_descending("created_at")) + + self.assertEqual({"brand": {"$in_query": {"title": self.const_value}}}, + query.query_params["query"]) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("0", query.query_params["skip"]) + self.assertEqual("10", query.query_params["limit"]) + self.assertEqual("created_at", query.query_params["desc"]) + + def test_29_where_not_in_with_base_query_methods(self): + """Test where_not_in combined with BaseQuery methods""" + query1 = self.query3.where("title", QueryOperation.EQUALS, fields=self.const_value) + query = (self.query + .where_not_in("brand", query1) + .include_count() + .skip(5) + .limit(20) + .order_by_ascending("title")) + + self.assertEqual({"brand": {"$nin_query": {"title": self.const_value}}}, + query.query_params["query"]) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("5", query.query_params["skip"]) + self.assertEqual("20", query.query_params["limit"]) + self.assertEqual("title", query.query_params["asc"]) + + def test_30_query_operator_with_base_query_methods(self): + """Test query_operator (OR/AND) combined with BaseQuery methods""" + query1 = self.query1.where("price", QueryOperation.IS_LESS_THAN, fields=90) + query2 = self.query2.where("discount", QueryOperation.INCLUDES, fields=[20, 45]) + query = (self.query + .query_operator(QueryType.OR, query1, query2) + .include_count() + .skip(10) + .limit(25) + .order_by_descending("price")) + + self.assertIn("query", query.query_params) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("10", query.query_params["skip"]) + self.assertEqual("25", query.query_params["limit"]) + self.assertEqual("price", query.query_params["desc"]) + + def test_31_complex_combination_all_methods(self): + """Test complex combination of all Query and BaseQuery methods""" + query1 = self.query1.where("price", QueryOperation.IS_LESS_THAN, fields=90) + query = (self.query + .where("title", QueryOperation.EQUALS, fields="Test") + .where("status", QueryOperation.INCLUDES, fields=["active", "published"]) + .tags('tag1', 'tag2', 'tag3') + .where_in("category", query1) + .include_count() + .skip(15) + .limit(30) + .order_by_ascending("created_at") + .param("locale", "en-us") + .include_fallback() + .include_metadata()) + + # Verify parameters + self.assertEqual("Test", query.parameters["title"]) + self.assertEqual({"$in": ["active", "published"]}, query.parameters["status"]) + + # Verify query_params + self.assertEqual("tag1,tag2,tag3", query.query_params["tags"]) + self.assertIn("query", query.query_params) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("15", query.query_params["skip"]) + self.assertEqual("30", query.query_params["limit"]) + self.assertEqual("created_at", query.query_params["asc"]) + self.assertEqual("en-us", query.query_params["locale"]) + self.assertEqual("true", query.query_params["include_fallback"]) + self.assertEqual("true", query.query_params["include_metadata"]) + + def test_32_locale_with_where_and_pagination(self): + """Test locale combined with where and pagination""" + query = (self.query + .locale('en-us') + .where("title", QueryOperation.EQUALS, fields="Test") + .include_count() + .skip(0) + .limit(10)) + + # locale is stored in entry_queryable_param, not query_params + self.assertEqual("en-us", query.entry_queryable_param["locale"]) + self.assertEqual({"title": "Test"}, query.parameters) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("0", query.query_params["skip"]) + self.assertEqual("10", query.query_params["limit"]) + + def test_33_include_fallback_with_where_and_base_methods(self): + """Test include_fallback combined with where and BaseQuery methods""" + query = (self.query3 + .where("title", QueryOperation.EQUALS, fields=self.const_value) + .include_fallback() + .include_count() + .skip(5) + .limit(15) + .order_by_ascending("title")) + + self.assertEqual({"title": self.const_value}, query.parameters) + self.assertEqual("true", query.query_params["include_fallback"]) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("5", query.query_params["skip"]) + self.assertEqual("15", query.query_params["limit"]) + self.assertEqual("title", query.query_params["asc"]) + + def test_34_include_metadata_with_where_and_base_methods(self): + """Test include_metadata combined with where and BaseQuery methods""" + query = (self.query3 + .where("price", QueryOperation.IS_GREATER_THAN, fields=100) + .include_metadata() + .include_count() + .skip(10) + .limit(20) + .order_by_descending("price")) + + self.assertEqual({"price": {"$gt": 100}}, query.parameters) + self.assertEqual("true", query.query_params["include_metadata"]) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("10", query.query_params["skip"]) + self.assertEqual("20", query.query_params["limit"]) + self.assertEqual("price", query.query_params["desc"]) + + def test_35_add_params_with_where_and_other_methods(self): + """Test add_params combined with where and other methods""" + query = (self.query3 + .where("title", QueryOperation.EQUALS, fields=self.const_value) + .add_params({"locale": "en-us", "include_count": "true"}) + .skip(5) + .limit(10)) + + self.assertEqual({"title": self.const_value}, query.parameters) + self.assertEqual("en-us", query.query_params["locale"]) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("5", query.query_params["skip"]) + self.assertEqual("10", query.query_params["limit"]) + + def test_36_remove_param_after_combination(self): + """Test remove_param after building a complex query""" + query = (self.query3 + .where("title", QueryOperation.EQUALS, fields=self.const_value) + .include_count() + .skip(10) + .limit(20) + .param("key1", "value1") + .param("key2", "value2") + .remove_param("key1")) + + self.assertEqual({"title": self.const_value}, query.parameters) + self.assertNotIn("key1", query.query_params) + self.assertEqual("value2", query.query_params["key2"]) + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("10", query.query_params["skip"]) + self.assertEqual("20", query.query_params["limit"]) + + def test_37_order_by_ascending_then_descending_coexist(self): + """Test that order_by_ascending and order_by_descending can coexist""" + query = (self.query3 + .where("title", QueryOperation.EQUALS, fields=self.const_value) + .order_by_ascending("title") + .order_by_descending("price")) + + self.assertEqual({"title": self.const_value}, query.parameters) + # Both asc and desc can coexist (they use different keys) + self.assertEqual("title", query.query_params["asc"]) + self.assertEqual("price", query.query_params["desc"]) + + def test_38_multiple_where_conditions_with_complex_operations(self): + """Test multiple where conditions with complex operations and all BaseQuery methods""" + query = (self.query3 + .where("title", QueryOperation.EQUALS, fields="Product") + .where("price", QueryOperation.IS_GREATER_THAN, fields=50) + .where("price", QueryOperation.IS_LESS_THAN, fields=500) + .where("tags", QueryOperation.INCLUDES, fields=["electronics", "sale"]) + .where("excluded_tags", QueryOperation.EXCLUDES, fields=["discontinued"]) + .include_count() + .skip(0) + .limit(100) + .order_by_descending("price") + .param("locale", "en-us") + .include_fallback()) + + # Verify all where conditions are present + self.assertEqual("Product", query.parameters["title"]) + # Note: price is overwritten by the second where call - last call wins + self.assertEqual({"$lt": 500}, query.parameters["price"]) + self.assertEqual({"$in": ["electronics", "sale"]}, query.parameters["tags"]) + self.assertEqual({"$nin": ["discontinued"]}, query.parameters["excluded_tags"]) + + # Verify all query_params + self.assertEqual("true", query.query_params["include_count"]) + self.assertEqual("0", query.query_params["skip"]) + self.assertEqual("100", query.query_params["limit"]) + self.assertEqual("price", query.query_params["desc"]) + self.assertEqual("en-us", query.query_params["locale"]) + self.assertEqual("true", query.query_params["include_fallback"]) diff --git a/tests/test_stack.py b/tests/test_stack.py index 7bc052c..7cf75b1 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -3,6 +3,7 @@ import contentstack import logging import io +from urllib3 import Retry from contentstack.stack import ContentstackRegion from contentstack.stack import Stack @@ -217,4 +218,202 @@ def test_stack_with_custom_logger(self): handler.flush() logs = log_stream.getvalue() print("\nCaptured Logs:\n", logs) - self.assertIn("INFO - contentstack.custom.test_logger - Test log entry", logs) \ No newline at end of file + self.assertIn("INFO - contentstack.custom.test_logger - Test log entry", logs) + + # ========== Additional Test Cases ========== + + def test_24_stack_with_branch(self): + """Test Stack initialization with branch parameter""" + stack = contentstack.Stack( + API_KEY, DELIVERY_TOKEN, ENVIRONMENT, host=HOST, branch="development") + self.assertEqual("development", stack.branch) + self.assertEqual("development", stack.get_branch) + + def test_25_stack_with_live_preview(self): + """Test Stack initialization with live_preview parameter""" + live_preview_config = { + 'enable': True, + 'host': 'api.contentstack.io', + 'authorization': 'test_token' + } + stack = contentstack.Stack( + API_KEY, DELIVERY_TOKEN, ENVIRONMENT, host=HOST, live_preview=live_preview_config) + self.assertEqual(live_preview_config, stack.live_preview) + self.assertEqual(live_preview_config, stack.get_live_preview) + + def test_26_image_transformation_with_multiple_params(self): + """Test image transformation with multiple parameters""" + image_url = 'https://images.contentstack.io/v3/assets/download' + image_transform = self.stack.image_transform( + image_url, width=500, height=550, format='webp', quality=80, auto='webp') + result_url = image_transform.get_url() + self.assertIn('width=500', result_url) + self.assertIn('height=550', result_url) + self.assertIn('format=webp', result_url) + self.assertIn('quality=80', result_url) + self.assertIn('auto=webp', result_url) + + def test_27_image_transformation_with_crop_params(self): + """Test image transformation with crop parameters""" + image_url = 'https://images.contentstack.io/v3/assets/download' + image_transform = self.stack.image_transform( + image_url, width=300, height=200, crop='fit', align='center') + result_url = image_transform.get_url() + self.assertIn('width=300', result_url) + self.assertIn('height=200', result_url) + self.assertIn('crop=fit', result_url) + self.assertIn('align=center', result_url) + + def test_28_content_type_method(self): + """Test content_type method returns ContentType instance""" + content_type = self.stack.content_type('test_content_type') + self.assertIsNotNone(content_type) + self.assertEqual('test_content_type', content_type._ContentType__content_type_uid) + + def test_29_content_type_with_none_uid(self): + """Test content_type method with None UID""" + content_type = self.stack.content_type(None) + self.assertIsNotNone(content_type) + self.assertIsNone(content_type._ContentType__content_type_uid) + + def test_30_taxonomy_method(self): + """Test taxonomy method returns Taxonomy instance""" + taxonomy = self.stack.taxonomy() + self.assertIsNotNone(taxonomy) + self.assertIsNotNone(taxonomy.http_instance) + + def test_31_global_field_method(self): + """Test global_field method returns GlobalField instance""" + global_field = self.stack.global_field('test_global_field') + self.assertIsNotNone(global_field) + self.assertEqual('test_global_field', global_field._GlobalField__global_field_uid) + + def test_32_global_field_with_none_uid(self): + """Test global_field method with None UID""" + global_field = self.stack.global_field(None) + self.assertIsNotNone(global_field) + self.assertIsNone(global_field._GlobalField__global_field_uid) + + def test_33_asset_method_with_valid_uid(self): + """Test asset method with valid UID""" + asset = self.stack.asset('test_asset_uid') + self.assertIsNotNone(asset) + self.assertEqual('test_asset_uid', asset.uid) + + def test_34_asset_method_with_invalid_uid(self): + """Test asset method with invalid UID raises error""" + with self.assertRaises(KeyError): + self.stack.asset(None) + + with self.assertRaises(KeyError): + self.stack.asset(123) # Not a string + + def test_35_asset_query_method(self): + """Test asset_query method returns AssetQuery instance""" + asset_query = self.stack.asset_query() + self.assertIsNotNone(asset_query) + self.assertIsNotNone(asset_query.http_instance) + + def test_36_sync_init_with_only_start_from(self): + """Test sync_init with only start_from parameter""" + result = self.stack.sync_init(start_from='2018-01-14T00:00:00.000Z') + if result is not None: + self.assertIn('items', result or {}) + + def test_37_sync_init_with_only_locale(self): + """Test sync_init with only locale parameter""" + result = self.stack.sync_init(locale='en-us') + if result is not None: + self.assertIn('items', result or {}) + + def test_38_sync_init_with_only_publish_type(self): + """Test sync_init with only publish_type parameter""" + result = self.stack.sync_init(publish_type='entry_published') + if result is not None: + self.assertIn('items', result or {}) + + def test_39_sync_token_with_empty_string(self): + """Test sync_token with empty string""" + result = self.stack.sync_token('') + if result is not None: + self.assertIsNotNone(result) + + def test_40_pagination_with_empty_string(self): + """Test pagination with empty string""" + result = self.stack.pagination('') + if result is not None: + self.assertIsNotNone(result) + + def test_41_get_branch_property(self): + """Test get_branch property""" + stack = contentstack.Stack( + API_KEY, DELIVERY_TOKEN, ENVIRONMENT, host=HOST, branch="test_branch") + self.assertEqual("test_branch", stack.get_branch) + + def test_42_get_live_preview_property(self): + """Test get_live_preview property""" + live_preview = {'enable': True} + stack = contentstack.Stack( + API_KEY, DELIVERY_TOKEN, ENVIRONMENT, host=HOST, live_preview=live_preview) + self.assertEqual(live_preview, stack.get_live_preview) + + def test_43_stack_with_all_optional_params(self): + """Test Stack initialization with all optional parameters""" + live_preview = {'enable': True, 'host': 'api.contentstack.io'} + retry_strategy = Retry(total=3, backoff_factor=1, status_forcelist=[408]) + stack = contentstack.Stack( + API_KEY, DELIVERY_TOKEN, ENVIRONMENT, + host=HOST, + version='v3', + region=ContentstackRegion.EU, + timeout=60, + retry_strategy=retry_strategy, + live_preview=live_preview, + branch="main", + early_access=["taxonomy"], + logger=logging.getLogger("test") + ) + self.assertEqual(API_KEY, stack.api_key) + self.assertEqual(DELIVERY_TOKEN, stack.delivery_token) + self.assertEqual(ENVIRONMENT, stack.environment) + self.assertEqual("main", stack.branch) + self.assertEqual(live_preview, stack.live_preview) + self.assertEqual(["taxonomy"], stack.early_access) + self.assertEqual(60, stack.timeout) + + def test_44_image_transformation_with_special_characters(self): + """Test image transformation URL encoding with special characters""" + image_url = 'https://images.contentstack.io/v3/assets/download?param=value' + image_transform = self.stack.image_transform(image_url, width=100) + result_url = image_transform.get_url() + self.assertIn('width=100', result_url) + + def test_45_image_transformation_empty_url(self): + """Test image transformation with empty URL""" + try: + image_transform = self.stack.image_transform('') + result = image_transform.get_url() + self.assertIsNone(result) + except PermissionError: + pass # Expected behavior + + def test_46_early_access_empty_list(self): + """Test early_access with empty list""" + stack = contentstack.Stack( + API_KEY, DELIVERY_TOKEN, ENVIRONMENT, host=HOST, early_access=[]) + self.assertEqual([], stack.early_access) + self.assertEqual([], stack.get_early_access) + + def test_47_early_access_single_item(self): + """Test early_access with single item""" + stack = contentstack.Stack( + API_KEY, DELIVERY_TOKEN, ENVIRONMENT, host=HOST, early_access=["taxonomy"]) + self.assertEqual(["taxonomy"], stack.early_access) + self.assertEqual(["taxonomy"], stack.get_early_access) + + def test_48_region_property_access(self): + """Test region property access""" + stack = contentstack.Stack( + API_KEY, DELIVERY_TOKEN, ENVIRONMENT, host=HOST, region=ContentstackRegion.AZURE_NA) + self.assertEqual(ContentstackRegion.AZURE_NA, stack.region) + self.assertEqual('azure-na', stack.region.value) \ No newline at end of file diff --git a/tests/test_taxonomies.py b/tests/test_taxonomies.py index b4c14c0..06c0eaa 100644 --- a/tests/test_taxonomies.py +++ b/tests/test_taxonomies.py @@ -94,6 +94,153 @@ def test_10_taxonomy_find_with_params(self): result = taxonomy.in_("taxonomies.category", ["test"]).find({'limit': 5}) if result is not None: self.assertIn('entries', result) + + # ========== Additional Test Cases ========== + + def test_11_taxonomy_method_chaining(self): + """Test taxonomy method chaining with multiple filters""" + taxonomy = self.stack.taxonomy() + result = (taxonomy + .in_("taxonomies.category", ["category1", "category2"]) + .exists("taxonomies.status") + .find({'limit': 10})) + if result is not None: + self.assertIn('entries', result) + + def test_12_taxonomy_complex_nested_query(self): + """Test complex nested taxonomy query with and_ and or_""" + taxonomy = self.stack.taxonomy() + result = (taxonomy + .and_( + {"taxonomies.category": {"$in": ["test"]}}, + {"taxonomies.status": {"$in": ["active"]}} + ) + .or_( + {"taxonomies.priority": {"$in": ["high"]}}, + {"taxonomies.type": {"$exists": True}} + ) + .find({'limit': 5})) + if result is not None: + self.assertIn('entries', result) + + def test_13_taxonomy_in_with_empty_list(self): + """Test taxonomy in_ method with empty list""" + taxonomy = self.stack.taxonomy() + result = taxonomy.in_("taxonomies.category", []).find() + if result is not None: + self.assertIsNotNone(result) + + def test_14_taxonomy_in_with_single_item(self): + """Test taxonomy in_ method with single item list""" + taxonomy = self.stack.taxonomy() + result = taxonomy.in_("taxonomies.category", ["single_category"]).find() + if result is not None: + self.assertIn('entries', result) + + def test_15_taxonomy_equal_and_below_with_different_levels(self): + """Test taxonomy equal_and_below with different level values""" + taxonomy = self.stack.taxonomy() + result = taxonomy.equal_and_below("taxonomies.color", "blue", levels=0).find() + if result is not None: + self.assertIn('entries', result) + + result2 = taxonomy.equal_and_below("taxonomies.color", "blue", levels=5).find() + if result2 is not None: + self.assertIn('entries', result2) + + def test_16_taxonomy_below_with_different_levels(self): + """Test taxonomy below with different level values""" + taxonomy = self.stack.taxonomy() + result = taxonomy.below("taxonomies.hierarchy", "parent_uid", levels=1).find() + if result is not None: + self.assertIn('entries', result) + + def test_17_taxonomy_equal_and_above_with_different_levels(self): + """Test taxonomy equal_and_above with different level values""" + taxonomy = self.stack.taxonomy() + result = taxonomy.equal_and_above("taxonomies.hierarchy", "child_uid", levels=1).find() + if result is not None: + self.assertIn('entries', result) + + def test_18_taxonomy_above_with_different_levels(self): + """Test taxonomy above with different level values""" + taxonomy = self.stack.taxonomy() + result = taxonomy.above("taxonomies.hierarchy", "child_uid", levels=1).find() + if result is not None: + self.assertIn('entries', result) + + def test_19_taxonomy_multiple_exists(self): + """Test taxonomy with multiple exists filters""" + taxonomy = self.stack.taxonomy() + result = (taxonomy + .exists("taxonomies.field1") + .exists("taxonomies.field2") + .find()) + if result is not None: + self.assertIn('entries', result) + + def test_20_taxonomy_find_with_multiple_params(self): + """Test taxonomy find with multiple parameters""" + taxonomy = self.stack.taxonomy() + result = taxonomy.in_("taxonomies.category", ["test"]).find({ + 'limit': 10, + 'skip': 0 + }) + if result is not None: + self.assertIn('entries', result) + + def test_21_taxonomy_or_with_multiple_conditions(self): + """Test taxonomy or_ with multiple conditions""" + taxonomy = self.stack.taxonomy() + result = taxonomy.or_( + {"taxonomies.category": {"$in": ["cat1"]}}, + {"taxonomies.category": {"$in": ["cat2"]}}, + {"taxonomies.category": {"$in": ["cat3"]}} + ).find() + if result is not None: + self.assertIn('entries', result) + + def test_22_taxonomy_and_with_multiple_conditions(self): + """Test taxonomy and_ with multiple conditions""" + taxonomy = self.stack.taxonomy() + result = taxonomy.and_( + {"taxonomies.category": {"$in": ["test"]}}, + {"taxonomies.status": {"$in": ["active"]}}, + {"taxonomies.priority": {"$exists": True}} + ).find() + if result is not None: + self.assertIn('entries', result) + + def test_23_taxonomy_combination_all_methods(self): + """Test taxonomy with combination of all methods""" + taxonomy = self.stack.taxonomy() + result = (taxonomy + .in_("taxonomies.category", ["category1"]) + .exists("taxonomies.status") + .and_( + {"taxonomies.type": {"$in": ["type1"]}}, + {"taxonomies.active": {"$exists": True}} + ) + .or_( + {"taxonomies.priority": {"$in": ["high"]}} + ) + .find({'limit': 5, 'skip': 0})) + if result is not None: + self.assertIn('entries', result) + + def test_24_taxonomy_find_without_params(self): + """Test taxonomy find without any parameters""" + taxonomy = self.stack.taxonomy() + result = taxonomy.in_("taxonomies.category", ["test"]).find() + if result is not None: + self.assertIn('entries', result) + + def test_25_taxonomy_find_with_none_params(self): + """Test taxonomy find with None params""" + taxonomy = self.stack.taxonomy() + result = taxonomy.in_("taxonomies.category", ["test"]).find(None) + if result is not None: + self.assertIn('entries', result) if __name__ == '__main__': unittest.main()