Skip to content

Better error management in REST API draw endpoint #121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ and this project adheres to
- Update drawing parameters reference doc.
- Add some data in example database.
- cli: Adopt colored logger from `RFL.log` library (#103).
- web: Update description of HTTP/400 error on `draw` REST API endpoint in
OpenAPI description to mention all new cases.
- pkg: Add _tests_ extra packages with all dependencies required to run unit
tests.

Expand All @@ -52,8 +54,11 @@ and this project adheres to
- String representation of computed property.
- Add current schema object in set of loaded classes to fix resolution of
references in some corner cases.
- draw: Detect when unable to find racks to draw in infrastructure and raise
specific exception instead of crashing with `ZeroDivisionError` (#97→#100).
- draw:
- Detect when unable to find racks to draw in infrastructure and raise
specific exception instead of crashing with `ZeroDivisionError` (#97→#100).
- Raise `RacksDBDrawingError` when drawer is called with an unsupported
output image format.
- docs:
- Wrong APT sources file extension in quickstart guide.
- Path of system packages examples directory in quickstart guide.
Expand All @@ -65,6 +70,9 @@ and this project adheres to
option of `racksdb-web`.
- Log critical error instead of crashing when `racksdb-web` is unable to load
schema or database (#110).
- Return HTTP/400 error when draw endpoint is called with unsupported image
format.
- Return HTTP/400 error when draw endpoint is called with unsupported entity.
- ui:
- Limit specs links size in equipment type modal (#76).
- Update bundled dependencies to fix security issues CVE-2024-39338 (axios),
Expand Down
5 changes: 3 additions & 2 deletions docs/modules/usage/attachments/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1113,8 +1113,9 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
description: Unable to load drawing parameters, unsupported coordinates
formator unable to load requested entity in database.
description: Unsupported entity, unable to load drawing parameters, unsupported
image format, unsupported coordinates formator unable to load requested
entity in database.
'415':
content:
application/json:
Expand Down
3 changes: 3 additions & 0 deletions racksdb/drawers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import gi

from .coordinates import CoordinateDumperFactory
from ..errors import RacksDBDrawingError

gi.require_version("Pango", "1.0")
gi.require_version("PangoCairo", "1.0")
Expand Down Expand Up @@ -63,6 +64,8 @@ def init_ctx(self, width, height):
self.surface = cairo.SVGSurface(self.file, width, height)
elif self.output_format == "pdf":
self.surface = cairo.PDFSurface(self.file, width, height)
else:
raise RacksDBDrawingError(f"Unsupported image format {self.output_format}")
self.ctx = cairo.Context(self.surface)

def write(self):
Expand Down
284 changes: 284 additions & 0 deletions racksdb/tests/web/test_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

import unittest
import tempfile
import json

import werkzeug
import yaml
import flask
from requests_toolbelt import MultipartDecoder

from racksdb.web.app import RacksDBWebBlueprint
from racksdb import RacksDB
Expand Down Expand Up @@ -515,6 +517,288 @@ def test_racks_fold(self):
# FIXME: nb_nodes_unfolded should be greater than nb_nodes_folded
self.assertEqual(nb_racks_unfolded, nb_racks_folded)

#
# draw
#
def test_draw_room_png(self):
response = self.client.post(f"/v{get_version()}/draw/room/noisy.png")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "image/png")

def test_draw_room_svg(self):
response = self.client.post(f"/v{get_version()}/draw/room/noisy.svg")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "image/svg+xml")

def test_draw_room_pdf(self):
response = self.client.post(f"/v{get_version()}/draw/room/noisy.pdf")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "application/pdf")

def test_draw_room_invalid(self):
response = self.client.post(f"/v{get_version()}/draw/room/fail.png")
self.assertEqual(response.status_code, 400) # FIXME: should be HTTP/404
self.assertEqual(response.mimetype, "application/json")
self.assertEqual(
response.json,
{
"code": 400,
"description": "Unable to find room fail in database",
"name": "Bad Request",
},
)

def test_draw_room_coordinates(self):
response = self.client.post(
f"/v{get_version()}/draw/room/noisy.png?coordinates"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "multipart/form-data")
decoder = MultipartDecoder(response.get_data(), response.content_type)
(image_part, coordinates_part) = decoder.parts
self.assertEqual(image_part.headers.get(b"Content-Type").decode(), "image/png")
self.assertEqual(
coordinates_part.headers.get(b"Content-Type").decode(), "application/json"
)
coordinates = json.loads(coordinates_part.text)
self.assertEqual(coordinates, {}) # FIXME: room coordinates are empty

def test_draw_room_coordinates_yaml(self):
response = self.client.post(
f"/v{get_version()}/draw/room/noisy.png?coordinates&coordinates_format=yaml"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "multipart/form-data")
decoder = MultipartDecoder(response.get_data(), response.content_type)
(image_part, coordinates_part) = decoder.parts
self.assertEqual(image_part.headers.get(b"Content-Type").decode(), "image/png")
self.assertEqual(
coordinates_part.headers.get(b"Content-Type").decode(), "application/x-yaml"
)
coordinates = yaml.safe_load(coordinates_part.text)
self.assertEqual(coordinates, {}) # FIXME: room coordinates are empty

def test_draw_room_parameters(self):
drawing_parameters = {"margin": {"left": 10, "top": 10}}
response = self.client.post(
f"/v{get_version()}/draw/room/noisy.png",
data=json.dumps(drawing_parameters),
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "image/png")

def test_draw_room_parameters_yaml(self):
drawing_parameters = {"margin": {"left": 10, "top": 10}}
response = self.client.post(
f"/v{get_version()}/draw/room/noisy.png",
data=yaml.dump(drawing_parameters),
content_type="application/x-yaml",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "image/png")

def test_draw_room_invalid_parameters(self):
drawing_parameters = {"fail": True}
response = self.client.post(
f"/v{get_version()}/draw/room/noisy.png",
data=json.dumps(drawing_parameters),
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.mimetype, "application/json")
self.assertEqual(
response.json,
{
"code": 400,
"description": (
"Unable to load drawing parameters: Property fail is not defined "
"in schema for object Schema_content"
),
"name": "Bad Request",
},
)

def test_draw_room_parameters_invalid_format(self):
response = self.client.post(
f"/v{get_version()}/draw/room/noisy.png",
data="fail",
content_type="text/html",
)
self.assertEqual(response.status_code, 415)
self.assertEqual(response.mimetype, "application/json")
self.assertEqual(
response.json,
{
"code": 415,
"description": "Unsupported request body format",
"name": "Unsupported Media Type",
},
)

def test_draw_infrastructure_png(self):
response = self.client.post(
f"/v{get_version()}/draw/infrastructure/mercury.png"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "image/png")

def test_draw_infrastructure_svg(self):
response = self.client.post(
f"/v{get_version()}/draw/infrastructure/mercury.svg"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "image/svg+xml")

def test_draw_infrastructure_pdf(self):
response = self.client.post(
f"/v{get_version()}/draw/infrastructure/mercury.pdf"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "application/pdf")

def test_draw_infrastructure_invalid(self):
response = self.client.post(f"/v{get_version()}/draw/infrastructure/fail.png")
self.assertEqual(response.status_code, 400) # FIXME: should be HTTP/404
self.assertEqual(response.mimetype, "application/json")
self.assertEqual(
response.json,
{
"code": 400,
"description": "Unable to find infrastructure fail in database",
"name": "Bad Request",
},
)

def test_draw_infrastructure_coordinates(self):
response = self.client.post(
f"/v{get_version()}/draw/infrastructure/mercury.png?coordinates"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "multipart/form-data")
decoder = MultipartDecoder(response.get_data(), response.content_type)
(image_part, coordinates_part) = decoder.parts
self.assertEqual(image_part.headers.get(b"Content-Type").decode(), "image/png")
self.assertEqual(
coordinates_part.headers.get(b"Content-Type").decode(), "application/json"
)
coordinates = json.loads(coordinates_part.text)
self.assertIn("mecn0001", coordinates)

def test_draw_infrastructure_coordinates_yaml(self):
response = self.client.post(
f"/v{get_version()}/draw/infrastructure/mercury.png?coordinates&"
"coordinates_format=yaml"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "multipart/form-data")
decoder = MultipartDecoder(response.get_data(), response.content_type)
(image_part, coordinates_part) = decoder.parts
self.assertEqual(image_part.headers.get(b"Content-Type").decode(), "image/png")
self.assertEqual(
coordinates_part.headers.get(b"Content-Type").decode(), "application/x-yaml"
)
coordinates = yaml.safe_load(coordinates_part.text)
self.assertIn("mecn0001", coordinates)

def test_draw_infrastructure_parameters(self):
drawing_parameters = {"margin": {"left": 10, "top": 10}}
response = self.client.post(
f"/v{get_version()}/draw/infrastructure/mercury.png",
data=json.dumps(drawing_parameters),
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "image/png")

def test_draw_infrastructure_parameters_yaml(self):
drawing_parameters = {"margin": {"left": 10, "top": 10}}
response = self.client.post(
f"/v{get_version()}/draw/infrastructure/mercury.png",
data=yaml.dump(drawing_parameters),
content_type="application/x-yaml",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "image/png")

def test_draw_infrastructure_invalid_parameters(self):
drawing_parameters = {"fail": True}
response = self.client.post(
f"/v{get_version()}/draw/infrastructure/mercury.png",
data=json.dumps(drawing_parameters),
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.mimetype, "application/json")
self.assertEqual(
response.json,
{
"code": 400,
"description": (
"Unable to load drawing parameters: Property fail is not defined "
"in schema for object Schema_content"
),
"name": "Bad Request",
},
)

def test_draw_infrastructure_parameters_invalid_format(self):
response = self.client.post(
f"/v{get_version()}/draw/infrastructure/mercury.png",
data="fail",
content_type="text/html",
)
self.assertEqual(response.status_code, 415)
self.assertEqual(response.mimetype, "application/json")
self.assertEqual(
response.json,
{
"code": 415,
"description": "Unsupported request body format",
"name": "Unsupported Media Type",
},
)

def test_draw_invalid_entity(self):
response = self.client.post(f"/v{get_version()}/draw/fail/noisy.png")
self.assertEqual(response.status_code, 400)
self.assertEqual(response.mimetype, "application/json")
self.assertEqual(
response.json,
{
"code": 400,
"description": "Unable to draw entity fail",
"name": "Bad Request",
},
)

def test_draw_invalid_format(self):
response = self.client.post(f"/v{get_version()}/draw/room/noisy.fail")
self.assertEqual(response.status_code, 400)
self.assertEqual(response.mimetype, "application/json")
self.assertEqual(
response.json,
{
"code": 400,
"description": "Unsupported image format fail",
"name": "Bad Request",
},
)

def test_draw_coordinates_invalid_format(self):
response = self.client.post(
f"/v{get_version()}/draw/room/noisy.png?coordinates&coordinates_format=fail"
)
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.json,
{
"code": 400,
"description": "Unsupported coordinates format",
"name": "Bad Request",
},
)

#
# tags
#
Expand Down
3 changes: 2 additions & 1 deletion racksdb/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ class RacksDBViews(DBViewSet):
errors=[
DBActionError(
400,
"Unable to load drawing parameters, unsupported coordinates format"
"Unsupported entity, unable to load drawing parameters, "
"unsupported image format, unsupported coordinates format"
"or unable to load requested entity in database.",
),
DBActionError(415, "Unsupported drawing parameters format."),
Expand Down
4 changes: 3 additions & 1 deletion racksdb/web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,11 @@ def _draw(self, entity, name, format):
coordinates_fh,
coordinates_format,
)
else:
abort(400, f"Unable to draw entity {entity}")
drawer.draw()
except RacksDBError as err:
abort(400, str(err))
drawer.draw()
file.seek(0)

if with_coordinates:
Expand Down
Loading