diff --git a/aiida_optimade/transformers/aiida.py b/aiida_optimade/transformers/aiida.py index 85e5013d..e90c0f67 100644 --- a/aiida_optimade/transformers/aiida.py +++ b/aiida_optimade/transformers/aiida.py @@ -65,6 +65,14 @@ def value_list(self, args): """value_list: [ OPERATOR ] value ( "," [ OPERATOR ] value )*""" values = [] for value in args: + if value in self.reversed_operator_map: + # value is OPERATOR + # This is currently not supported + raise NotImplementedError( + f"OPERATOR {value} inside value_list {args} has not been " + "implemented." + ) + try: value = float(value) except ValueError: diff --git a/tasks.py b/tasks.py index 0a90ff90..ab22ead9 100644 --- a/tasks.py +++ b/tasks.py @@ -39,6 +39,9 @@ def setver(_, patch=False, new_ver=""): "aiida_optimade/config.json", ('"version": ([^,]+),', f'"version": "{new_ver}",'), ) + update_file( + "tests/test_config.json", ('"version": ([^,]+),', f'"version": "{new_ver}",'), + ) print("Bumped version to {}".format(new_ver)) diff --git a/tests/config.json b/tests/config.json deleted file mode 100644 index f7ec19b7..00000000 --- a/tests/config.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "page_limit": 15, - "page_limit_max": 500, - "base_url": null, - "provider": { - "prefix": "_aiida_", - "name": "AiiDA", - "description": "AiiDA: Automated Interactive Infrastructure and Database for Computational Science (http://www.aiida.net)", - "homepage": "http://www.aiida.net", - "index_base_url": null - }, - "implementation": { - "name": "aiida-optimade", - "version": "0.6.1", - "source_url": "https://github.com/aiidateam/aiida-optimade", - "maintainer": {"email": "casper.andersen@epfl.ch"} - }, - "provider_fields": { - "structures": [ - "ctime" - ] - }, - "aliases": { - "structures": { - "immutable_id": "uuid", - "last_modified": "mtime", - "type": "attributes.something.non.existing", - "relationships": "attributes.something.non.existing", - "links": "attributes.something.non.existing" - } - } -} diff --git a/tests/conftest.py b/tests/conftest.py index 5a1e96d1..2fe8b16b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,5 +5,17 @@ def pytest_configure(config): """Method that runs before pytest collects tests so no modules are imported""" - config_file = Path(__file__).parent.parent.joinpath("aiida_optimade/config.json") - os.environ["OPTIMADE_CONFIG_FILE"] = str(config_file) + set_config_file() + load_aiida_profile() + + +def set_config_file(): + """Set config file environment variable pointing to `/tests/test_config.json`""" + cwd = Path(__file__).parent.resolve() + os.environ["OPTIMADE_CONFIG_FILE"] = str(cwd.joinpath("test_config.json")) + + +def load_aiida_profile(): + """Load AiiDA profile""" + if os.getenv("AIIDA_PROFILE", None) is None: + os.environ["AIIDA_PROFILE"] = "optimade_sqla" diff --git a/tests/filtertransformers/test_mongo.py b/tests/filtertransformers/test_aiida.py similarity index 50% rename from tests/filtertransformers/test_mongo.py rename to tests/filtertransformers/test_aiida.py index 63c20212..94bb50af 100644 --- a/tests/filtertransformers/test_mongo.py +++ b/tests/filtertransformers/test_aiida.py @@ -1,30 +1,38 @@ import unittest +import pytest from lark.exceptions import VisitError from optimade.filterparser import LarkParser, ParserError -from optimade.filtertransformers.mongo import MongoTransformer from optimade.server.mappers import BaseResourceMapper +from aiida_optimade.transformers import AiidaTransformer + + +class TestAiidaTransformer(unittest.TestCase): + """Tests for AiidaTransformer""" -class TestMongoTransformer(unittest.TestCase): version = (0, 10, 1) variant = "default" + maxDiff = None def setUp(self): - p = LarkParser(version=self.version, variant=self.variant) - t = MongoTransformer() - self.transform = lambda inp: t.transform(p.parse(inp)) + parser = LarkParser(version=self.version, variant=self.variant) + transformer = AiidaTransformer() + self.transform = lambda inp: transformer.transform(parser.parse(inp)) def test_empty(self): + """Check passing "empty" strings""" self.assertIsNone(self.transform(" ")) + self.assertIsNone(self.transform("")) def test_property_names(self): - self.assertEqual(self.transform("band_gap = 1"), {"band_gap": {"$eq": 1}}) + """Check `property` names""" + self.assertEqual(self.transform("band_gap = 1"), {"band_gap": {"==": 1}}) self.assertEqual( - self.transform("cell_length_a = 1"), {"cell_length_a": {"$eq": 1}} + self.transform("cell_length_a = 1"), {"cell_length_a": {"==": 1}} ) - self.assertEqual(self.transform("cell_volume = 1"), {"cell_volume": {"$eq": 1}}) + self.assertEqual(self.transform("cell_volume = 1"), {"cell_volume": {"==": 1}}) with self.assertRaises(ParserError): self.transform("0_kvak IS KNOWN") # starts with a number @@ -37,43 +45,45 @@ def test_property_names(self): # database-provider-specific prefixes self.assertEqual( - self.transform("_exmpl_formula_sum = 1"), {"_exmpl_formula_sum": {"$eq": 1}} + self.transform("_exmpl_formula_sum = 1"), {"_exmpl_formula_sum": {"==": 1}} ) self.assertEqual( - self.transform("_exmpl_band_gap = 1"), {"_exmpl_band_gap": {"$eq": 1}} + self.transform("_exmpl_band_gap = 1"), {"_exmpl_band_gap": {"==": 1}} ) # Nested property names self.assertEqual( self.transform("identifier1.identifierd2 = 42"), - {"identifier1.identifierd2": {"$eq": 42}}, + {"identifier1.identifierd2": {"==": 42}}, ) def test_string_values(self): + """Check various string values validity""" self.assertEqual( self.transform('author="Sąžininga Žąsis"'), - {"author": {"$eq": "Sąžininga Žąsis"}}, + {"author": {"==": "Sąžininga Žąsis"}}, ) self.assertEqual( self.transform('field = "!#$%&\'() * +, -./:; <= > ? @[] ^ `{|}~ % "'), - {"field": {"$eq": "!#$%&'() * +, -./:; <= > ? @[] ^ `{|}~ % "}}, + {"field": {"==": "!#$%&'() * +, -./:; <= > ? @[] ^ `{|}~ % "}}, ) def test_number_values(self): - self.assertEqual(self.transform("a = 12345"), {"a": {"$eq": 12345}}) - self.assertEqual(self.transform("b = +12"), {"b": {"$eq": 12}}) - self.assertEqual(self.transform("c = -34"), {"c": {"$eq": -34}}) - self.assertEqual(self.transform("d = 1.2"), {"d": {"$eq": 1.2}}) - self.assertEqual(self.transform("e = .2E7"), {"e": {"$eq": 2000000.0}}) - self.assertEqual(self.transform("f = -.2E+7"), {"f": {"$eq": -2000000.0}}) - self.assertEqual(self.transform("g = +10.01E-10"), {"g": {"$eq": 1.001e-09}}) - self.assertEqual(self.transform("h = 6.03e23"), {"h": {"$eq": 6.03e23}}) - self.assertEqual(self.transform("i = .1E1"), {"i": {"$eq": 1.0}}) - self.assertEqual(self.transform("j = -.1e1"), {"j": {"$eq": -1.0}}) - self.assertEqual(self.transform("k = 1.e-12"), {"k": {"$eq": 1e-12}}) - self.assertEqual(self.transform("l = -.1e-12"), {"l": {"$eq": -1e-13}}) - self.assertEqual( - self.transform("m = 1000000000.E1000000000"), {"m": {"$eq": float("inf")}} + """Check various number values validity""" + self.assertEqual(self.transform("a = 12345"), {"a": {"==": 12345}}) + self.assertEqual(self.transform("b = +12"), {"b": {"==": 12}}) + self.assertEqual(self.transform("c = -34"), {"c": {"==": -34}}) + self.assertEqual(self.transform("d = 1.2"), {"d": {"==": 1.2}}) + self.assertEqual(self.transform("e = .2E7"), {"e": {"==": 2000000.0}}) + self.assertEqual(self.transform("f = -.2E+7"), {"f": {"==": -2000000.0}}) + self.assertEqual(self.transform("g = +10.01E-10"), {"g": {"==": 1.001e-09}}) + self.assertEqual(self.transform("h = 6.03e23"), {"h": {"==": 6.03e23}}) + self.assertEqual(self.transform("i = .1E1"), {"i": {"==": 1.0}}) + self.assertEqual(self.transform("j = -.1e1"), {"j": {"==": -1.0}}) + self.assertEqual(self.transform("k = 1.e-12"), {"k": {"==": 1e-12}}) + self.assertEqual(self.transform("l = -.1e-12"), {"l": {"==": -1e-13}}) + self.assertEqual( + self.transform("m = 1000000000.E1000000000"), {"m": {"==": float("inf")}} ) with self.assertRaises(ParserError): @@ -92,64 +102,71 @@ def test_number_values(self): self.transform("number=0.0.1") def test_simple_comparisons(self): - self.assertEqual(self.transform("a<3"), {"a": {"$lt": 3}}) - self.assertEqual(self.transform("a<=3"), {"a": {"$lte": 3}}) - self.assertEqual(self.transform("a>3"), {"a": {"$gt": 3}}) - self.assertEqual(self.transform("a>=3"), {"a": {"$gte": 3}}) - self.assertEqual(self.transform("a=3"), {"a": {"$eq": 3}}) - self.assertEqual(self.transform("a!=3"), {"a": {"$ne": 3}}) + """Check simple comparisons""" + self.assertEqual(self.transform("a<3"), {"a": {"<": 3}}) + self.assertEqual(self.transform("a<=3"), {"a": {"<=": 3}}) + self.assertEqual(self.transform("a>3"), {"a": {">": 3}}) + self.assertEqual(self.transform("a>=3"), {"a": {">=": 3}}) + self.assertEqual(self.transform("a=3"), {"a": {"==": 3}}) + self.assertEqual(self.transform("a!=3"), {"a": {"!==": 3}}) def test_id(self): - self.assertEqual(self.transform('id="example/1"'), {"id": {"$eq": "example/1"}}) + """Test `id` valued `property` name""" + self.assertEqual(self.transform('id="example/1"'), {"id": {"==": "example/1"}}) self.assertEqual( - self.transform('"example/1" = id'), {"id": {"$eq": "example/1"}} + self.transform('"example/1" = id'), {"id": {"==": "example/1"}} ) self.assertEqual( self.transform('id="test/2" OR "example/1" = id'), - {"$or": [{"id": {"$eq": "test/2"}}, {"id": {"$eq": "example/1"}}]}, + {"or": [{"id": {"==": "test/2"}}, {"id": {"==": "example/1"}}]}, ) def test_operators(self): + """Test OPTIMADE filter operators""" # Basic boolean operations - # TODO: {"a": {"$not": {"$lt": 3}}} can be simplified to {"a": {"$gte": 3}} - self.assertEqual(self.transform("NOT a<3"), {"a": {"$not": {"$lt": 3}}}) + # TODO: {"!and": [{"a": {"<": 3}}]} can be simplified to {"a": {">=": 3}} + self.assertEqual(self.transform("NOT a<3"), {"!and": [{"a": {"<": 3}}]}) - # TODO: {'$not': {'$eq': 'Ti'}} can be simplified to {'$ne': 'Ti'} + # TODO: {'!and': [{'==': 'Ti'}]} can be simplified to {'!==': 'Ti'} self.assertEqual( self.transform( "NOT ( " 'chemical_formula_hill = "Al" AND chemical_formula_anonymous = "A" OR ' - 'chemical_formula_anonymous = "H2O" AND NOT chemical_formula_hill = "Ti" ' - ")" + 'chemical_formula_anonymous = "H2O" AND NOT chemical_formula_hill = ' + '"Ti" )' ), { - "$nor": [ - { - "$and": [ - {"chemical_formula_hill": {"$eq": "Al"}}, - {"chemical_formula_anonymous": {"$eq": "A"}}, - ] - }, + "!and": [ { - "$and": [ - {"chemical_formula_anonymous": {"$eq": "H2O"}}, - {"chemical_formula_hill": {"$not": {"$eq": "Ti"}}}, + "or": [ + { + "and": [ + {"chemical_formula_hill": {"==": "Al"}}, + {"chemical_formula_anonymous": {"==": "A"}}, + ] + }, + { + "and": [ + {"chemical_formula_anonymous": {"==": "H2O"}}, + {"!and": [{"chemical_formula_hill": {"==": "Ti"}}]}, + ] + }, ] - }, + } ] }, ) # Numeric and String comparisons - self.assertEqual(self.transform("nelements > 3"), {"nelements": {"$gt": 3}}) + self.assertEqual(self.transform("nelements > 3"), {"nelements": {">": 3}}) self.assertEqual( self.transform( 'chemical_formula_hill = "H2O" AND chemical_formula_anonymous != "AB"' ), { - "$and": [ - {"chemical_formula_hill": {"$eq": "H2O"}}, - {"chemical_formula_anonymous": {"$ne": "AB"}}, + "and": [ + {"chemical_formula_hill": {"==": "H2O"}}, + {"chemical_formula_anonymous": {"!==": "AB"}}, ] }, ) @@ -159,15 +176,19 @@ def test_operators(self): 'NOT ( _exmpl_x != "Some string" OR NOT _exmpl_a = 7)' ), { - "$or": [ - {"_exmpl_aax": {"$lte": 10000000.0}}, + "or": [ + {"_exmpl_aax": {"<=": 10000000.0}}, { - "$and": [ - {"nelements": {"$gte": 10}}, + "and": [ + {"nelements": {">=": 10}}, { - "$nor": [ - {"_exmpl_x": {"$ne": "Some string"}}, - {"_exmpl_a": {"$not": {"$eq": 7}}}, + "!and": [ + { + "or": [ + {"_exmpl_x": {"!==": "Some string"}}, + {"!and": [{"_exmpl_a": {"==": 7}}]}, + ] + } ] }, ] @@ -177,18 +198,18 @@ def test_operators(self): ) self.assertEqual( self.transform('_exmpl_spacegroup="P2"'), - {"_exmpl_spacegroup": {"$eq": "P2"}}, + {"_exmpl_spacegroup": {"==": "P2"}}, ) self.assertEqual( self.transform("_exmpl_cell_volume<100.0"), - {"_exmpl_cell_volume": {"$lt": 100.0}}, + {"_exmpl_cell_volume": {"<": 100.0}}, ) self.assertEqual( self.transform("_exmpl_bandgap > 5.0 AND _exmpl_molecular_weight < 350"), { - "$and": [ - {"_exmpl_bandgap": {"$gt": 5.0}}, - {"_exmpl_molecular_weight": {"$lt": 350}}, + "and": [ + {"_exmpl_bandgap": {">": 5.0}}, + {"_exmpl_molecular_weight": {"<": 350}}, ] }, ) @@ -197,47 +218,47 @@ def test_operators(self): '_exmpl_melting_point<300 AND nelements=4 AND elements="Si,O2"' ), { - "$and": [ - {"_exmpl_melting_point": {"$lt": 300}}, - {"nelements": {"$eq": 4}}, - {"elements": {"$eq": "Si,O2"}}, + "and": [ + {"_exmpl_melting_point": {"<": 300}}, + {"nelements": {"==": 4}}, + {"elements": {"==": "Si,O2"}}, ] }, ) self.assertEqual( self.transform("_exmpl_some_string_property = 42"), - {"_exmpl_some_string_property": {"$eq": 42}}, + {"_exmpl_some_string_property": {"==": 42}}, ) - self.assertEqual(self.transform("5 < _exmpl_a"), {"_exmpl_a": {"$gt": 5}}) + self.assertEqual(self.transform("5 < _exmpl_a"), {"_exmpl_a": {">": 5}}) self.assertEqual( - self.transform("a<5 AND b=0"), - {"$and": [{"a": {"$lt": 5}}, {"b": {"$eq": 0}}]}, + self.transform("a<5 AND b=0"), {"and": [{"a": {"<": 5}}, {"b": {"==": 0}}]}, ) self.assertEqual( self.transform("a >= 8 OR a<5 AND b>=8"), - { - "$or": [ - {"a": {"$gte": 8}}, - {"$and": [{"a": {"$lt": 5}}, {"b": {"$gte": 8}}]}, - ] - }, + {"or": [{"a": {">=": 8}}, {"and": [{"a": {"<": 5}}, {"b": {">=": 8}}]},]}, ) # OPTIONAL - # self.assertEqual(self.transform("((NOT (_exmpl_a>_exmpl_b)) AND _exmpl_x>0)"), {}) + # self.assertEqual( + # self.transform("((NOT (_exmpl_a>_exmpl_b)) AND _exmpl_x>0)"), {} + # ) self.assertEqual( self.transform("NOT (a>1 AND b>1)"), - {"$and": [{"a": {"$not": {"$gt": 1}}}, {"b": {"$not": {"$gt": 1}}}]}, + {"!and": [{"and": [{"a": {">": 1}}, {"b": {">": 1}}]}]}, ) self.assertEqual( self.transform("NOT (a>1 AND b>1 OR c>1)"), { - "$nor": [ - {"$and": [{"a": {"$gt": 1}}, {"b": {"$gt": 1}}]}, - {"c": {"$gt": 1}}, + "!and": [ + { + "or": [ + {"and": [{"a": {">": 1}}, {"b": {">": 1}}]}, + {"c": {">": 1}}, + ] + } ] }, ) @@ -245,9 +266,13 @@ def test_operators(self): self.assertEqual( self.transform("NOT (a>1 AND ( b>1 OR c>1 ))"), { - "$and": [ - {"a": {"$not": {"$gt": 1}}}, - {"$nor": [{"b": {"$gt": 1}}, {"c": {"$gt": 1}}]}, + "!and": [ + { + "and": [ + {"a": {">": 1}}, + {"or": [{"b": {">": 1}}, {"c": {">": 1}}]}, + ] + } ] }, ) @@ -255,14 +280,18 @@ def test_operators(self): self.assertEqual( self.transform("NOT (a>1 AND ( b>1 OR (c>1 AND d>1 ) ))"), { - "$and": [ - {"a": {"$not": {"$gt": 1}}}, + "!and": [ { - "$nor": [ - {"b": {"$gt": 1}}, - {"$and": [{"c": {"$gt": 1}}, {"d": {"$gt": 1}}]}, + "and": [ + {"a": {">": 1}}, + { + "or": [ + {"b": {">": 1}}, + {"and": [{"c": {">": 1}}, {"d": {">": 1}}]}, + ] + }, ] - }, + } ] }, ) @@ -272,40 +301,42 @@ def test_operators(self): 'elements HAS "Ag" AND NOT ( elements HAS "Ir" AND elements HAS "Ac" )' ), { - "$and": [ - {"elements": {"$in": ["Ag"]}}, + "and": [ + {"elements": {"contains": ["Ag"]}}, { - "$and": [ - {"elements": {"$not": {"$in": ["Ir"]}}}, - {"elements": {"$not": {"$in": ["Ac"]}}}, + "!and": [ + { + "and": [ + {"elements": {"contains": ["Ir"]}}, + {"elements": {"contains": ["Ac"]}}, + ] + } ] }, ] }, ) - self.assertEqual(self.transform("5 < 7"), {7: {"$gt": 5}}) + self.assertEqual(self.transform("5 < 7"), {7: {">": 5}}) with self.assertRaises(VisitError): self.transform('"some string" > "some other string"') + @pytest.mark.skip("Relationships have not yet been implemented") def test_filtering_on_relationships(self): - """ Test the nested properties with special names - like "structures", "references" etc. are applied - to the relationships field. - - """ + """Test the nested properties with special names like "structures", + "references" etc. are applied to the relationships field""" self.assertEqual( self.transform('references.id HAS "dummy/2019"'), - {"relationships.references.data.id": {"$in": ["dummy/2019"]}}, + {"relationships.references.data.id": {"contains": ["dummy/2019"]}}, ) self.assertEqual( self.transform('structures.id HAS ANY "dummy/2019", "dijkstra1968"'), { "relationships.structures.data.id": { - "$in": ["dummy/2019", "dijkstra1968"] + "contains": ["dummy/2019", "dijkstra1968"] } }, ) @@ -314,81 +345,84 @@ def test_filtering_on_relationships(self): self.transform('structures.id HAS ALL "dummy/2019", "dijkstra1968"'), { "relationships.structures.data.id": { - "$all": ["dummy/2019", "dijkstra1968"] + "contains": ["dummy/2019", "dijkstra1968"] } }, ) - self.assertEqual( - self.transform('structures.id HAS ONLY "dummy/2019"'), - { - "$and": [ - {"relationships.structures.data": {"$size": 1}}, - {"relationships.structures.data.id": {"$all": ["dummy/2019"]}}, - ] - }, - ) - - self.assertEqual( - self.transform( - 'structures.id HAS ONLY "dummy/2019" AND structures.id HAS "dummy/2019"' - ), - { - "$and": [ - { - "$and": [ - {"relationships.structures.data": {"$size": 1}}, - { - "relationships.structures.data.id": { - "$all": ["dummy/2019"] - } - }, - ] - }, - {"relationships.structures.data.id": {"$in": ["dummy/2019"]}}, - ], - }, - ) + # NOTE: HAS ONLY has not yet been implemented. + # self.assertEqual( + # self.transform('structures.id HAS ONLY "dummy/2019"'), + # { + # "and": [ + # {"relationships.structures.data": {"$size": 1}}, + # {"relationships.structures.data.id": { + # "contains": ["dummy/2019"]} + # }, + # ] + # }, + # ) + + # self.assertEqual( + # self.transform( + # 'structures.id HAS ONLY "dummy/2019" AND structures.id HAS ' + # '"dummy/2019"' + # ), + # { + # "and": [ + # { + # "and": [ + # {"relationships.structures.data": {"$size": 1}}, + # { + # "relationships.structures.data.id": { + # "contains": ["dummy/2019"] + # } + # }, + # ] + # }, + # {"relationships.structures.data.id": { + # "contains": ["dummy/2019"]} + # }, + # ], + # }, + # ) def test_not_implemented(self): - """ Test that list properties that are currently not implemented - give a sensible response. - - """ + """Test list properties that are currently not implemented give a sensible + response""" # NOTE: Lark catches underlying filtertransformer exceptions and # raises VisitErrors, most of these actually correspond to NotImplementedError with self.assertRaises(VisitError): try: self.transform("list HAS < 3") except Exception as exc: - self.assertTrue("not implemented" in str(exc)) + self.assertTrue("not been implemented" in repr(exc)) raise exc with self.assertRaises(VisitError): try: self.transform("list HAS ALL < 3, > 3") except Exception as exc: - self.assertTrue("not implemented" in str(exc)) + self.assertTrue("not been implemented" in repr(exc)) raise exc with self.assertRaises(VisitError): try: self.transform("list HAS ANY > 3, < 6") except Exception as exc: - self.assertTrue("not implemented" in str(exc)) + self.assertTrue("not been implemented" in repr(exc)) raise exc - self.assertEqual(self.transform("list LENGTH 3"), {"list": {"$size": 3}}) - with self.assertRaises(VisitError): self.transform("list:list HAS >=2:<=5") with self.assertRaises(VisitError): self.transform( - 'elements:_exmpl_element_counts HAS "H":6 AND elements:_exmpl_element_counts ' - 'HAS ALL "H":6,"He":7 AND elements:_exmpl_element_counts HAS ONLY "H":6 AND ' - 'elements:_exmpl_element_counts HAS ANY "H":6,"He":7 AND ' - 'elements:_exmpl_element_counts HAS ONLY "H":6,"He":7' + 'elements:_exmpl_element_counts HAS "H":6 AND elements:' + '_exmpl_element_counts HAS ALL "H":6,"He":7 AND elements:' + '_exmpl_element_counts HAS ONLY "H":6 AND elements:' + '_exmpl_element_counts HAS ANY "H":6,"He":7 AND elements:' + '_exmpl_element_counts HAS ONLY "H":6,"He":7' ) with self.assertRaises(VisitError): @@ -403,14 +437,12 @@ def test_not_implemented(self): 'HAS ANY > 3:"He":>55.3 , = 6:>"Ti":<37.6 , 8:<"Ga":0' ) - self.assertEqual( - self.transform("list LENGTH > 3"), {"list.4": {"$exists": True}} - ) - + @pytest.mark.skip("AiidaTransformer does not implement custom mapper") def test_list_length_aliases(self): + """Check LENGTH aliases for lists""" from optimade.server.mappers import StructureMapper - transformer = MongoTransformer(mapper=StructureMapper()) + transformer = AiidaTransformer(mapper=StructureMapper()) parser = LarkParser(version=self.version, variant=self.variant) self.assertEqual( @@ -421,56 +453,61 @@ def test_list_length_aliases(self): transformer.transform( parser.parse('elements HAS "Li" AND elements LENGTH = 3') ), - {"$and": [{"elements": {"$in": ["Li"]}}, {"nelements": 3}]}, + {"and": [{"elements": {"contains": ["Li"]}}, {"nelements": 3}]}, ) self.assertEqual( transformer.transform(parser.parse("elements LENGTH > 3")), - {"nelements": {"$gt": 3}}, + {"nelements": {">": 3}}, ) self.assertEqual( transformer.transform(parser.parse("elements LENGTH < 3")), - {"nelements": {"$lt": 3}}, + {"nelements": {"<": 3}}, ) self.assertEqual( transformer.transform(parser.parse("elements LENGTH = 3")), {"nelements": 3} ) self.assertEqual( transformer.transform(parser.parse("cartesian_site_positions LENGTH <= 3")), - {"nsites": {"$lte": 3}}, + {"nsites": {"<=": 3}}, ) self.assertEqual( transformer.transform(parser.parse("cartesian_site_positions LENGTH >= 3")), - {"nsites": {"$gte": 3}}, + {"nsites": {">=": 3}}, ) def test_unaliased_length_operator(self): + """Check unaliased LENGTH lists""" + + self.assertEqual( + self.transform("cartesian_site_positions LENGTH 3"), + {"cartesian_site_positions": {"of_length": 3}}, + ) self.assertEqual( self.transform("cartesian_site_positions LENGTH <= 3"), - {"cartesian_site_positions.4": {"$exists": False}}, + {"cartesian_site_positions": {"or": [{"shorter": 3}, {"of_length": 3}]},}, ) self.assertEqual( self.transform("cartesian_site_positions LENGTH < 3"), - {"cartesian_site_positions.3": {"$exists": False}}, + {"cartesian_site_positions": {"shorter": 3}}, ) self.assertEqual( - self.transform("cartesian_site_positions LENGTH 3"), - {"cartesian_site_positions": {"$size": 3}}, + self.transform("cartesian_site_positions LENGTH > 10"), + {"cartesian_site_positions": {"longer": 10}}, ) self.assertEqual( self.transform("cartesian_site_positions LENGTH >= 10"), - {"cartesian_site_positions.10": {"$exists": True}}, - ) - - self.assertEqual( - self.transform("cartesian_site_positions LENGTH > 10"), - {"cartesian_site_positions.11": {"$exists": True}}, + {"cartesian_site_positions": {"or": [{"longer": 10}, {"of_length": 10}]},}, ) + @pytest.mark.skip("AiidaTransformer does not implement custom mapper") def test_aliased_length_operator(self): + """Test LENGTH operator alias""" from optimade.server.mappers import StructureMapper class MyMapper(StructureMapper): + """Test mapper with LENGTH_ALIASES""" + ALIASES = (("elements", "my_elements"), ("nelements", "nelem")) LENGTH_ALIASES = ( ("chemsys", "nelements"), @@ -479,16 +516,16 @@ class MyMapper(StructureMapper): ) PROVIDER_FIELDS = ("chemsys",) - transformer = MongoTransformer(mapper=MyMapper()) + transformer = AiidaTransformer(mapper=MyMapper()) parser = LarkParser(version=self.version, variant=self.variant) self.assertEqual( transformer.transform(parser.parse("cartesian_site_positions LENGTH <= 3")), - {"nsites": {"$lte": 3}}, + {"nsites": {"<=": 3}}, ) self.assertEqual( transformer.transform(parser.parse("cartesian_site_positions LENGTH < 3")), - {"nsites": {"$lt": 3}}, + {"nsites": {"<": 3}}, ) self.assertEqual( transformer.transform(parser.parse("cartesian_site_positions LENGTH 3")), @@ -502,17 +539,17 @@ class MyMapper(StructureMapper): transformer.transform( parser.parse("cartesian_site_positions LENGTH >= 10") ), - {"nsites": {"$gte": 10}}, + {"nsites": {">=": 10}}, ) self.assertEqual( transformer.transform(parser.parse("structure_features LENGTH > 10")), - {"structure_features.11": {"$exists": True}}, + {"structure_features": {"longer": 10}}, ) self.assertEqual( transformer.transform(parser.parse("nsites LENGTH > 10")), - {"nsites.11": {"$exists": True}}, + {"nsites": {"longer": 10}}, ) self.assertEqual( @@ -521,20 +558,20 @@ class MyMapper(StructureMapper): self.assertEqual( transformer.transform(parser.parse('elements HAS "Ag"')), - {"my_elements": {"$in": ["Ag"]}}, + {"my_elements": {"contains": ["Ag"]}}, ) self.assertEqual( transformer.transform(parser.parse("chemsys LENGTH 3")), {"nelem": 3}, ) + @pytest.mark.skip("AiidaTransformer does not implement custom mapper") def test_aliases(self): - """ Test that valid aliases are allowed, but do not effect - r-values. - - """ + """Test that valid aliases are allowed, but do not affect r-values""" class MyStructureMapper(BaseResourceMapper): + """Test mapper with ALIASES""" + ALIASES = ( ("elements", "my_elements"), ("A", "D"), @@ -543,90 +580,103 @@ class MyStructureMapper(BaseResourceMapper): ) mapper = MyStructureMapper() - t = MongoTransformer(mapper=mapper) + transformer = AiidaTransformer(mapper=mapper) self.assertEqual(mapper.alias_for("elements"), "my_elements") - test_filter = {"elements": {"$in": ["A", "B", "C"]}} + test_filter = {"elements": {"contains": ["A", "B", "C"]}} self.assertEqual( - t.postprocess(test_filter), {"my_elements": {"$in": ["A", "B", "C"]}}, + transformer.postprocess(test_filter), + {"my_elements": {"contains": ["A", "B", "C"]}}, ) - test_filter = {"$and": [{"elements": {"$in": ["A", "B", "C"]}}]} + test_filter = {"and": [{"elements": {"contains": ["A", "B", "C"]}}]} self.assertEqual( - t.postprocess(test_filter), - {"$and": [{"my_elements": {"$in": ["A", "B", "C"]}}]}, + transformer.postprocess(test_filter), + {"and": [{"my_elements": {"contains": ["A", "B", "C"]}}]}, ) test_filter = {"elements": "A"} - self.assertEqual(t.postprocess(test_filter), {"my_elements": "A"}) + self.assertEqual(transformer.postprocess(test_filter), {"my_elements": "A"}) test_filter = ["A", "B", "C"] - self.assertEqual(t.postprocess(test_filter), ["A", "B", "C"]) + self.assertEqual(transformer.postprocess(test_filter), ["A", "B", "C"]) test_filter = ["A", "elements", "C"] - self.assertEqual(t.postprocess(test_filter), ["A", "elements", "C"]) + self.assertEqual(transformer.postprocess(test_filter), ["A", "elements", "C"]) def test_list_properties(self): - """ Test the HAS ALL, ANY and optional ONLY queries. - - """ - self.assertEqual( - self.transform('elements HAS ONLY "H","He","Ga","Ta"'), - {"elements": {"$all": ["H", "He", "Ga", "Ta"], "$size": 4}}, - ) + """Test the HAS ALL, ANY and optional ONLY queries""" + # NOTE: HAS ONLY has not yet been implemented. + # self.assertEqual( + # self.transform('elements HAS ONLY "H","He","Ga","Ta"'), + # {"elements": {"contains": ["H", "He", "Ga", "Ta"], "$size": 4}}, + # ) self.assertEqual( self.transform('elements HAS ANY "H","He","Ga","Ta"'), - {"elements": {"$in": ["H", "He", "Ga", "Ta"]}}, + { + "elements": { + "or": [ + {"contains": ["H"]}, + {"contains": ["He"]}, + {"contains": ["Ga"]}, + {"contains": ["Ta"]}, + ] + } + }, ) self.assertEqual( self.transform('elements HAS ALL "H","He","Ga","Ta"'), - {"elements": {"$all": ["H", "He", "Ga", "Ta"]}}, - ) - - self.assertEqual( - self.transform( - 'elements HAS "H" AND elements HAS ALL "H","He","Ga","Ta" AND elements HAS ' - 'ONLY "H","He","Ga","Ta" AND elements HAS ANY "H", "He", "Ga", "Ta"' - ), - { - "$and": [ - {"elements": {"$in": ["H"]}}, - {"elements": {"$all": ["H", "He", "Ga", "Ta"]}}, - {"elements": {"$all": ["H", "He", "Ga", "Ta"], "$size": 4}}, - {"elements": {"$in": ["H", "He", "Ga", "Ta"]}}, - ] - }, - ) + {"elements": {"contains": ["H", "He", "Ga", "Ta"]}}, + ) + + # self.assertEqual( + # self.transform( + # 'elements HAS "H" AND elements HAS ALL "H","He","Ga","Ta" AND ' + # 'elements HAS ONLY "H","He","Ga","Ta" AND elements HAS ANY "H", ' + # '"He", "Ga", "Ta"' + # ), + # { + # "and": [ + # {"elements": {"contains": ["H"]}}, + # {"elements": {"contains": ["H", "He", "Ga", "Ta"]}}, + # {"elements": {"contains": ["H", "He", "Ga", "Ta"], "$size": 4}}, + # {"elements": {"contains": ["H", "He", "Ga", "Ta"]}}, + # ] + # }, + # ) def test_properties(self): - # Filtering on Properties with unknown value - # TODO: {'$not': {'$exists': False}} can be simplified to {'$exists': True} - # The { $not: { $gt: 1.99 } } is different from the $lte operator. { $lte: 1.99 } returns only the documents - # where price field exists and its value is less than or equal to 1.99. - # Remember that the $not operator only affects other operators and cannot check fields and documents - # independently. So, use the $not operator for logical disjunctions and the $ne operator to test - # the contents of fields directly. + """Filtering on Properties with unknown value""" + # The { !and: [{ >: 1.99 }] } is different from the <= operator. + # { <=: 1.99 } returns only the documents where price field exists and its + # value is less than or equal to 1.99. + # Remember that the !and operator only affects other operators and cannot check + # fields and documents independently. + # So, use the !and operator for logical disjunctions and the !== operator to + # test the contents of fields directly. # source: https://docs.mongodb.com/manual/reference/operator/query/not/ self.assertEqual( self.transform( - "chemical_formula_hill IS KNOWN AND NOT chemical_formula_anonymous IS UNKNOWN" + "chemical_formula_hill IS KNOWN AND NOT chemical_formula_anonymous IS " + "UNKNOWN" ), { - "$and": [ - {"chemical_formula_hill": {"$exists": True}}, - {"chemical_formula_anonymous": {"$not": {"$exists": False}}}, + "and": [ + {"chemical_formula_hill": {"!==": None}}, + {"!and": [{"chemical_formula_anonymous": {"==": None}}]}, ] }, ) def test_precedence(self): + """Check OPERATOR precedence""" self.assertEqual( self.transform('NOT a > b OR c = 100 AND f = "C2 H6"'), { - "$or": [ - {"a": {"$not": {"$gt": "b"}}}, - {"$and": [{"c": {"$eq": 100}}, {"f": {"$eq": "C2 H6"}}]}, + "or": [ + {"!and": [{"a": {">": "b"}}]}, + {"and": [{"c": {"==": 100}}, {"f": {"==": "C2 H6"}}]}, ] }, ) @@ -640,20 +690,21 @@ def test_precedence(self): ) def test_special_cases(self): - self.assertEqual(self.transform("te < st"), {"te": {"$lt": "st"}}) + """Check special cases""" + self.assertEqual(self.transform("te < st"), {"te": {"<": "st"}}) self.assertEqual( - self.transform('spacegroup="P2"'), {"spacegroup": {"$eq": "P2"}} + self.transform('spacegroup="P2"'), {"spacegroup": {"==": "P2"}} ) self.assertEqual( self.transform("_cod_cell_volume<100.0"), - {"_cod_cell_volume": {"$lt": 100.0}}, + {"_cod_cell_volume": {"<": 100.0}}, ) self.assertEqual( self.transform("_mp_bandgap > 5.0 AND _cod_molecular_weight < 350"), { - "$and": [ - {"_mp_bandgap": {"$gt": 5.0}}, - {"_cod_molecular_weight": {"$lt": 350}}, + "and": [ + {"_mp_bandgap": {">": 5.0}}, + {"_cod_molecular_weight": {"<": 350}}, ] }, ) @@ -662,20 +713,20 @@ def test_special_cases(self): '_cod_melting_point<300 AND nelements=4 AND elements="Si,O2"' ), { - "$and": [ - {"_cod_melting_point": {"$lt": 300}}, - {"nelements": {"$eq": 4}}, - {"elements": {"$eq": "Si,O2"}}, + "and": [ + {"_cod_melting_point": {"<": 300}}, + {"nelements": {"==": 4}}, + {"elements": {"==": "Si,O2"}}, ] }, ) - self.assertEqual(self.transform("key=value"), {"key": {"$eq": "value"}}) + self.assertEqual(self.transform("key=value"), {"key": {"==": "value"}}) self.assertEqual( - self.transform('author=" someone "'), {"author": {"$eq": " someone "}} + self.transform('author=" someone "'), {"author": {"==": " someone "}} ) - self.assertEqual(self.transform("notice=val"), {"notice": {"$eq": "val"}}) + self.assertEqual(self.transform("notice=val"), {"notice": {"==": "val"}}) self.assertEqual( - self.transform("NOTice=val"), {"ice": {"$not": {"$eq": "val"}}} + self.transform("NOTice=val"), {"!and": [{"ice": {"==": "val"}}]} ) self.assertEqual( self.transform( @@ -683,15 +734,15 @@ def test_special_cases(self): "number=0e1ANDnumber=0e-1ANDnumber=0e+1" ), { - "$and": [ - {"number": {"$eq": 0.0}}, - {"number": {"$eq": 0.0}}, - {"number": {"$eq": 0.0}}, - {"number": {"$eq": 0}}, - {"_n_u_m_b_e_r_": {"$eq": 0}}, - {"number": {"$eq": 0.0}}, - {"number": {"$eq": 0.0}}, - {"number": {"$eq": 0.0}}, + "and": [ + {"number": {"==": 0.0}}, + {"number": {"==": 0.0}}, + {"number": {"==": 0.0}}, + {"number": {"==": 0}}, + {"_n_u_m_b_e_r_": {"==": 0}}, + {"number": {"==": 0.0}}, + {"number": {"==": 0.0}}, + {"number": {"==": 0.0}}, ] }, ) diff --git a/tests/server/routers/test_info.py b/tests/server/routers/test_info.py index 987eb4e7..fa3094f7 100644 --- a/tests/server/routers/test_info.py +++ b/tests/server/routers/test_info.py @@ -1,17 +1,19 @@ # pylint: disable=relative-beyond-top-level import unittest -from optimade.models import InfoResponse, EntryInfoResponse, IndexInfoResponse +from optimade.models import InfoResponse, EntryInfoResponse from ..utils import EndpointTestsMixin class InfoEndpointTests(EndpointTestsMixin, unittest.TestCase): + """Tests for /info""" request_str = "/info" response_cls = InfoResponse def test_info_endpoint_attributes(self): + """Check known properties/attributes for successful response""" self.assertTrue("data" in self.json_response) self.assertEqual(self.json_response["data"]["type"], "info") self.assertEqual(self.json_response["data"]["id"], "/") @@ -27,43 +29,20 @@ def test_info_endpoint_attributes(self): class InfoStructuresEndpointTests(EndpointTestsMixin, unittest.TestCase): + """Tests for /info/structures""" request_str = "/info/structures" response_cls = EntryInfoResponse def test_info_structures_endpoint_data(self): + """Check known properties/attributes for successful response""" self.assertTrue("data" in self.json_response) data_keys = ["description", "properties", "formats", "output_fields_by_format"] self.check_keys(data_keys, self.json_response["data"]) class InfoReferencesEndpointTests(EndpointTestsMixin, unittest.TestCase): + """Tests for /info/references""" request_str = "/info/references" response_cls = EntryInfoResponse - - -class IndexInfoEndpointTests(EndpointTestsMixin, unittest.TestCase): - - server = "index" - request_str = "/info" - response_cls = IndexInfoResponse - - def test_info_endpoint_attributes(self): - self.assertTrue("data" in self.json_response) - self.assertEqual(self.json_response["data"]["type"], "info") - self.assertEqual(self.json_response["data"]["id"], "/") - self.assertTrue("attributes" in self.json_response["data"]) - attributes = [ - "api_version", - "available_api_versions", - "formats", - "entry_types_by_format", - "available_endpoints", - "is_index", - ] - self.check_keys(attributes, self.json_response["data"]["attributes"]) - self.assertTrue("relationships" in self.json_response["data"]) - relationships = ["default"] - self.check_keys(relationships, self.json_response["data"]["relationships"]) - self.assertTrue(len(self.json_response["data"]["relationships"]["default"]), 1) diff --git a/tests/server/routers/test_links.py b/tests/server/routers/test_links.py index b42c708a..4ca64f48 100644 --- a/tests/server/routers/test_links.py +++ b/tests/server/routers/test_links.py @@ -1,4 +1,3 @@ -# pylint: disable=relative-beyond-top-level import unittest from optimade.models import LinksResponse @@ -7,13 +6,7 @@ class LinksEndpointTests(EndpointTestsMixin, unittest.TestCase): + """Tests for /links""" request_str = "/links" response_cls = LinksResponse - - -class IndexLinksEndpointTests(EndpointTestsMixin, unittest.TestCase): - - server = "index" - request_str = "/links" - response_cls = LinksResponse diff --git a/tests/server/routers/test_references.py b/tests/server/routers/test_references.py index 8ecf5cb8..85c29f15 100644 --- a/tests/server/routers/test_references.py +++ b/tests/server/routers/test_references.py @@ -1,18 +1,22 @@ -# pylint: disable=relative-beyond-top-level import unittest +import pytest from optimade.models import ReferenceResponseMany, ReferenceResponseOne from ..utils import EndpointTestsMixin +pytestmark = pytest.mark.skip("References has not yet been implemented") + class ReferencesEndpointTests(EndpointTestsMixin, unittest.TestCase): + """Tests for /references""" request_str = "/references" response_cls = ReferenceResponseMany class SingleReferenceEndpointTests(EndpointTestsMixin, unittest.TestCase): + """Tests for /references/""" test_id = "dijkstra1968" request_str = f"/references/{test_id}" @@ -20,7 +24,25 @@ class SingleReferenceEndpointTests(EndpointTestsMixin, unittest.TestCase): class SingleReferenceEndpointTestsDifficult(EndpointTestsMixin, unittest.TestCase): + """Tests for /references/, + where contains difficult characters""" test_id = "dummy/20.19" request_str = f"/references/{test_id}" response_cls = ReferenceResponseOne + + +class MissingSingleReferenceEndpointTests(EndpointTestsMixin, unittest.TestCase): + """Tests for /references/ for unknown """ + + test_id = "random_string_that_is_not_in_test_data" + request_str = f"/references/{test_id}" + response_cls = ReferenceResponseOne + + def test_references_endpoint_data(self): + """Check known properties/attributes for successful response""" + self.assertTrue("data" in self.json_response) + self.assertTrue("meta" in self.json_response) + self.assertEqual(self.json_response["data"], None) + self.assertEqual(self.json_response["meta"]["data_returned"], 0) + self.assertEqual(self.json_response["meta"]["more_data_available"], False) diff --git a/tests/server/routers/test_structures.py b/tests/server/routers/test_structures.py index 67884e79..72d8055d 100644 --- a/tests/server/routers/test_structures.py +++ b/tests/server/routers/test_structures.py @@ -1,4 +1,3 @@ -# pylint: disable=relative-beyond-top-level import unittest from optimade.models import ( @@ -11,57 +10,60 @@ class StructuresEndpointTests(EndpointTestsMixin, unittest.TestCase): + """Tests for /structures""" request_str = "/structures" response_cls = StructureResponseMany def test_structures_endpoint_data(self): - self.assertTrue("meta" in self.json_response) - self.assertEqual(self.json_response["meta"]["data_available"], 17) - self.assertEqual(self.json_response["meta"]["more_data_available"], False) - self.assertTrue("data" in self.json_response) - self.assertEqual( - len(self.json_response["data"]), - self.json_response["meta"]["data_available"], - ) + """Check known properties/attributes for successful response""" + assert "data" in self.json_response + + # Should match page_limit in test_config.json + assert len(self.json_response["data"]) == 15 + + assert "meta" in self.json_response + assert self.json_response["meta"]["data_available"] == 1089 + assert self.json_response["meta"]["more_data_available"] def test_get_next_responses(self): + """Check pagination""" total_data = self.json_response["meta"]["data_available"] page_limit = 5 response = self.client.get(self.request_str + f"?page_limit={page_limit}") json_response = response.json() - self.assertEqual( - self.response.status_code, - 200, - msg=f"Request failed: {self.response.json()}", - ) + assert response.status_code == 200, f"Request failed: {response.json()}" cursor = json_response["data"].copy() - self.assertTrue(json_response["meta"]["more_data_available"]) + assert json_response["meta"]["more_data_available"] more_data_available = True next_request = json_response["links"]["next"] - while more_data_available: + id_ = len(cursor) + while more_data_available and id_ < page_limit * 3: next_response = self.client.get(next_request).json() next_request = next_response["links"]["next"] cursor.extend(next_response["data"]) more_data_available = next_response["meta"]["more_data_available"] if more_data_available: - self.assertEqual(len(next_response["data"]), page_limit) + assert len(next_response["data"]) == page_limit else: - self.assertEqual(len(next_response["data"]), total_data % page_limit) + assert len(next_response["data"]) == total_data % page_limit + id_ += len(next_response["data"]) - self.assertEqual(len(cursor), total_data) + assert len(cursor) == id_ class SingleStructureEndpointTests(EndpointTestsMixin, unittest.TestCase): + """Tests for /structures/""" test_id = "mpf_1" request_str = f"/structures/{test_id}" response_cls = StructureResponseOne def test_structures_endpoint_data(self): + """Check known properties/attributes for successful response""" self.assertTrue("data" in self.json_response) self.assertEqual(self.json_response["data"]["id"], self.test_id) self.assertEqual(self.json_response["data"]["type"], "structures") @@ -70,12 +72,14 @@ def test_structures_endpoint_data(self): class MissingSingleStructureEndpointTests(EndpointTestsMixin, unittest.TestCase): + """Tests for /structures/ for unknown """ test_id = "mpf_random_string_that_is_not_in_test_data" request_str = f"/structures/{test_id}" response_cls = StructureResponseOne def test_structures_endpoint_data(self): + """Check known properties/attributes for successful response""" self.assertTrue("data" in self.json_response) self.assertTrue("meta" in self.json_response) self.assertEqual(self.json_response["data"], None) @@ -84,12 +88,14 @@ def test_structures_endpoint_data(self): class SingleStructureWithRelationshipsTests(EndpointTestsMixin, unittest.TestCase): + """Tests for /structures/, where has relationships""" test_id = "mpf_1" request_str = f"/structures/{test_id}" response_cls = StructureResponseOne def test_structures_endpoint_data(self): + """Check known properties/attributes for successful response""" self.assertTrue("data" in self.json_response) self.assertEqual(self.json_response["data"]["id"], self.test_id) self.assertEqual(self.json_response["data"]["type"], "structures") @@ -109,12 +115,15 @@ def test_structures_endpoint_data(self): class MultiStructureWithSharedRelationshipsTests(EndpointTestsMixin, unittest.TestCase): + """Tests for /structures for entries with shared relationships""" - request_str = f"/structures?filter=id=mpf_1 OR id=mpf_2" + request_str = "/structures?filter=id=mpf_1 OR id=mpf_2" response_cls = StructureResponseMany def test_structures_endpoint_data(self): - # mpf_1 and mpf_2 both contain the same reference relationship, so response should not duplicate it + """Check known properties/attributes for successful response""" + # mpf_1 and mpf_2 both contain the same reference relationship, + # so the response should not duplicate it self.assertTrue("data" in self.json_response) self.assertEqual(len(self.json_response["data"]), 2) self.assertTrue("included" in self.json_response) @@ -122,12 +131,14 @@ def test_structures_endpoint_data(self): class MultiStructureWithRelationshipsTests(EndpointTestsMixin, unittest.TestCase): + """Tests for /structures for mixed entries with and without relationships""" - request_str = f"/structures?filter=id=mpf_1 OR id=mpf_23" + request_str = "/structures?filter=id=mpf_1 OR id=mpf_23" response_cls = StructureResponseMany def test_structures_endpoint_data(self): - # mpf_4 contains no relationships, which shouldn't break anything + """Check known properties/attributes for successful response""" + # mpf_23 contains no relationships, which shouldn't break anything self.assertTrue("data" in self.json_response) self.assertEqual(len(self.json_response["data"]), 2) self.assertTrue("included" in self.json_response) @@ -137,23 +148,18 @@ def test_structures_endpoint_data(self): class MultiStructureWithOverlappingRelationshipsTests( EndpointTestsMixin, unittest.TestCase ): + """Tests for /structures with entries with overlapping relationships + + One entry has multiple relationships, another entry has other relationships, + some of these relationships overlap between the entries, others don't. + """ - request_str = f"/structures?filter=id=mpf_1 OR id=mpf_3" + request_str = "/structures?filter=id=mpf_1 OR id=mpf_3" response_cls = StructureResponseMany def test_structures_endpoint_data(self): + """Check known properties/attributes for successful response""" self.assertTrue("data" in self.json_response) self.assertEqual(len(self.json_response["data"]), 2) self.assertTrue("included" in self.json_response) self.assertEqual(len(self.json_response["included"]), 2) - - -class SingleStructureEndpointEmptyTest(EndpointTestsMixin, unittest.TestCase): - - test_id = "non_existent_id" - request_str = f"/structures/{test_id}" - response_cls = StructureResponseOne - - def test_structures_endpoint_data(self): - self.assertTrue("data" in self.json_response) - self.assertEqual(self.json_response["data"], None) diff --git a/tests/server/test_config.py b/tests/server/test_config.py deleted file mode 100644 index 497b245f..00000000 --- a/tests/server/test_config.py +++ /dev/null @@ -1,156 +0,0 @@ -# pylint: disable=protected-access,pointless-statement,relative-beyond-top-level -import json -import os -import unittest - -from pathlib import Path - -from optimade.server.config import ServerConfig, CONFIG - -from .utils import SetClient - - -class ConfigTests(unittest.TestCase): - def test_env_variable(self): - """Set OPTIMADE_DEBUG environment variable and check CONFIG picks up on it correctly""" - os.environ["OPTIMADE_DEBUG"] = "true" - CONFIG = ServerConfig() - self.assertTrue(CONFIG.debug) - - os.environ.pop("OPTIMADE_DEBUG", None) - CONFIG = ServerConfig() - self.assertFalse(CONFIG.debug) - - def test_default_config_path(self): - """Make sure the default config path works - Expected default config path: PATH/TO/USER/HOMEDIR/.optimade.json - """ - # Reset OPTIMADE_CONFIG_FILE - original_OPTIMADE_CONFIG_FILE = os.environ.get("OPTIMADE_CONFIG_FILE", "") - os.environ.pop("OPTIMADE_CONFIG_FILE") - - with open( - Path(__file__).parent.parent.joinpath("test_config.json"), "r" - ) as config_file: - config = json.load(config_file) - - different_base_url = "http://something_you_will_never_think_of.com" - config["base_url"] = different_base_url - - # Try-finally to make sure we don't overwrite possible existing `.optimade.json` - original = Path.home().joinpath(".optimade.json") - restore = False - if original.exists(): - restore = True - with open(original, "rb") as original_file: - original_file_content = original_file.read() - try: - with open( - Path.home().joinpath(".optimade.json"), "w" - ) as default_config_file: - json.dump(config, default_config_file) - CONFIG = ServerConfig() - self.assertEqual( - CONFIG.base_url, - different_base_url, - f"\nDumped file content:\n{config}.\n\nLoaded CONFIG:\n{CONFIG}", - ) - finally: - if restore: - with open(original, "wb") as original_file: - original_file.write(original_file_content) - - # Restore OPTIMADE_CONFIG_FILE - os.environ["OPTIMADE_CONFIG_FILE"] = original_OPTIMADE_CONFIG_FILE - - -class TestRegularServerConfig(SetClient, unittest.TestCase): - - server = "regular" - - def test_debug_is_respected_when_off(self): - """Make sure traceback is toggleable according to debug mode - here OFF - - TODO: This should be moved to a separate test file that tests the exception handlers. - """ - if CONFIG.debug: - CONFIG.debug = False - - response = self.client.get("/non/existent/path") - self.assertEqual( - response.status_code, - 404, - msg=f"Request should have failed, but didn't: {response.json()}", - ) - - response = response.json() - self.assertNotIn("data", response) - self.assertIn("meta", response) - - self.assertNotIn(f"_{CONFIG.provider.prefix}_traceback", response["meta"]) - - def test_debug_is_respected_when_on(self): - """Make sure traceback is toggleable according to debug mode - here ON - - TODO: This should be moved to a separate test file that tests the exception handlers. - """ - CONFIG.debug = True - - response = self.client.get("/non/existent/path") - self.assertEqual( - response.status_code, - 404, - msg=f"Request should have failed, but didn't: {response.json()}", - ) - - response = response.json() - self.assertNotIn("data", response) - self.assertIn("meta", response) - - self.assertIn(f"_{CONFIG.provider.prefix}_traceback", response["meta"]) - - -class TestRegularIndexServerConfig(SetClient, unittest.TestCase): - - server = "index" - - def test_debug_is_respected_when_off(self): - """Make sure traceback is toggleable according to debug mode - here OFF - - TODO: This should be moved to a separate test file that tests the exception handlers. - """ - if CONFIG.debug: - CONFIG.debug = False - - response = self.client.get("/non/existent/path") - self.assertEqual( - response.status_code, - 404, - msg=f"Request should have failed, but didn't: {response.json()}", - ) - - response = response.json() - self.assertNotIn("data", response) - self.assertIn("meta", response) - - self.assertNotIn(f"_{CONFIG.provider.prefix}_traceback", response["meta"]) - - def test_debug_is_respected_when_on(self): - """Make sure traceback is toggleable according to debug mode - here OFF - - TODO: This should be moved to a separate test file that tests the exception handlers. - """ - CONFIG.debug = True - - response = self.client.get("/non/existent/path") - self.assertEqual( - response.status_code, - 404, - msg=f"Request should have failed, but didn't: {response.json()}", - ) - - response = response.json() - self.assertNotIn("data", response) - self.assertIn("meta", response) - - self.assertIn(f"_{CONFIG.provider.prefix}_traceback", response["meta"]) diff --git a/tests/server/test_mappers.py b/tests/server/test_mappers.py deleted file mode 100644 index ebc33a2f..00000000 --- a/tests/server/test_mappers.py +++ /dev/null @@ -1,18 +0,0 @@ -# pylint: disable=relative-beyond-top-level,import-outside-toplevel -import unittest -import mongomock - -from optimade.server.mappers import BaseResourceMapper -from optimade.server.entry_collections import MongoCollection -from optimade.models import StructureResource - - -class ResourceMapperTests(unittest.TestCase): - def test_disallowed_aliases(self): - class MyMapper(BaseResourceMapper): - ALIASES = (("$and", "my_special_and"), ("not", "$not")) - - mapper = MyMapper() - toy_collection = mongomock.MongoClient()["fake"]["fake"] - with self.assertRaises(RuntimeError): - MongoCollection(toy_collection, StructureResource, mapper) diff --git a/tests/server/test_middleware.py b/tests/server/test_middleware.py index 7b1dc4b6..a8ed54d6 100644 --- a/tests/server/test_middleware.py +++ b/tests/server/test_middleware.py @@ -68,7 +68,7 @@ class EnsureQueryParamIntegrityTest(SetClient, unittest.TestCase): server = "regular" - def _check_error_response( + def check_error_response( self, request: str, expected_status: int = None, @@ -77,7 +77,7 @@ def _check_error_response( ): expected_status = 400 if expected_status is None else expected_status expected_title = "Bad Request" if expected_title is None else expected_title - super()._check_error_response( + super().check_error_response( request, expected_status, expected_title, expected_detail ) @@ -88,7 +88,7 @@ def test_wrong_html_form(self): for valid_query_parameter in EntryListingQueryParams().__dict__: request = f"/structures?{valid_query_parameter}" with self.assertRaises(BadRequest): - self._check_error_response( + self.check_error_response( request, expected_detail="A query parameter without an equal sign (=) is not supported by this server", ) @@ -100,7 +100,7 @@ def test_wrong_html_form_one_wrong(self): """ request = f"/structures?filter&include=;response_format=json" with self.assertRaises(BadRequest): - self._check_error_response( + self.check_error_response( request, expected_detail="A query parameter without an equal sign (=) is not supported by this server", ) diff --git a/tests/server/test_query_params.py b/tests/server/test_query_params.py deleted file mode 100644 index 3dba2477..00000000 --- a/tests/server/test_query_params.py +++ /dev/null @@ -1,560 +0,0 @@ -# pylint: disable=relative-beyond-top-level,import-outside-toplevel -import unittest -from typing import Union, List, Set - -from optimade.server.config import CONFIG -from optimade.server import mappers -from optimade.server.entry_collections import CI_FORCE_MONGO - -from .utils import SetClient - -MONGOMOCK_OLD = False -MONGOMOCK_MSG = "" -if not CI_FORCE_MONGO and not CONFIG.use_real_mongo: - import mongomock - - MONGOMOCK_OLD = tuple( - int(val) for val in mongomock.__version__.split(".")[0:3] - ) <= (3, 19, 0) - MONGOMOCK_MSG = f"mongomock version {mongomock.__version__}<=3.19.0 is too old for this test, skipping..." - - -class IncludeTests(SetClient, unittest.TestCase): - """Make sure `include` is handled correctly - - NOTE: Currently _only_ structures have relationships (references). - """ - - server = "regular" - - def _check_response( - self, - request: str, - expected_included_types: Union[List, Set], - expected_included_resources: Union[List, Set], - expected_relationship_types: Union[List, Set] = None, - ): - try: - response = self.client.get(request) - self.assertEqual( - response.status_code, 200, msg=f"Request failed: {response.json()}" - ) - - response = response.json() - response_data = ( - response["data"] - if isinstance(response["data"], list) - else [response["data"]] - ) - - included_resource_types = list({_["type"] for _ in response["included"]}) - self.assertEqual( - sorted(expected_included_types), - sorted(included_resource_types), - msg=f"Expected relationship types: {expected_included_types}. " - f"Does not match relationship types in response's included field: {included_resource_types}", - ) - - if expected_relationship_types is None: - expected_relationship_types = expected_included_types - relationship_types = set() - for entry in response_data: - relationship_types.update(set(entry.get("relationships", {}).keys())) - self.assertEqual( - sorted(expected_relationship_types), - sorted(relationship_types), - msg=f"Expected relationship types: {expected_relationship_types}. " - f"Does not match relationship types found in response data: {relationship_types}", - ) - - included_resources = [_["id"] for _ in response["included"]] - self.assertEqual( - len(included_resources), - len(expected_included_resources), - msg=response["included"], - ) - self.assertEqual( - sorted(set(included_resources)), sorted(expected_included_resources) - ) - - except Exception as exc: - print("Request attempted:") - print(f"{self.client.base_url}{request}") - raise exc - - def _check_error_response( - self, - request: str, - expected_status: int = None, - expected_title: str = None, - expected_detail: str = None, - ): - expected_status = 400 if expected_status is None else expected_status - expected_title = "Bad Request" if expected_title is None else expected_title - super()._check_error_response( - request, expected_status, expected_title, expected_detail - ) - - def test_default_value(self): - """Default value for `include` is 'references' - - Test also that passing `include=` equals passing the default value - """ - request = "/structures" - expected_types = ["references"] - expected_reference_ids = ["dijkstra1968", "maddox1988", "dummy/2019"] - self._check_response(request, expected_types, expected_reference_ids) - - def test_empty_value(self): - """An empty value should resolve in no relationships being returned under `included`""" - request = "/structures?include=" - expected_types = [] - expected_reference_ids = [] - expected_data_relationship_types = ["references"] - self._check_response( - request, - expected_types, - expected_reference_ids, - expected_data_relationship_types, - ) - - def test_default_value_single_entry(self): - """For single entry. Default value for `include` is 'references'""" - request = "/structures/mpf_1" - expected_types = ["references"] - expected_reference_ids = ["dijkstra1968"] - self._check_response(request, expected_types, expected_reference_ids) - - def test_empty_value_single_entry(self): - """For single entry. An empty value should resolve in no relationships being returned under `included`""" - request = "/structures/mpf_1?include=" - expected_types = [] - expected_reference_ids = [] - expected_data_relationship_types = ["references"] - self._check_response( - request, - expected_types, - expected_reference_ids, - expected_data_relationship_types, - ) - - def test_wrong_relationship_type(self): - """A wrong type should result in a `400 Bad Request` response""" - from optimade.server.routers import ENTRY_COLLECTIONS - - for wrong_type in ("test", '""', "''"): - request = f"/structures?include={wrong_type}" - error_detail = ( - f"'{wrong_type}' cannot be identified as a valid relationship type. " - f"Known relationship types: {sorted(ENTRY_COLLECTIONS.keys())}" - ) - self._check_error_response(request, expected_detail=error_detail) - - -class ResponseFieldTests(SetClient, unittest.TestCase): - """Make sure response_fields is handled correctly""" - - server = "regular" - - get_mapper = { - "links": mappers.LinksMapper, - "references": mappers.ReferenceMapper, - "structures": mappers.StructureMapper, - } - - def required_fields_test_helper( - self, endpoint: str, known_unused_fields: set, expected_fields: set - ): - """Utility function for creating required fields tests""" - expected_fields |= ( - self.get_mapper[endpoint].get_required_fields() - known_unused_fields - ) - expected_fields.add("attributes") - request = f"/{endpoint}?response_fields={','.join(expected_fields)}" - - # Check response - try: - response = self.client.get(request) - self.assertEqual( - response.status_code, 200, msg=f"Request failed: {response.json()}" - ) - - response = response.json() - response_fields = set() - for entry in response["data"]: - response_fields.update(set(entry.keys())) - response_fields.update(set(entry["attributes"].keys())) - self.assertEqual(sorted(expected_fields), sorted(response_fields)) - except Exception as exc: - print("Request attempted:") - print(f"{self.client.base_url}{request}") - raise exc - - def test_required_fields_links(self): - """Certain fields are REQUIRED, no matter the value of `response_fields`""" - endpoint = "links" - illegal_top_level_field = "relationships" - non_used_top_level_fields = {"links"} - non_used_top_level_fields.add(illegal_top_level_field) - expected_fields = {"homepage", "base_url"} - self.required_fields_test_helper( - endpoint, non_used_top_level_fields, expected_fields - ) - - def test_required_fields_references(self): - """Certain fields are REQUIRED, no matter the value of `response_fields`""" - endpoint = "references" - non_used_top_level_fields = {"links", "relationships"} - expected_fields = {"year", "journal"} - self.required_fields_test_helper( - endpoint, non_used_top_level_fields, expected_fields - ) - - def test_required_fields_structures(self): - """Certain fields are REQUIRED, no matter the value of `response_fields`""" - endpoint = "structures" - non_used_top_level_fields = {"links"} - expected_fields = {"elements", "nelements"} - self.required_fields_test_helper( - endpoint, non_used_top_level_fields, expected_fields - ) - - -class FilterTests(SetClient, unittest.TestCase): - - server = "regular" - - def test_custom_field(self): - request = '/structures?filter=_exmpl_chemsys="Ac"' - expected_ids = ["mpf_1"] - self._check_response(request, expected_ids, len(expected_ids)) - - def test_id(self): - request = "/structures?filter=id=mpf_2" - expected_ids = ["mpf_2"] - self._check_response(request, expected_ids, len(expected_ids)) - - def test_geq(self): - request = "/structures?filter=nelements>=9" - expected_ids = ["mpf_3819"] - self._check_response(request, expected_ids, len(expected_ids)) - - def test_gt(self): - request = "/structures?filter=nelements>8" - expected_ids = ["mpf_3819"] - self._check_response(request, expected_ids, len(expected_ids)) - - def test_rhs_comparison(self): - request = "/structures?filter=83.19.0 has been released, which should - contain the bugfix for this: https://github.com/mongomock/mongomock/pull/597. - - """ - - request = '/structures?filter=elements HAS ONLY "Ac", "Mg"' - expected_ids = ["mpf_23"] - self._check_response(request, expected_ids, len(expected_ids)) - - request = '/structures?filter=elements HAS ONLY "Ac"' - expected_ids = ["mpf_1"] - self._check_response(request, expected_ids, len(expected_ids)) - - def test_list_correlated(self): - request = '/structures?filter=elements:elements_ratios HAS "Ag":"0.2"' - self._check_error_response( - request, expected_status=501, expected_title="NotImplementedError" - ) - # expected_ids = ["mpf_259"] - # self._check_response(request, expected_ids, len(expected_ids)) - - def test_is_known(self): - request = "/structures?filter=nsites IS KNOWN AND nsites>=44" - expected_ids = ["mpf_551", "mpf_3803", "mpf_3819"] - self._check_response(request, expected_ids, len(expected_ids)) - - request = "/structures?filter=lattice_vectors IS KNOWN AND nsites>=44" - expected_ids = ["mpf_551", "mpf_3803", "mpf_3819"] - self._check_response(request, expected_ids, len(expected_ids)) - - def test_aliased_is_known(self): - request = "/structures?filter=id IS KNOWN AND nsites>=44" - expected_ids = ["mpf_551", "mpf_3803", "mpf_3819"] - self._check_response(request, expected_ids, len(expected_ids)) - - request = "/structures?filter=chemical_formula_reduced IS KNOWN AND nsites>=44" - expected_ids = ["mpf_551", "mpf_3803", "mpf_3819"] - self._check_response(request, expected_ids, len(expected_ids)) - - request = ( - "/structures?filter=chemical_formula_descriptive IS KNOWN AND nsites>=44" - ) - expected_ids = ["mpf_551", "mpf_3803", "mpf_3819"] - self._check_response(request, expected_ids, len(expected_ids)) - - def test_aliased_fields(self): - request = '/structures?filter=chemical_formula_anonymous="A"' - expected_ids = ["mpf_1", "mpf_200"] - self._check_response(request, expected_ids, len(expected_ids)) - - request = '/structures?filter=chemical_formula_anonymous CONTAINS "A2BC"' - expected_ids = ["mpf_2", "mpf_3", "mpf_110"] - self._check_response(request, expected_ids, len(expected_ids)) - - def test_string_contains(self): - request = '/structures?filter=chemical_formula_descriptive CONTAINS "c2Ag"' - expected_ids = ["mpf_3", "mpf_2"] - self._check_response(request, expected_ids, len(expected_ids)) - - def test_string_start(self): - request = ( - '/structures?filter=chemical_formula_descriptive STARTS WITH "Ag2CSNCl"' - ) - expected_ids = ["mpf_259"] - self._check_response(request, expected_ids, len(expected_ids)) - - def test_string_end(self): - request = '/structures?filter=chemical_formula_descriptive ENDS WITH "NClO4"' - expected_ids = ["mpf_259"] - self._check_response(request, expected_ids, len(expected_ids)) - - def test_list_has_and(self): - request = '/structures?filter=elements HAS "Ac" AND nelements=1' - expected_ids = ["mpf_1"] - self._check_response(request, expected_ids, len(expected_ids)) - - def test_awkward_not_queries(self): - """ Test an awkward query from the spec examples. It should return all but 2 structures - in the test data. The test is done in three parts: - - - first query the individual expressions that make up the OR, - - then do an empty query to get all IDs - - then negate the expressions and ensure that all IDs are returned except - those from the first queries. - - """ - expected_ids = ["mpf_3819"] - request = ( - '/structures?filter=chemical_formula_descriptive="Ba2NaTi2MnRe2Si8HO26F" AND ' - 'chemical_formula_anonymous = "A26B8C2D2E2FGHI" ' - ) - self._check_response(request, expected_ids, len(expected_ids)) - - expected_ids = ["mpf_2"] - request = ( - '/structures?filter=chemical_formula_anonymous = "A2BC" AND ' - 'NOT chemical_formula_descriptive = "Ac2AgPb" ' - ) - self._check_response(request, expected_ids, len(expected_ids)) - - request = "/structures" - unexpected_ids = ["mpf_3819", "mpf_2"] - expected_ids = [ - structure["id"] - for structure in self.client.get(request).json()["data"] - if structure["id"] not in unexpected_ids - ] - - request = ( - "/structures?filter=" - "NOT ( " - 'chemical_formula_descriptive = "Ba2NaTi2MnRe2Si8HO26F" AND ' - 'chemical_formula_anonymous = "A26B8C2D2E2FGHI" OR ' - 'chemical_formula_anonymous = "A2BC" AND ' - 'NOT chemical_formula_descriptive = "Ac2AgPb" ' - ")" - ) - self._check_response(request, expected_ids, len(expected_ids)) - - def test_not_or_and_precedence(self): - request = '/structures?filter=NOT elements HAS "Ac" AND nelements=1' - expected_ids = ["mpf_200"] - self._check_response(request, expected_ids, len(expected_ids)) - - request = '/structures?filter=nelements=1 AND NOT elements HAS "Ac"' - expected_ids = ["mpf_200"] - self._check_response(request, expected_ids, len(expected_ids)) - - request = '/structures?filter=NOT elements HAS "Ac" AND nelements=1 OR nsites=1' - expected_ids = ["mpf_1", "mpf_200"] - self._check_response(request, expected_ids, len(expected_ids)) - - request = '/structures?filter=elements HAS "Ac" AND nelements>1 AND nsites=1' - expected_ids = [] - self._check_response(request, expected_ids, len(expected_ids)) - - def test_brackets(self): - request = '/structures?filter=elements HAS "Ac" AND nelements=1 OR nsites=1' - expected_ids = ["mpf_200", "mpf_1"] - self._check_response(request, expected_ids, len(expected_ids)) - - request = '/structures?filter=(elements HAS "Ac" AND nelements=1) OR (elements HAS "Ac" AND nsites=1)' - expected_ids = ["mpf_1"] - self._check_response(request, expected_ids, len(expected_ids)) - - def test_filter_on_relationships(self): - request = '/structures?filter=references.id HAS "dummy/2019"' - expected_ids = ["mpf_3819"] - self._check_response(request, expected_ids, len(expected_ids)) - - request = ( - '/structures?filter=references.id HAS ANY "dummy/2019", "dijkstra1968"' - ) - expected_ids = ["mpf_1", "mpf_2", "mpf_3819"] - self._check_response(request, expected_ids, len(expected_ids)) - - request = '/structures?filter=references.id HAS ONLY "dijkstra1968"' - expected_ids = ["mpf_1", "mpf_2"] - self._check_response(request, expected_ids, len(expected_ids)) - - request = '/structures?filter=references.doi HAS ONLY "10/123"' - error_detail = ( - 'Cannot filter relationships by field "doi", only "id" is supported.' - ) - self._check_error_response( - request, - expected_status=501, - expected_title="NotImplementedError", - expected_detail=error_detail, - ) - - def _check_response( - self, request: str, expected_ids: Union[List, Set], expected_return: int - ): - try: - response = self.client.get(request) - self.assertEqual( - response.status_code, 200, msg=f"Request failed: {response.json()}" - ) - response = response.json() - response_ids = [struct["id"] for struct in response["data"]] - self.assertEqual(sorted(expected_ids), sorted(response_ids)) - self.assertEqual(response["meta"]["data_returned"], expected_return) - except Exception as exc: - print("Request attempted:") - print(f"{self.client.base_url}{request}") - raise exc - - def _check_error_response( - self, - request: str, - expected_status: int = None, - expected_title: str = None, - expected_detail: str = None, - ): - expected_status = 500 if expected_status is None else expected_status - super()._check_error_response( - request, expected_status, expected_title, expected_detail - ) diff --git a/tests/server/test_server_validation.py b/tests/server/test_server_validation.py index d4beb35e..8bf1e9eb 100644 --- a/tests/server/test_server_validation.py +++ b/tests/server/test_server_validation.py @@ -1,7 +1,4 @@ -# pylint: disable=relative-beyond-top-level,import-outside-toplevel -import os import unittest -from traceback import print_exc from optimade.validator import ImplementationValidator @@ -9,45 +6,13 @@ class ServerTestWithValidator(SetClient, unittest.TestCase): - - server = "regular" + """Use OPTIMADE Validator on server""" def test_with_validator(self): + """Validate server""" validator = ImplementationValidator(client=self.client) try: validator.main() - except Exception: - print_exc() + except Exception as exc: # pylint: disable=broad-except + print(repr(exc)) self.assertTrue(validator.valid) - - -class IndexServerTestWithValidator(SetClient, unittest.TestCase): - - server = "index" - - def test_with_validator(self): - validator = ImplementationValidator(client=self.client, index=True) - try: - validator.main() - except Exception: - print_exc() - self.assertTrue(validator.valid) - - -def test_mongo_backend_package_used(): - import pymongo - import mongomock - from optimade.server.entry_collections import client - - force_mongo_env_var = os.environ.get("OPTIMADE_CI_FORCE_MONGO", None) - if force_mongo_env_var is None: - return - - if int(force_mongo_env_var) == 1: - assert issubclass(client.__class__, pymongo.MongoClient) - elif int(force_mongo_env_var) == 0: - assert issubclass(client.__class__, mongomock.MongoClient) - else: - raise Exception( - f"The environment variable OPTIMADE_CI_FORCE_MONGO cannot be parsed as an int." - ) diff --git a/tests/server/utils.py b/tests/server/utils.py index 5cd6c6ad..3eb49aba 100644 --- a/tests/server/utils.py +++ b/tests/server/utils.py @@ -1,73 +1,55 @@ # pylint: disable=import-outside-toplevel,no-name-in-module import abc -from typing import Dict from pydantic import BaseModel from fastapi.testclient import TestClient -def get_regular_client() -> TestClient: - """Return TestClient for regular OPTIMADE server""" - from optimade.server.main import app - from optimade.server.routers import info, links, references, structures +def get_client() -> TestClient: + """Return TestClient for OPTIMADE server""" + from aiida_optimade.main import APP + from aiida_optimade.routers import info, structures # We need to remove the version prefixes in order to have the tests run correctly. - app.include_router(info.router) - app.include_router(links.router) - app.include_router(references.router) - app.include_router(structures.router) + APP.include_router(info.ROUTER) + APP.include_router(structures.ROUTER) # need to explicitly set base_url, as the default "http://testserver" # does not validate as pydantic AnyUrl model - return TestClient(app, base_url="http://example.org/v0") - - -def get_index_client() -> TestClient: - """Return TestClient for index meta-database OPTIMADE server""" - from optimade.server.main_index import app - from optimade.server.routers import index_info, links - - # # We need to remove the version prefixes in order to have the tests run correctly. - app.include_router(index_info.router) - app.include_router(links.router) - # need to explicitly set base_url, as the default "http://testserver" - # does not validate as pydantic UrlStr model - return TestClient(app, base_url="http://example.org/v0") + return TestClient(APP, base_url="http://example.org/v0") class SetClient(abc.ABC): """Metaclass to instantiate the TestClients once""" - server: str = None - _client: Dict[str, TestClient] = { - "index": get_index_client(), - "regular": get_regular_client(), - } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._client = None @property def client(self) -> TestClient: - exception_message = "Test classes using EndpointTestsMixin MUST specify a `server` attribute with a value that is either 'regular' or 'index'" - if not hasattr(self, "server"): - raise AttributeError(exception_message) - if self.server in self._client: - return self._client[self.server] - raise ValueError(exception_message) + """Return test client for OPTIMADE server""" + if self._client is None: + self._client = get_client() + return self._client # pylint: disable=no-member - def _check_error_response( + def check_error_response( self, request: str, expected_status: int = None, expected_title: str = None, expected_detail: str = None, ): + """General method for testing expected errornous response""" try: response = self.client.get(request) self.assertEqual( response.status_code, expected_status, - msg=f"Request should have been an error with status code {expected_status}, " - f"but instead {response.status_code} was received.\nResponse:\n{response.json()}", + msg="Request should have been an error with status code " + f"{expected_status}, but instead {response.status_code} was received." + f"\nResponse:\n{response.json()}", ) response = response.json() self.assertEqual(len(response["errors"]), 1) @@ -90,9 +72,8 @@ def _check_error_response( class EndpointTestsMixin(SetClient): - """ Mixin "base" class for common tests between endpoints. """ + """Mixin "base" class for common tests between endpoints""" - server: str = "regular" request_str: str = None response_cls: BaseModel = None @@ -108,6 +89,7 @@ def __init__(self, *args, **kwargs): ) def test_meta_response(self): + """General test for `meta` property in response""" self.assertTrue("meta" in self.json_response) meta_required_keys = [ "query", @@ -123,12 +105,14 @@ def test_meta_response(self): self.check_keys(meta_optional_keys, self.json_response["meta"]) def test_serialize_response(self): + """General test for response JSON and pydantic model serializability""" self.assertTrue( self.response_cls is not None, msg="Response class unset for this endpoint" ) self.response_cls(**self.json_response) # pylint: disable=not-callable def check_keys(self, keys: list, response_subset: dict): + """Utility function to help validate dict keys""" for key in keys: self.assertTrue( key in response_subset, diff --git a/tests/test_config.json b/tests/test_config.json index f0dac398..577ee320 100644 --- a/tests/test_config.json +++ b/tests/test_config.json @@ -11,7 +11,7 @@ "maintainer": {"email": "casper.andersen@epfl.ch"} }, "provider": { - "prefix": "_aiida_", + "prefix": "aiida", "name": "AiiDA", "description": "AiiDA: Automated Interactive Infrastructure and Database for Computational Science (http://www.aiida.net)", "homepage": "http://www.aiida.net",