Skip to content
Open
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
35 changes: 17 additions & 18 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
connexion[swagger-ui] >= 2.6.0, <3
swagger-ui-bundle >= 0.0.2
python_dateutil >= 2.6.0
setuptools >= 21.0.0
Flask == 2.1.1
httpx == 0.27.2
APScheduler >= 3.10.0
aenum == 3.1.15
SQLAlchemy >= 2.0.23
psycopg >= 3.1.0
psycopg-binary
Flask_Testing==0.8.1
python-keycloak == 4.7.0
pyjwt == 2.3.0
cryptography == 3.4.8
coverage>=7.3.2
pydantic >= 2.5.3
tenacity == 9.1.2
connexion[swagger-ui] == 2.15.1
swagger-ui-bundle == 1.1.0
python_dateutil == 2.9.0
setuptools == 80.10.2
Flask == 2.3.3
httpx == 0.28.1
APScheduler == 3.11.2
aenum == 3.1.16
SQLAlchemy == 2.0.46
psycopg == 3.3.2
Flask_Testing == 0.8.1
python-keycloak == 7.0.3
pyjwt == 2.11.0
cryptography == 46.0.4
coverage == 7.13.3
pydantic == 2.12.5
tenacity == 9.1.2
1 change: 1 addition & 0 deletions server/.openapi-generator-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

.openapi-generator-ignore
fleetv2_http_api/controllers/security_controller.py
fleetv2_http_api/encoder.py
requirements.txt
setup.py
README.md
Expand Down
5 changes: 3 additions & 2 deletions server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
set_status_wait_timeout_s,
)

from server.fleetv2_http_api.encoder import JSONEncoder # type: ignore
from server.fleetv2_http_api.encoder import CustomJSONProvider # type: ignore
from server.database.security import _AdminBase as _AdminBase # type: ignore

# Keep the following import to make all the tables be created by the get_test_app function
Expand All @@ -26,8 +26,9 @@

def get_app() -> _connexion.FlaskApp:
app = _connexion.App(__name__, specification_dir="fleetv2_http_api/openapi/")
app.app.json_encoder = JSONEncoder
app.add_api("openapi.yaml")
# Flask 2.3+ uses json provider instead of json_encoder
app.app.json = CustomJSONProvider(app.app)
return app


Expand Down
43 changes: 29 additions & 14 deletions server/fleetv2_http_api/encoder.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
from connexion.apps.flask_app import FlaskJSONEncoder
from enum import Enum
from typing import Any

from flask.json.provider import DefaultJSONProvider

from server.fleetv2_http_api.models.base_model import Model


class JSONEncoder(FlaskJSONEncoder):
include_nulls = False
def _serialize(obj: Any, include_nulls: bool = False) -> Any:
"""Recursively serialize Model objects and Enums to JSON-compatible types."""
if isinstance(obj, Model):
result = {}
for attr in obj.openapi_types:
value = getattr(obj, attr)
if value is None and not include_nulls:
continue
key = obj.attribute_map[attr]
result[key] = _serialize(value, include_nulls)
return result
elif isinstance(obj, Enum):
return obj.value
elif isinstance(obj, list):
return [_serialize(item, include_nulls) for item in obj]
elif isinstance(obj, dict):
return {k: _serialize(v, include_nulls) for k, v in obj.items()}
return obj


class CustomJSONProvider(DefaultJSONProvider):
"""Custom JSON provider for Flask 2.3+ that handles OpenAPI models."""

def default(self, o):
if isinstance(o, Model):
dikt = {}
for attr in o.openapi_types:
value = getattr(o, attr)
if value is None and not self.include_nulls:
continue
attr = o.attribute_map[attr]
dikt[attr] = value
return dikt
return FlaskJSONEncoder.default(self, o)
def dumps(self, obj: Any, **kwargs: Any) -> str:
obj = _serialize(obj)
return super().dumps(obj, **kwargs)
64 changes: 52 additions & 12 deletions tests_integration/messages/test_since_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,21 @@ def setUp(self, mock_timestamp: Mock) -> None:
def test_thread_waits_for_status_with_newer_timestamp_than_since_parameter(
self, mock_timestamp: Mock
):
with _Executor(max_workers=2) as executor, self.app.app.test_client() as c:
future = executor.submit(c.get, "/status/test_company/test_car?since=50&wait=True")
status_5 = self.status_5

def get_status():
with self.app.app.test_client() as c:
return c.get("/status/test_company/test_car?since=50&wait=True")

def post_status():
with self.app.app.test_client() as c:
return c.post("/status/test_company/test_car", json=[status_5])

with _Executor(max_workers=2) as executor:
future = executor.submit(get_status)
mock_timestamp.return_value = 60
time.sleep(0.01)
executor.submit(c.post, "/status/test_company/test_car", json=[self.status_5])
executor.submit(post_status)
response = future.result()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json[0]["timestamp"], 60) # type: ignore
Expand All @@ -170,11 +180,21 @@ def test_thread_waits_for_status_with_newer_timestamp_than_since_parameter(
def test_sending_status_with_timestamp_older_than_since_parameter_will_not_resume_waiting_thread(
self, mock_timestamp: Mock
):
with _Executor(max_workers=2) as executor, self.app.app.test_client() as c:
future = executor.submit(c.get, "/status/test_company/test_car?since=100&wait=True")
status_5 = self.status_5

def get_status():
with self.app.app.test_client() as c:
return c.get("/status/test_company/test_car?since=100&wait=True")

def post_status():
with self.app.app.test_client() as c:
return c.post("/status/test_company/test_car", json=[status_5])

with _Executor(max_workers=2) as executor:
future = executor.submit(get_status)
mock_timestamp.return_value = 80
time.sleep(0.01)
executor.submit(c.post, "/status/test_company/test_car", json=[self.status_5])
executor.submit(post_status)
response = future.result()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, [])
Expand Down Expand Up @@ -220,11 +240,21 @@ def setUp(self, mock_timestamp: Mock) -> None:
def test_thread_waits_for_command_with_newer_timestamp_than_since_parameter(
self, mock_timestamp: Mock
):
with _Executor(max_workers=2) as executor, self.app.app.test_client() as c:
future = executor.submit(c.get, "/command/test_company/test_car?since=50&wait=True")
command_4 = self.command_4

def get_command():
with self.app.app.test_client() as c:
return c.get("/command/test_company/test_car?since=50&wait=True")

def post_command():
with self.app.app.test_client() as c:
return c.post("/command/test_company/test_car", json=[command_4])

with _Executor(max_workers=2) as executor:
future = executor.submit(get_command)
mock_timestamp.return_value = 55
time.sleep(0.01)
executor.submit(c.post, "/command/test_company/test_car", json=[self.command_4])
executor.submit(post_command)
response = future.result()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json[0]["timestamp"], 55) # type: ignore
Expand All @@ -233,11 +263,21 @@ def test_thread_waits_for_command_with_newer_timestamp_than_since_parameter(
def test_sending_command_with_timestamp_older_than_since_parameter_will_not_resume_waiting_thread(
self, mock_timestamp: Mock
):
with _Executor(max_workers=2) as executor, self.app.app.test_client() as c:
future = executor.submit(c.get, "/command/test_company/test_car?since=100&wait=True")
command_4 = self.command_4

def get_command():
with self.app.app.test_client() as c:
return c.get("/command/test_company/test_car?since=100&wait=True")

def post_command():
with self.app.app.test_client() as c:
return c.post("/command/test_company/test_car", json=[command_4])

with _Executor(max_workers=2) as executor:
future = executor.submit(get_command)
mock_timestamp.return_value = 80
time.sleep(0.01)
executor.submit(c.post, "/command/test_company/test_car", json=[self.command_4])
executor.submit(post_command)
response = future.result()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, [])
Expand Down
64 changes: 50 additions & 14 deletions tests_integration/wait_mechanism/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,45 @@ def setUp(self) -> None:
def test_if_status_and_relevant_command_is_sent_before_timeout_code_200_and_empty_list_are_returned(
self,
):
with _Executor(max_workers=2) as executor, self.app.app.test_client() as c:
def get_command():
with self.app.app.test_client() as c:
return c.get("/command/test_company/test_car?wait=True")

def post_command():
with self.app.app.test_client() as c:
return c.post("/command/test_company/test_car", json=[self.command])

with _Executor(max_workers=2) as executor:
time.sleep(0.05)
future = executor.submit(c.get, "/command/test_company/test_car?wait=True")
future = executor.submit(get_command)
time.sleep(0.05)
executor.submit(c.post, "/command/test_company/test_car", json=[self.command])
executor.submit(post_command)
response = future.result()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json[0]["payload"]["data"]["instruction"], "start")

def test_if_status_but_no_command_is_sent_before_timeout_code_200_and_empty_list_are_returned(
self,
):
with _Executor(max_workers=2) as executor, self.app.app.test_client() as c:
future = executor.submit(c.get, "/command/test_company/test_car?wait=True")
def get_command():
with self.app.app.test_client() as c:
return c.get("/command/test_company/test_car?wait=True")

with _Executor(max_workers=2) as executor:
future = executor.submit(get_command)
response = future.result()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, [])

def test_if_no_relevant_status_is_sent_before_timeout_code_404_and_empty_list_are_returned(
self,
):
with _Executor(max_workers=2) as executor, self.app.app.test_client() as c:
future = executor.submit(c.get, "/command/other_company/other_car?wait=True")
def get_command():
with self.app.app.test_client() as c:
return c.get("/command/other_company/other_car?wait=True")

with _Executor(max_workers=2) as executor:
future = executor.submit(get_command)
response = future.result()
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json, [])
Expand All @@ -76,24 +92,44 @@ def setUp(self) -> None:
self.status = Message(device_id=self.device_id, payload=self.status_payload)

def test_no_status_being_sent_before_timeout_yields_code_404_and_empty_list_after_timeout_is_exceeded(self):
with _Executor() as executor, self.app.app.test_client() as c:
def get_command():
with self.app.app.test_client() as c:
return c.get("/command/test_company/test_car?wait=True")

with _Executor() as executor:
start_time = time.time()
future = executor.submit(c.get, "/command/test_company/test_car?wait=True")
future = executor.submit(get_command)
response = future.result()
response_time = time.time() - start_time
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json, [])
self.assertGreaterEqual(response_time, self._timeout)

def test_commands_sent_before_car_becomes_available_are_not_sent_to_waiting_thread(self):
with _Executor() as executor, self.app.app.test_client() as c:
future = executor.submit(c.get, "/command/test_company/test_car?wait=True")
def get_command():
with self.app.app.test_client() as c:
return c.get("/command/test_company/test_car?wait=True")

def post_command_1():
with self.app.app.test_client() as c:
return c.post("/command/test_company/test_car", json=[self.command_1])

def post_status():
with self.app.app.test_client() as c:
return c.post("/status/test_company/test_car", json=[self.status])

def post_command_2():
with self.app.app.test_client() as c:
return c.post("/command/test_company/test_car", json=[self.command_2])

with _Executor() as executor:
future = executor.submit(get_command)
time.sleep(0.05)
# this command will be ignored by the waiting thread
executor.submit(c.post, "/command/test_company/test_car", json=[self.command_1])
executor.submit(c.post, "/status/test_company/test_car", json=[self.status])
executor.submit(post_command_1)
executor.submit(post_status)
time.sleep(0.05)
executor.submit(c.post, "/command/test_company/test_car", json=[self.command_2])
executor.submit(post_command_2)
response = future.result()
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json), 1)
Expand Down
Loading