Repository: CS-SI/eodag
Pull request #417 (2022-03-01 14:08:35)

STAC server use mapping for criterias from 'stac_provider.yml'
Closes  #395

github-actions[bot]: ## Unit Test Results
    2 files  ±0      2 suites  ±0   9m 9s [:stopwatch:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "duration of all tests") -9s
206 tests +1  204 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests") +1  2 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "skipped / disabled tests") ±0  0 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "failed tests") ±0 
412 runs  +2  408 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests") +2  4 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "skipped / disabled tests") ±0  0 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "failed tests") ±0 

Results for commit ebd0fcde. ± Comparison against base commit 059bc078.

github-actions[bot]: <strong> </strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `76%` | :white_check_mark: |
| eodag/rest/utils.py | `61%` | :x: |
| eodag/utils/\_\_init\_\_.py | `87%` | :white_check_mark: |

<strong> </strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `76%` | :white_check_mark: |
| eodag/rest/utils.py | `61%` | :x: |
| eodag/utils/\_\_init\_\_.py | `87%` | :white_check_mark: |
| eodag/rest/utils.py | `61%` | :x: |
| eodag/utils/\_\_init\_\_.py | `87%` | :white_check_mark: |

_Minimum allowed coverage is `70%`_

<p align="right">Generated by :monkey: cobertura-action against ebd0fcded7ce25e06a1654bd83c3c045ade95f71 </p>

Patch:
diff --git a/eodag/rest/utils.py b/eodag/rest/utils.py
index ebe5850c..94e6eec1 100644
--- a/eodag/rest/utils.py
+++ b/eodag/rest/utils.py
@@ -2,6 +2,7 @@
 # Copyright 2017-2018 CS GROUP - France (CS SI)
 # All rights reserved
 
+import ast
 import datetime
 import os
 import re
@@ -10,13 +11,14 @@ from collections import namedtuple
 import dateutil.parser
 import markdown
 from dateutil import tz
+from jsonpath_ng.ext import parse
 from shapely.geometry import Polygon, shape
 
 import eodag
 from eodag.api.core import DEFAULT_ITEMS_PER_PAGE, DEFAULT_PAGE
 from eodag.api.product.metadata_mapping import OSEO_METADATA_MAPPING
 from eodag.api.search_result import SearchResult
-from eodag.config import load_stac_config
+from eodag.config import load_stac_config, load_stac_provider_config
 from eodag.plugins.crunch.filter_latest_intersect import FilterLatestIntersect
 from eodag.plugins.crunch.filter_latest_tpl_name import FilterLatestByName
 from eodag.plugins.crunch.filter_overlap import FilterOverlap
@@ -37,6 +39,9 @@ crunchers = {
     "overlap": Cruncher(FilterOverlap, ["minimum_overlap"]),
 }
 stac_config = load_stac_config()
+stac_provider_config = load_stac_provider_config()
+
+STAC_QUERY_PATTERN = "query.*.*"
 
 
 def format_product_types(product_types):
@@ -263,46 +268,154 @@ def get_geometry(arguments):
     return geom
 
 
+def get_datetime(arguments):
+    """Get the datetime criterias from the search arguments
+
+    :param arguments: Request args
+    :type arguments: dict
+    :returns: Start date and end date from datetime string.
+    :rtype: Tuple[Optional[str], Optional[str]]
+    """
+    datetime_str = arguments.pop("datetime", None)
+    if datetime_str:
+        datetime_split = datetime_str.split("/")
+        if len(datetime_split) == 1:
+            return get_date(datetime_split[0]), None
+        elif len(datetime_split) == 2:
+            return get_date(datetime_split[0]), get_date(datetime_split[1])
+    dtstart = get_date(arguments.pop("dtstart", None))
+    dtend = get_date(arguments.pop("dtend", None))
+    return dtstart, dtend
+
+
+def get_metadata_query_paths(metadata_mapping):
+    """Get dict of query paths and their names from metadata_mapping
+
+    >>> metadata_mapping = {
+    ...     'cloudCover': [
+    ...         '{{"query":{{"eo:cloud_cover":{{"lte":"{cloudCover}"}}}}}}',
+    ...         '$.properties."eo:cloud_cover"'
+    ...     ]
+    ... }
+    >>> get_metadata_query_paths(metadata_mapping)
+    {'query.eo:cloud_cover.lte': 'cloudCover'}
+
+    :param metadata_mapping: STAC metadata mapping (see 'resources/stac_provider.yml')
+    :type metadata_mapping: dict
+    :returns: Mapping of query paths with their corresponding names
+    :rtype: dict
+    """
+    metadata_query_paths = {}
+    for metadata_name, metadata_spec in metadata_mapping.items():
+        # When metadata_spec have a length of 1 the query path is not specified
+        if len(metadata_spec) == 2:
+            metadata_query_template = metadata_spec[0]
+            try:
+                # We create the dict corresponding to the metadata query of the metadata
+                metadata_query_dict = ast.literal_eval(
+                    metadata_query_template.format(**{metadata_name: None})
+                )
+                # We check if our query path pattern matches one or more of the dict path
+                matches = [
+                    (str(match.full_path))
+                    for match in parse(STAC_QUERY_PATTERN).find(metadata_query_dict)
+                ]
+                if matches:
+                    metadata_query_path = matches[0]
+                    metadata_query_paths[metadata_query_path] = metadata_name
+            except KeyError:
+                pass
+    return metadata_query_paths
+
+
+def get_arguments_query_paths(arguments):
+    """Get dict of query paths and their values from arguments
+
+    Build a mapping of the query paths present in the arguments
+    with their values. All matching paths of our STAC_QUERY_PATTERN
+    ('query.*.*') are used.
+
+    >>> arguments = {'another': 'example', 'query': {'eo:cloud_cover': {'lte': '10'}, 'foo': {'eq': 'bar'}}}
+    >>> get_arguments_query_paths(arguments)
+    {'query.eo:cloud_cover.lte': '10', 'query.foo.eq': 'bar'}
+
+    :param arguments: Request args
+    :type arguments: dict
+    :returns: Mapping of query paths with their corresponding values
+    :rtype: dict
+    """
+    return dict(
+        (str(match.full_path), match.value)
+        for match in parse(STAC_QUERY_PATTERN).find(arguments)
+    )
+
+
+def get_criterias_from_metadata_mapping(metadata_mapping, arguments):
+    """Get criterias from the search arguments with the metadata mapping config
+
+    :param metadata_mapping: STAC metadata mapping (see 'resources/stac_provider.yml')
+    :type metadata_mapping: dict
+    :param arguments: Request args
+    :type arguments: dict
+    :returns: Mapping of criterias with their corresponding values
+    :rtype: dict
+    """
+    criterias = {}
+    metadata_query_paths = get_metadata_query_paths(metadata_mapping)
+    arguments_query_paths = get_arguments_query_paths(arguments)
+    for query_path in arguments_query_paths:
+        if query_path in metadata_query_paths:
+            criteria_name = metadata_query_paths[query_path]
+        else:
+            # The criteria is custom and we must read
+            # its name from the query path
+            criteria_name = query_path.split(".")[1]
+        criteria_value = arguments_query_paths[query_path]
+        criterias[criteria_name] = criteria_value
+    return criterias
+
+
 def search_products(product_type, arguments):
     """Returns product search results
 
     :param product_type: The product type criteria
     :type product_type: str
-    :param arguments: Filter criteria
+    :param arguments: Request args
     :type arguments: dict
     :returns: A search result
     :rtype serialized GeoJSON response"""
 
     try:
+        arg_product_type = arguments.pop("product_type", None)
+        unserialized = arguments.pop("unserialized", None)
+
         page, items_per_page = get_pagination_info(arguments)
+        dtstart, dtend = get_datetime(arguments)
         geom = get_geometry(arguments)
 
-        criteria = {
+        criterias = {
+            "productType": product_type if product_type else arg_product_type,
+            "page": page,
+            "items_per_page": items_per_page,
+            "raise_errors": True,
+            "start": dtstart,
+            "end": dtend,
             "geom": geom,
-            "startTimeFromAscendingNode": get_date(arguments.pop("dtstart", None)),
-            "completionTimeFromAscendingNode": get_date(arguments.pop("dtend", None)),
-            "cloudCover": get_int(arguments.pop("cloudCover", None)),
-            "productType": product_type,
         }
-        if "id" in arguments.keys():
-            criteria["id"] = arguments["id"]
 
-        if items_per_page is None:
-            items_per_page = DEFAULT_ITEMS_PER_PAGE
-        if page is None:
-            page = DEFAULT_PAGE
-
-        unserialized = arguments.pop("unserialized", None)
-        arguments.pop("product_type", None)
-
-        products, total = eodag_api.search(
-            page=page,
-            items_per_page=items_per_page,
-            raise_errors=True,
-            **dict(criteria, **arguments),
+        stac_provider_metadata_mapping = stac_provider_config.get("search", {}).get(
+            "metadata_mapping", {}
+        )
+        extra_criterias = get_criterias_from_metadata_mapping(
+            stac_provider_metadata_mapping, arguments
         )
+        criterias.update(extra_criterias)
 
-        products = filter_products(products, arguments, **criteria)
+        # We remove potential None values to use the default values of the search method
+        criterias = dict((k, v) for k, v in criterias.items() if v is not None)
+
+        products, total = eodag_api.search(**criterias)
+        products = filter_products(products, arguments, **criterias)
 
         if not unserialized:
             response = SearchResult(products).as_geojson_object()
@@ -413,17 +526,17 @@ def get_stac_collection_by_id(url, root, collection_id, provider=None):
     ).get_collection_by_id(collection_id)
 
 
-def get_stac_item_by_id(url, item_id, root="/", catalogs=[], provider=None):
+def get_stac_item_by_id(url, item_id, catalogs, root="/", provider=None):
     """Build STAC item by id
 
     :param url: Requested URL
     :type url: str
     :param item_id: Product ID
     :type item_id: str
+    :param catalogs: Catalogs list (only first is used as product_type)
+    :type catalogs: list
     :param root: (optional) API root
     :type root: str
-    :param catalogs: (optional) Catalogs list (only first is used as product_type)
-    :type catalogs: list
     :param provider: (optional) Chosen provider
     :type provider: str
     :returns: Collection dictionnary


Test patch:
diff --git a/tests/context.py b/tests/context.py
index bfe7e850..03cdb71b 100644
--- a/tests/context.py
+++ b/tests/context.py
@@ -44,7 +44,34 @@ from eodag.plugins.download.http import HTTPDownload
 from eodag.plugins.manager import PluginManager
 from eodag.plugins.search.base import Search
 from eodag.rest import server as eodag_http_server
-from eodag.rest.utils import eodag_api, get_date
+from eodag.rest.utils import (
+    download_stac_item_by_id,
+    eodag_api,
+    filter_products,
+    format_product_types,
+    get_arguments_query_paths,
+    get_criterias_from_metadata_mapping,
+    get_date,
+    get_datetime,
+    get_detailled_collections_list,
+    get_geometry,
+    get_home_page_content,
+    get_int,
+    get_metadata_query_paths,
+    get_pagination_info,
+    get_product_types,
+    get_stac_catalogs,
+    get_stac_collection_by_id,
+    get_stac_collections,
+    get_stac_conformance,
+    get_stac_extension_oseo,
+    get_stac_item_by_id,
+    get_templates_path,
+    search_bbox,
+    search_product_by_id,
+    search_products,
+    search_stac_items,
+)
 from eodag.utils import (
     get_geometry_from_various,
     get_timestamp,
diff --git a/tests/units/test_http_server.py b/tests/units/test_http_server.py
index 7d436f6f..ac2aa9c7 100644
--- a/tests/units/test_http_server.py
+++ b/tests/units/test_http_server.py
@@ -22,13 +22,7 @@ import unittest
 import geojson
 
 from tests import mock
-from tests.context import (
-    DEFAULT_ITEMS_PER_PAGE,
-    SearchResult,
-    ValidationError,
-    eodag_http_server,
-    get_date,
-)
+from tests.context import DEFAULT_ITEMS_PER_PAGE, SearchResult, eodag_http_server
 
 
 class RequestTestCase(unittest.TestCase):
@@ -176,20 +170,6 @@ class RequestTestCase(unittest.TestCase):
         )
         self.assertGreaterEqual(len(result1.features), len(result2.features))
 
-    def test_get_date(self):
-        """Date validation function must correctly validate dates"""
-        get_date("2018-01-01")
-        get_date("2018-01-01T")
-        get_date("2018-01-01T00:00")
-        get_date("2018-01-01T00:00:00")
-        get_date("2018-01-01T00:00:00Z")
-        get_date("20180101")
-
-        self.assertRaises(ValidationError, get_date, "foo")
-        self.assertRaises(ValidationError, get_date, "foo2018-01-01")
-
-        self.assertIsNone(get_date(None))
-
     def test_date_search(self):
         result1 = self._request_valid(
             "search?collections={}&bbox=0,43,1,44".format(self.tested_product_type)
diff --git a/tests/units/test_stac_utils.py b/tests/units/test_stac_utils.py
new file mode 100644
index 00000000..3ee70333
--- /dev/null
+++ b/tests/units/test_stac_utils.py
@@ -0,0 +1,289 @@
+# -*- coding: utf-8 -*-
+# Copyright 2021, CS GROUP - France, https://www.csgroup.eu/
+#
+# This file is part of EODAG project
+#     https://www.github.com/CS-SI/EODAG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import unittest
+
+from eodag.utils.exceptions import ValidationError
+from tests import TEST_RESOURCES_PATH, mock
+from tests.context import (
+    SearchResult,
+    eodag_api,
+    filter_products,
+    format_product_types,
+    get_arguments_query_paths,
+    get_criterias_from_metadata_mapping,
+    get_date,
+    get_datetime,
+    get_detailled_collections_list,
+    get_home_page_content,
+    get_int,
+    get_metadata_query_paths,
+    get_pagination_info,
+    get_product_types,
+    get_stac_catalogs,
+    get_stac_collection_by_id,
+    get_stac_collections,
+    get_stac_conformance,
+    get_stac_extension_oseo,
+    get_stac_item_by_id,
+    get_templates_path,
+    search_products,
+)
+
+
+class TestStacUtils(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        search_results_file = os.path.join(
+            TEST_RESOURCES_PATH, "eodag_search_result_peps.geojson"
+        )
+        with open(search_results_file, encoding="utf-8") as f:
+            search_results_geojson = json.load(f)
+        cls.products = SearchResult.from_geojson(search_results_geojson)
+        cls.arguments = {
+            "query": {"eo:cloud_cover": {"lte": "10"}},
+            "filter": "latestIntersect",
+        }
+        cls.criterias = {
+            "productType": "S2_MSI_L1C",
+            "page": 1,
+            "items_per_page": 1,
+            "raise_errors": True,
+            "cloudCover": "10",
+        }
+        cls.empty_products = SearchResult([])
+        cls.empty_arguments = {}
+        cls.empty_criterias = {}
+
+    def test_download_stac_item_by_id(self):
+        pass  # TODO
+
+    def test_filter_products_unknown_cruncher_raise_error(self):
+        """filter_products must raise a ValidationError if an unknown cruncher is given"""
+        with self.assertRaises(ValidationError) as context:
+            filter_products(self.products, {"filter": "unknown_cruncher"})
+        self.assertTrue("unknown filter name" in str(context.exception))
+
+    def test_filter_products_missing_additional_parameters_raise_error(self):
+        """filter_products must raise a ValidationError if additional parameters are required by the cruncher"""
+        with self.assertRaises(ValidationError) as context:
+            filter_products(self.products, {"filter": "latestByName"})
+        self.assertTrue("additional parameters required" in str(context.exception))
+
+    def test_filter_products_filter_misuse_raise_error(self):
+        """filter_products must raise a ValidationError if the cruncher is not used correctly"""
+        with self.assertRaises(ValidationError):
+            filter_products(
+                self.products,
+                {"filter": "latestByName", "name_pattern": "MisconfiguredError"},
+            )
+
+    def test_filter_products(self):
+        """filter_products returns a SearchResult corresponding to the filter"""
+        products_empty_filter = filter_products(self.products, {})
+        products_filtered = filter_products(
+            self.products,
+            {
+                "filter": "latestByName",
+                "name_pattern": r"S2[AB]_MSIL1C_20(?P<tileid>\d{6}).*T21NY.*",
+            },
+        )
+        self.assertEqual(self.products, products_empty_filter)
+        self.assertNotEqual(self.products, products_filtered)
+
+    @mock.patch(
+        "eodag.rest.utils.eodag_api.list_product_types",
+        autospec=True,
+        return_value=[{"ID": "S2_MSI_L1C", "abstract": "test"}],
+    )
+    def test_format_product_types(self, list_pt):
+        """format_product_types must return a string representation of the product types"""
+        product_types = eodag_api.list_product_types()
+        self.assertEqual(
+            format_product_types(product_types), "* *__S2_MSI_L1C__*: test"
+        )
+
+    def test_get_arguments_query_paths(self):
+        """get_arguments_query_paths must extract the query paths and their values from a request arguments"""
+        arguments = {
+            "another": "example",
+            "query": {"eo:cloud_cover": {"lte": "10"}, "foo": {"eq": "bar"}},
+        }
+        arguments_query_path = get_arguments_query_paths(arguments)
+        self.assertEqual(
+            arguments_query_path,
+            {"query.eo:cloud_cover.lte": "10", "query.foo.eq": "bar"},
+        )
+
+    def test_get_criterias_from_metadata_mapping(self):
+        """get_criterias_from_metadata_mapping must extract search criterias
+        from request arguments with metadata_mapping config"""
+        metadata_mapping = {
+            "doi": [
+                '{{"query":{{"sci:doi":{{"eq":"{doi}"}}}}}}',
+                '$.properties."sci:doi"',
+            ],
+            "platform": [
+                '{{"query":{{"constellation":{{"eq":"{platform}"}}}}}}',
+                "$.properties.constellation",
+            ],
+            "cloudCover": [
+                '{{"query":{{"eo:cloud_cover":{{"lte":"{cloudCover}"}}}}}}',
+                '$.properties."eo:cloud_cover"',
+            ],
+            "productVersion": [
+                '{{"query":{{"version":{{"eq":"{productVersion}"}}}}}}',
+                "$.properties.version",
+            ],
+            "id": ['{{"ids":["{id}"]}}', "$.id"],
+            "downloadLink": "%(base_uri)s/collections/{productType}/items/{id}",
+        }
+        arguments = {
+            "query": {"eo:cloud_cover": {"lte": "10"}, "foo": {"eq": "bar"}},
+        }
+        criterias = get_criterias_from_metadata_mapping(metadata_mapping, arguments)
+        self.assertEqual(criterias, {"cloudCover": "10", "foo": "bar"})
+
+    def test_get_date(self):
+        """Date validation function must correctly validate dates"""
+        get_date("2018-01-01")
+        get_date("2018-01-01T")
+        get_date("2018-01-01T00:00")
+        get_date("2018-01-01T00:00:00")
+        get_date("2018-01-01T00:00:00Z")
+        get_date("20180101")
+
+        self.assertRaises(ValidationError, get_date, "foo")
+        self.assertRaises(ValidationError, get_date, "foo2018-01-01")
+
+        self.assertIsNone(get_date(None))
+
+    def test_get_datetime(self):
+        """get_datetime must extract start and end datetime from datetime request args"""
+        start = "2021-01-01T00:00:00"
+        end = "2021-01-28T00:00:00"
+
+        dtstart, dtend = get_datetime({"datetime": f"{start}/{end}"})
+        self.assertEqual(dtstart, start)
+        self.assertEqual(dtend, end)
+
+        dtstart, dtend = get_datetime({"datetime": start})
+        self.assertEqual(dtstart, start)
+        self.assertIsNone(dtend)
+
+        dtstart, dtend = get_datetime({"dtstart": start, "dtend": end})
+        self.assertEqual(dtstart, start)
+        self.assertEqual(dtend, end)
+
+    @mock.patch(
+        "eodag.rest.utils.eodag_api.list_product_types",
+        autospec=True,
+        return_value=[{"ID": "S2_MSI_L1C"}],
+    )
+    def test_detailled_collections_list(self, list_pt):
+        """get_detailled_collections_list returned list is non-empty"""
+        self.assertTrue(get_detailled_collections_list())
+        self.assertTrue(list_pt.called)
+
+    def test_get_geometry(self):
+        pass  # TODO
+
+    def test_home_page_content(self):
+        """get_home_page_content runs without any error"""
+        get_home_page_content("http://127.0.0.1/")
+
+    def test_get_int(self):
+        """get_int must raise a ValidationError for strings that cannot be interpreted as integers"""
+        get_int("1")
+        with self.assertRaises(ValidationError):
+            get_int("a")
+
+    def test_get_metadata_query_paths(self):
+        """get_metadata_query_paths returns query paths from metadata_mapping and their corresponding names"""
+        metadata_mapping = {
+            "cloudCover": [
+                '{{"query":{{"eo:cloud_cover":{{"lte":"{cloudCover}"}}}}}}',
+                '$.properties."eo:cloud_cover"',
+            ]
+        }
+        metadata_query_paths = get_metadata_query_paths(metadata_mapping)
+        self.assertEqual(
+            metadata_query_paths, {"query.eo:cloud_cover.lte": "cloudCover"}
+        )
+
+    def test_get_pagination_info(self):
+        """get_pagination_info must raise a ValidationError if wrong values are given"""
+        get_pagination_info({})
+        with self.assertRaises(ValidationError):
+            get_pagination_info({"page": "-1"})
+        with self.assertRaises(ValidationError):
+            get_pagination_info({"limit": "-1"})
+
+    def test_get_product_types(self):
+        """get_product_types use"""
+        self.assertTrue(get_product_types())
+        self.assertTrue(get_product_types(filters={"sensorType": "OPTICAL"}))
+
+    def test_get_stac_catalogs(self):
+        """get_stac_catalogs runs without any error"""
+        get_stac_catalogs(url="")
+
+    def test_get_stac_collection_by_id(self):
+        """get_stac_collection_by_id runs without any error"""
+        get_stac_collection_by_id(url="", root="", collection_id="S2_MSI_L1C")
+
+    def test_get_stac_collections(self):
+        """get_stac_collections runs without any error"""
+        get_stac_collections(url="", root="", arguments={})
+
+    def test_get_stac_conformance(self):
+        """get_stac_conformance runs without any error"""
+        get_stac_conformance()
+
+    def test_get_stac_extension_oseo(self):
+        """get_stac_extension_oseo runs without any error"""
+        get_stac_extension_oseo(url="")
+
+    def test_get_stac_item_by_id(self):
+        """get_stac_item_by_id returns None if no StacItem was found"""
+        self.assertIsNotNone(
+            get_stac_item_by_id(url="", item_id="", catalogs=["S2_MSI_L1C"])
+        )
+        self.assertIsNone(
+            get_stac_item_by_id(url="", item_id="", catalogs=["S3_MSI_L1C"])
+        )
+
+    def test_get_templates_path(self):
+        """get_templates_path returns an existing dir path"""
+        self.assertTrue(os.path.isdir(get_templates_path()))
+
+    def test_search_bbox(self):
+        pass  # TODO
+
+    def test_search_product_by_id(self):
+        pass  # TODO
+
+    def test_search_products(self):
+        """search_products runs without any error"""
+        search_products("S2_MSI_L1C", {})
+        search_products("S2_MSI_L1C", {"unserialized": "true"})
+
+    def test_search_stac_items(self):
+        pass  # TODO