diff --git a/docs/samples/config/mswms/mswms_settings.py.sample b/docs/samples/config/mswms/mswms_settings.py.sample index 94a5eec6e..f87a0f00d 100644 --- a/docs/samples/config/mswms/mswms_settings.py.sample +++ b/docs/samples/config/mswms/mswms_settings.py.sample @@ -108,6 +108,23 @@ basemap_use_cache = False basemap_request_size = 200 basemap_cache_size = 20 +# +# Request limits ### +# + +# A GetMap request beyond these limits gets a ServiceException instead of a plot, +# so that a single request cannot use up the memory of the server. +# 'max_image_width' and 'max_image_height' limit the image size in pixels (msui +# requests the size of the plot on screen), 'max_layers' the number of LAYERS in +# one request, each layer can be requested once per style. 'max_section_points' limits the number of interpolation points and +# labels of vertical and linear sections (msui option 'num_interpolation_points'), +# 'max_path_waypoints' the number of waypoints in PATH. +max_image_width = 4096 +max_image_height = 4096 +max_layers = 10 +max_section_points = 2000 +max_path_waypoints = 1000 + # # Registration of horizontal layers. ### # diff --git a/mslib/mswms/app/__init__.py b/mslib/mswms/app/__init__.py index f1f056b08..dfa97ed53 100644 --- a/mslib/mswms/app/__init__.py +++ b/mslib/mswms/app/__init__.py @@ -74,6 +74,12 @@ class default_mswms_settings: GDPR = "" data = {} ENABLE_BASIC_HTTP_AUTHENTICATION = False + # Limits of a GetMap request, see the sample mswms_settings.py + max_image_width = 4096 + max_image_height = 4096 + max_layers = 10 + max_section_points = 2000 + max_path_waypoints = 1000 __file__ = None diff --git a/mslib/mswms/mpl_hsec.py b/mslib/mswms/mpl_hsec.py index 0b0a5eb50..b8ef3be23 100644 --- a/mslib/mswms/mpl_hsec.py +++ b/mslib/mswms/mpl_hsec.py @@ -200,8 +200,8 @@ def _draw_auto_graticule(self, bm): spacingValues = [0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 20, 30, 40] deltaLon = mapLonStop - mapLonStart deltaLat = mapLatStop - mapLatStart - spacingLon = [i for i in spacingValues if i > (deltaLon / 10.)][0] - spacingLat = [i for i in spacingValues if i > (deltaLat / 10.)][0] + spacingLon = next((i for i in spacingValues if i > (deltaLon / 10.)), spacingValues[-1]) + spacingLat = next((i for i in spacingValues if i > (deltaLat / 10.)), spacingValues[-1]) # c) parallels and meridians start at the first value in the # spacingLon/Lat grid that's smaller than the lon/lat of the @@ -302,7 +302,7 @@ def plot_hsection(self, data, lats, lons, bbox=(-180, -90, 180, 90), # access. key = repr((proj_params, bbox, bbox_units)) basemap_use_cache = getattr(mswms_settings, "basemap_use_cache", False) - basemap_request_size = getattr(mswms_settings, "basemap_request_size ", 200) + basemap_request_size = getattr(mswms_settings, "basemap_request_size", 200) basemap_cache_size = getattr(mswms_settings, "basemap_cache_size", 20) bm_params = {"area_thresh": 1000., "ax": ax, "fix_aspect": (not noframe)} bm_params.update(proj_params) diff --git a/mslib/mswms/wms.py b/mslib/mswms/wms.py index c6cea1f8e..15c48fd36 100644 --- a/mslib/mswms/wms.py +++ b/mslib/mswms/wms.py @@ -46,6 +46,7 @@ import io import inspect import logging +import math import shutil import tempfile import traceback @@ -143,6 +144,78 @@ def squash_multiple_xml(xml_strings): return ElementTree.tostring(base) +class InvalidRequestValue(ValueError): + """ + A GetMap parameter is invalid, e.g. not a finite number or outside the limits set in mswms_settings + + produce_plot answers it with a ServiceException. code is the WMS exception code, None for none. + """ + def __init__(self, text, code=None): + super().__init__(text) + self.code = code + + +def check_range(number, name, lowest=-math.inf, highest=math.inf, integer=False): + """ + Returns number, an int if integer is True, if it is in the range lowest..highest. + Raises InvalidRequestValue otherwise. + """ + if integer and not float(number).is_integer(): + raise InvalidRequestValue(f"{name} must be an integer, got '{number}'") + if not lowest <= number <= highest: + raise InvalidRequestValue(f"{name} must be between {lowest} and {highest}, got '{number}'") + return int(number) if integer else number + + +def parse_number(value, name, lowest=-math.inf, highest=math.inf, integer=False): + """ + Converts a query value to a finite float (to an int if integer is True) + in the range lowest..highest. Raises InvalidRequestValue otherwise. + """ + try: + number = float(value) + except (TypeError, ValueError): + raise InvalidRequestValue(f"Invalid {name}: '{value}'") + if not math.isfinite(number): + raise InvalidRequestValue(f"{name} must be a finite number, got '{value}'") + return check_range(number, name, lowest, highest, integer) + + +def parse_numbers(values, name, count=None, min_count=None, max_count=None, multiple_of=1): + """ + Converts a comma separated query value to a list of finite floats. count + is the required number of values, min_count and max_count the minimum and + maximum number, multiple_of what the number has to be a multiple of, e.g. + 2 for lat/lon pairs. The number is checked before the string is split. + """ + number = values.count(",") + 1 + if count is not None and number != count: + raise InvalidRequestValue(f"{name} needs {count} values, got {number}") + if max_count is not None and number > max_count: + raise InvalidRequestValue(f"{name} has more than {max_count} values") + if min_count is not None and number < min_count: + raise InvalidRequestValue(f"{name} needs at least {min_count} values, got {number}") + if number % multiple_of != 0: + raise InvalidRequestValue(f"{name} needs a multiple of {multiple_of} values, got {number}") + return [parse_number(value, name) for value in values.split(",")] + + +def check_bbox(bbox, bbox_units): + """ + Checks the BBOX (x_min, y_min, x_max, y_max) of a horizontal section, already in x/y order + + The box must not be empty. In degrees the latitudes are in -90..90 and the box spans at most 360 degrees of + longitude, beyond these the map and its grid lines can't be drawn. + """ + if not (bbox[0] < bbox[2] and bbox[1] < bbox[3]): + raise InvalidRequestValue(f"BBOX must have minimum values smaller than its maximum values, got {bbox}") + if bbox_units == "degree": + if not (-90 <= bbox[1] and bbox[3] <= 90): + raise InvalidRequestValue(f"BBOX latitudes must be between -90 and 90, got {bbox}") + if bbox[2] - bbox[0] > 360: + raise InvalidRequestValue(f"BBOX must span at most 360 degrees of longitude, got {bbox}") + + class WMSServer: def __init__(self): @@ -618,7 +691,10 @@ def get_capabilities(self, query, server_url=None): service_post_code=mswms_settings.service_post_code, service_country=mswms_settings.service_country, service_fees=mswms_settings.service_fees, - service_access_constraints=mswms_settings.service_access_constraints) + service_access_constraints=mswms_settings.service_access_constraints, + max_layers=mswms_settings.max_layers, + max_image_width=mswms_settings.max_image_width, + max_image_height=mswms_settings.max_image_height) return return_data.encode("utf-8"), "text/xml" def produce_plot(self, query, mode): @@ -627,6 +703,12 @@ def produce_plot(self, query, mode): the parameters specified in the URL. """ + try: + return self._produce_plot(query, mode) + except InvalidRequestValue as ex: + return self.create_service_exception(code=ex.code, text=str(ex), version=query.get("VERSION", "1.1.1")) + + def _produce_plot(self, query, mode): logging.debug("GetMap/GetVSec request. Interpreting parameters..") # Evaluate query parameters: @@ -634,14 +716,28 @@ def produce_plot(self, query, mode): version = query.get("VERSION", "1.1.1") - # Image size. - width = query.get('WIDTH', 900) - height = query.get('HEIGHT', 600) - figsize = float(width if width != "" else 900), float(height if height != "" else 600) - logging.debug(" requested image size = %sx%s", figsize[0], figsize[1]) + def requested_figsize(): + # Image size, only of the modes that draw an image + width = query.get('WIDTH') or 900 + height = query.get('HEIGHT') or 600 + figsize = (parse_number(width, "WIDTH", 1, mswms_settings.max_image_width), + parse_number(height, "HEIGHT", 1, mswms_settings.max_image_height)) + logging.debug(" requested image size = %sx%s", figsize[0], figsize[1]) + return figsize # Requested layers. layers = [layer for layer in query.get('LAYERS', '').strip().split(',') if layer] + if not layers: + return self.create_service_exception(code="LayerNotDefined", text="LAYERS not specified", + version=version) + if len(layers) > mswms_settings.max_layers: + raise InvalidRequestValue(f"At most {mswms_settings.max_layers} LAYERS can be requested at once") + # Requested style(s). + styles = [style for style in query.get('STYLES', 'default').strip().split(',') if style] + # each layer is drawn once per style, repeating it would multiply the cost of the request + layer_styles = [(layer, styles[index] if len(styles) > index else None) for index, layer in enumerate(layers)] + if len(set(layer_styles)) != len(layer_styles): + raise InvalidRequestValue("LAYERS must not contain the same layer with the same style more than once") images = [] for index, layer in enumerate(layers): if layer.find(".") > 0: @@ -650,9 +746,7 @@ def produce_plot(self, query, mode): dataset = None logging.debug(" requested dataset = '%s', layer = '%s'", dataset, layer) - # Requested style(s). - styles = [style for style in query.get('STYLES', 'default').strip().split(',') if style] - style = styles[index] if len(styles) > index else None + style = layer_styles[index][1] logging.debug(" requested style = '%s'", style) # Forecast initialisation time. @@ -691,7 +785,7 @@ def produce_plot(self, query, mode): mode = "getlsec" else: try: - get_projection_params(crs) + bbox_units = get_projection_params(crs)["bbox"] except ValueError: return self.create_service_exception( code="InvalidSRS", text=f"The requested CRS '{crs}' is not supported.", version=version) @@ -748,19 +842,20 @@ def produce_plot(self, query, mode): version=version) # Bounding box. - try: - if is_yx: - bbox = [float(v) for v in query.get('BBOX', '-90,-180,90,180').split(',')] - bbox = (bbox[1], bbox[0], bbox[3], bbox[2]) - else: - bbox = [float(v) for v in query.get('BBOX', '-180,-90,180,90').split(',')] - - except ValueError: - return self.create_service_exception(text=f"Invalid BBOX: {query.get('BBOX')}", version=version) + if is_yx: + bbox = parse_numbers(query.get('BBOX', '-90,-180,90,180'), "BBOX", count=4) + bbox = (bbox[1], bbox[0], bbox[3], bbox[2]) + else: + bbox = parse_numbers(query.get('BBOX', '-180,-90,180,90'), "BBOX", count=4) + check_bbox(bbox, bbox_units) + figsize = requested_figsize() # Vertical level, if applicable. level = query.get('ELEVATION') - level = float(level) if level is not None else None + try: + level = parse_number(level, "ELEVATION") if level is not None else None + except InvalidRequestValue as ex: + raise InvalidRequestValue(str(ex), code="InvalidDimensionValue") layer_datatypes = self.hsec_layer_registry[dataset][layer].required_datatypes() from mslib.utils.netCDF4tools import VERTICAL_AXIS if level is None and any(_x in layer_datatypes for _x in VERTICAL_AXIS): @@ -793,11 +888,10 @@ def produce_plot(self, query, mode): path = query.get("PATH") if path is None: return self.create_service_exception(text="PATH not specified", version=version) - try: - path = [float(v) for v in path.split(',')] - path = [[lat, lon] for lat, lon in zip(path[0::2], path[1::2])] - except ValueError: - return self.create_service_exception(text=f"Invalid PATH: {path}", version=version) + # at least two waypoints of lat, lon + path = parse_numbers(path, "PATH", min_count=4, max_count=2 * mswms_settings.max_path_waypoints, + multiple_of=2) + path = [[lat, lon] for lat, lon in zip(path[0::2], path[1::2])] logging.debug("VSEC PATH: %s", path) # Check requested layers. @@ -820,10 +914,12 @@ def produce_plot(self, query, mode): version=version) # Bounding box (num interp. points, p_bot, num labels, p_top). - try: - bbox = [float(v) for v in query.get("BBOX", "101,1050,10,180").split(",")] - except ValueError: - return self.create_service_exception(text=f"Invalid BBOX: {query.get('BBOX')}", version=version) + bbox = parse_numbers(query.get("BBOX", "101,1050,10,180"), "BBOX", count=4) + numpoints = check_range(bbox[0], "BBOX number of points", + 2, mswms_settings.max_section_points, integer=True) + numlabels = check_range(bbox[2], "BBOX number of labels", + 1, mswms_settings.max_section_points, integer=True) + figsize = requested_figsize() draw_verticals = query.get("DRAWVERTICALS", "false").lower() == "true" @@ -831,9 +927,9 @@ def produce_plot(self, query, mode): try: plot_driver.set_plot_parameters(plot_object=self.vsec_layer_registry[dataset][layer], vsec_path=path, - vsec_numpoints=bbox[0], + vsec_numpoints=numpoints, vsec_path_connection="greatcircle", - vsec_numlabels=bbox[2], + vsec_numlabels=numlabels, init_time=init_time, valid_time=valid_time, style=style, @@ -863,11 +959,10 @@ def produce_plot(self, query, mode): path = query.get("PATH") if path is None: return self.create_service_exception(text="PATH not specified", version=version) - try: - path = [float(v) for v in path.split(',')] - path = [[lat, lon, alt] for lat, lon, alt in zip(path[0::3], path[1::3], path[2::3])] - except ValueError: - return self.create_service_exception(text=f"Invalid PATH: {path}", version=version) + # at least two waypoints of lat, lon, alt + path = parse_numbers(path, "PATH", min_count=6, max_count=3 * mswms_settings.max_path_waypoints, + multiple_of=3) + path = [[lat, lon, alt] for lat, lon, alt in zip(path[0::3], path[1::3], path[2::3])] logging.debug("LSEC PATH: %s", path) # Check requested layers. @@ -890,10 +985,8 @@ def produce_plot(self, query, mode): version=version) # Bounding box (num interp. points). - try: - bbox = float(query.get("BBOX", "101")) - except ValueError: - return self.create_service_exception(text=f"Invalid BBOX: {query.get('BBOX')}", version=version) + bbox = parse_number(query.get("BBOX", "101"), "BBOX number of points", + 2, mswms_settings.max_section_points, integer=True) plot_driver = self.lsec_drivers[dataset] try: diff --git a/mslib/mswms/xml_templates/get_capabilities130.pt b/mslib/mswms/xml_templates/get_capabilities130.pt index 2e5f36ede..23170b063 100644 --- a/mslib/mswms/xml_templates/get_capabilities130.pt +++ b/mslib/mswms/xml_templates/get_capabilities130.pt @@ -28,6 +28,9 @@ ${ service_fees } ${ service_access_constraints } + ${ max_layers } + ${ max_image_width } + ${ max_image_height } diff --git a/tests/_test_mswms/test_wms.py b/tests/_test_mswms/test_wms.py index db5e7af62..71af6ba08 100644 --- a/tests/_test_mswms/test_wms.py +++ b/tests/_test_mswms/test_wms.py @@ -39,6 +39,22 @@ from tests.constants import MSWMS_DATA_DIR +HSEC_QUERY = ( + 'layers=ecmwf_EUR_LL015.PLDiv01&styles=&elevation=200&srs=EPSG%3A4326&format=image%2Fpng&' + 'request=GetMap&height=376&dim_init_time=2012-10-17T12%3A00%3A00Z&width=479&' + 'version=1.1.1&bbox=-50.0%2C20.0%2C20.0%2C75.0&time=2012-10-17T12%3A00%3A00Z&transparent=FALSE') +VSEC_QUERY = ( + 'layers=ecmwf_EUR_LL015.VS_HV01&styles=&srs=VERT%3ALOGP&format=image%2Fpng&' + 'request=GetMap&height=245&dim_init_time=2012-10-17T12%3A00%3A00Z&width=842&' + 'version=1.1.1&bbox=201%2C500.0%2C10%2C100.0&time=2012-10-17T12%3A00%3A00Z&' + 'path=52.78%2C-8.93%2C48.08%2C11.28&transparent=FALSE') +LSEC_QUERY = ( + 'layers=ecmwf_EUR_LL015.LS_HV01&styles=&srs=LINE%3A1&format=text%2Fxml&' + 'request=GetMap&dim_init_time=2012-10-17T12%3A00%3A00Z&' + 'version=1.1.1&bbox=201&time=2012-10-17T12%3A00%3A00Z&' + 'path=52.78%2C-8.93%2C25000%2C48.08%2C11.28%2C25000') + + class Test_WMS: @pytest.fixture(autouse=True) def setup(self, mswms_app): @@ -377,6 +393,111 @@ def test_multiple_xml(self): result = self.client.get('/?{}'.format(environ["QUERY_STRING"])) callback_ok_xml(result.status, result.headers) + @pytest.mark.parametrize("query, orig, fake, message", [ + (HSEC_QUERY, "width=479", "width=65000", b"WIDTH must be between 1 and 4096"), + (HSEC_QUERY, "height=376", "height=65000", b"HEIGHT must be between 1 and 4096"), + (VSEC_QUERY, "width=842", "width=4097", b"WIDTH must be between 1 and 4096"), + (HSEC_QUERY, "width=479", "width=0", b"WIDTH must be between"), + (HSEC_QUERY, "width=479", "width=-479", b"WIDTH must be between"), + (HSEC_QUERY, "width=479", "width=nan", b"WIDTH must be a finite number"), + (HSEC_QUERY, "height=376", "height=inf", b"HEIGHT must be a finite number"), + (HSEC_QUERY, "width=479", "width=abc", b"Invalid WIDTH"), + (HSEC_QUERY, "layers=ecmwf_EUR_LL015.PLDiv01", "layers=" + ",".join(["ecmwf_EUR_LL015.PLDiv01"] * 11), + b"At most 10 LAYERS"), + (HSEC_QUERY, "layers=ecmwf_EUR_LL015.PLDiv01", "layers=ecmwf_EUR_LL015.PLDiv01,ecmwf_EUR_LL015.PLDiv01", + b"LAYERS must not contain the same layer with the same style more than once"), + (HSEC_QUERY, "bbox=-50.0%2C20.0%2C20.0%2C75.0", "bbox=-1000%2C-90%2C1000%2C90", + b"BBOX must span at most 360 degrees of longitude"), + (HSEC_QUERY, "bbox=-50.0%2C20.0%2C20.0%2C75.0", "bbox=0%2C0%2C0%2C0", + b"BBOX must have minimum values smaller than its maximum values"), + (HSEC_QUERY, "bbox=-50.0%2C20.0%2C20.0%2C75.0", "bbox=20.0%2C20.0%2C-50.0%2C75.0", + b"BBOX must have minimum values smaller than its maximum values"), + (HSEC_QUERY, "bbox=-50.0%2C20.0%2C20.0%2C75.0", "bbox=-50.0%2C20.0%2C20.0%2C95.0", + b"BBOX latitudes must be between -90 and 90"), + (HSEC_QUERY, "layers=ecmwf_EUR_LL015.PLDiv01", "layers=", b"LAYERS not specified"), + (HSEC_QUERY, "bbox=-50.0", "bbox=nan", b"BBOX must be a finite number"), + (HSEC_QUERY, "bbox=-50.0%2C", "bbox=", b"BBOX needs 4 values, got 3"), + (HSEC_QUERY, "elevation=200", "elevation=abc", b"Invalid ELEVATION"), + (HSEC_QUERY, "elevation=200", "elevation=-inf", b"ELEVATION must be a finite number"), + (VSEC_QUERY, "bbox=201", "bbox=1e8", b"BBOX number of points must be between 2 and 2000"), + (VSEC_QUERY, "bbox=201", "bbox=1", b"BBOX number of points must be between 2 and 2000"), + (VSEC_QUERY, "bbox=201", "bbox=201.5", b"BBOX number of points must be an integer"), + (VSEC_QUERY, "bbox=201", "bbox=nan", b"BBOX must be a finite number"), + (VSEC_QUERY, "%2C10%2C", "%2C0%2C", b"BBOX number of labels must be between 1 and 2000"), + (VSEC_QUERY, "%2C10%2C", "%2C1e9%2C", b"BBOX number of labels must be between 1 and 2000"), + (VSEC_QUERY, "500.0", "inf", b"BBOX must be a finite number"), + (VSEC_QUERY, "%2C100.0", "", b"BBOX needs 4 values, got 3"), + (VSEC_QUERY, "path=52.78%2C-8.93%2C48.08%2C11.28", "path=" + "%2C".join(["52.78%2C-8.93"] * 1001), + b"PATH has more than 2000 values"), + (VSEC_QUERY, "path=52.78", "path=inf", b"PATH must be a finite number"), + (VSEC_QUERY, "path=52.78%2C-8.93%2C48.08%2C11.28", "path=5", b"PATH needs at least 4 values, got 1"), + (VSEC_QUERY, "path=52.78%2C-8.93%2C48.08%2C11.28", "path=52.78%2C-8.93%2C48.08", + b"PATH needs at least 4 values, got 3"), + (VSEC_QUERY, "path=52.78%2C-8.93%2C48.08%2C11.28", "path=52.78%2C-8.93%2C48.08%2C11.28%2C50", + b"PATH needs a multiple of 2 values, got 5"), + (LSEC_QUERY, "bbox=201", "bbox=5e6", b"BBOX number of points must be between 2 and 2000"), + (LSEC_QUERY, "bbox=201", "bbox=nan", b"BBOX number of points must be a finite number"), + (LSEC_QUERY, "path=52.78%2C-8.93%2C25000%2C48.08%2C11.28%2C25000", + "path=" + "%2C".join(["52.78%2C-8.93%2C25000"] * 1001), b"PATH has more than 3000 values"), + (LSEC_QUERY, "path=52.78", "path=nan", b"PATH must be a finite number"), + (LSEC_QUERY, "path=52.78%2C-8.93%2C25000%2C48.08%2C11.28%2C25000", "path=52.78%2C-8.93%2C25000", + b"PATH needs at least 6 values, got 3"), + (LSEC_QUERY, "path=52.78%2C-8.93%2C25000%2C48.08%2C11.28%2C25000", + "path=52.78%2C-8.93%2C25000%2C48.08%2C11.28%2C25000%2C1", b"PATH needs a multiple of 3 values, got 7"), + ]) + def test_produce_plot_rejects_values_beyond_limits(self, query, orig, fake, message): + assert orig in query + result = self.app.test_client().get('/?{}'.format(query.replace(orig, fake, 1))) + callback_ok_xml(result.status, result.headers) + assert b"ServiceExceptionReport" in result.data, result.data + assert message in result.data, result.data + # InvalidParameterValue is no exception code of WMS 1.1.1 or 1.3.0 + assert b"InvalidParameterValue" not in result.data, result.data + + @pytest.mark.parametrize("query, setting, value", [ + (HSEC_QUERY, "max_image_width", 479), + (HSEC_QUERY, "max_image_height", 376), + (HSEC_QUERY, "max_layers", 1), + (VSEC_QUERY, "max_section_points", 201), + (VSEC_QUERY, "max_path_waypoints", 2), + (LSEC_QUERY, "max_section_points", 201), + (LSEC_QUERY, "max_path_waypoints", 2), + ]) + def test_produce_plot_limits_are_configurable(self, query, setting, value): + client = self.app.test_client() + # the query uses exactly the limit + with mock.patch.object(mslib.mswms.wms.mswms_settings, setting, value): + result = client.get('/?{}'.format(query)) + assert b"ServiceExceptionReport" not in result.data, result.data + with mock.patch.object(mslib.mswms.wms.mswms_settings, setting, value - 1): + result = client.get('/?{}'.format(query)) + callback_ok_xml(result.status, result.headers) + assert b"ServiceExceptionReport" in result.data, result.data + + def test_produce_plot_accepts_limits(self): + # msui sends integers, but other clients may send WIDTH/HEIGHT as float + result = self.app.test_client().get('/?{}'.format( + HSEC_QUERY.replace("width=479", "width=479.0").replace("height=376", "height="))) + callback_ok_image(result.status, result.headers) + result = self.app.test_client().get('/?{}'.format(VSEC_QUERY.replace("bbox=201", "bbox=2"))) + callback_ok_image(result.status, result.headers) + result = self.app.test_client().get('/?{}'.format( + HSEC_QUERY.replace("width=479", "width=4096").replace("height=376", "height=4096"))) + callback_ok_image(result.status, result.headers) + + def test_produce_plot_linear_section_ignores_image_size(self): + # a linear section returns XML, it draws no image + result = self.app.test_client().get('/?{}&width=65000&height=nan'.format(LSEC_QUERY)) + callback_ok_xml(result.status, result.headers) + assert b"ServiceExceptionReport" not in result.data, result.data + + def test_get_capabilities_advertises_limits(self): + result = self.app.test_client().get('/?request=GetCapabilities&service=WMS&version=1.3.0') + callback_ok_xml(result.status, result.headers) + assert b"10" in result.data + assert b"4096" in result.data + assert b"4096" in result.data + @pytest.mark.skip(reason="disabled because of reload") def test_import_error(self): with mock.patch.dict("sys.modules", {"mswms_settings": None, "mswms_auth": None}):