Repository: bringauto/fleet-protocol-http-api
Pull request #24: Baf 1051/login endpoint fix
Author: login: jiristrouhal
Labels: name: bug
description: Something isn't working
Created: 2025-03-18T14:07:21Z | Merged: 2025-03-19T13:57:16Z

There has been an error related to global variable _security in the impl.controllers.py module. 

Thanks to the contents of __main__.py and  all the imports inside the custom modules of the http api, what happened was, that the __main__.py first called init_security, which set the **_security** to some instance of SecurityObjImpl. Then, due to some import of the ...impl.controllers module elsewhere, the **_security** has been reset to empty_security_obj. 

The _security is now set to empty only on demand or when first accessed.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit


- **Style**
  - Improved configuration file formatting for enhanced readability.
- **Bug Fixes**
  - Fixed URL construction issues to prevent double slashes, ensuring smoother connectivity.
- **Refactor**
  - Enhanced security handling with streamlined initialization and clearer error messaging for a more robust experience.
  - Renamed message-related classes for consistency across the codebase.
  - Adjusted method signatures and return types for clarity and consistency.
- **New Features**
  - Introduced global variables for public key and client ID in the security controller.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/config/config.json b/config/config.json
index 94556948..5ff7c859 100644
--- a/config/config.json
+++ b/config/config.json
@@ -1,43 +1,42 @@
 {
-    "logging": {
-        "console": {
-            "level": "info",
-            "use": true
-        },
-        "file": {
-            "level": "info",
-            "use": true,
-            "path": "./log/"
-        }
+  "logging": {
+    "console": {
+      "level": "debug",
+      "use": true
     },
-    "http_server": {
-        "base_uri": "https://test-api.bringautofleet.com/v2/protocol",
-        "port": 8080
-    },
-    "database": {
-        "server": {
-            "location": "localhost",
-            "port": 5432,
-            "username": "postgres",
-            "password": "1234",
-            "database_name": "protocol_api"
-        },
-        "cleanup": {
-            "timing_in_seconds": {
-                "retention_period": 1200,
-                "cleanup_period": 60
-
-            }
-        }
-    },
-    "request_for_messages": {
-        "timeout_in_seconds": 5
+    "file": {
+      "level": "info",
+      "use": true,
+      "path": "./log/"
+    }
+  },
+  "http_server": {
+    "base_uri": "https://test-api.bringautofleet.com/v2/protocol",
+    "port": 8080
+  },
+  "database": {
+    "server": {
+      "location": "localhost",
+      "port": 5432,
+      "username": "postgres",
+      "password": "1234",
+      "database_name": "protocol_api"
     },
-    "security": {
-        "keycloak_url": "https://keycloak.bringauto.com",
-        "client_id": "",
-        "client_secret_key": "",
-        "scope": "",
-        "realm": ""
+    "cleanup": {
+      "timing_in_seconds": {
+        "retention_period": 1200,
+        "cleanup_period": 60
+      }
     }
-}
\ No newline at end of file
+  },
+  "request_for_messages": {
+    "timeout_in_seconds": 5
+  },
+  "security": {
+    "keycloak_url": "https://keycloak.bringauto.com",
+    "client_id": "",
+    "client_secret_key": "",
+    "scope": "",
+    "realm": ""
+  }
+}
diff --git a/server/__main__.py b/server/__main__.py
index 79abe711..a15ba695 100644
--- a/server/__main__.py
+++ b/server/__main__.py
@@ -1,25 +1,21 @@
 import sys
-
-sys.path.append("server")
 import logging
-import requests  # type: ignore
 import os
 
+sys.path.append("server")
+
+import requests  # type: ignore
 import connexion  # type: ignore
-from fleetv2_http_api import encoder  # type: ignore
 from apscheduler.schedulers.background import BackgroundScheduler  # type: ignore
 
+from .fleetv2_http_api import encoder  # type: ignore
 from .config import CleanupTiming
 from server.database.database_controller import remove_old_messages, set_message_retention_period  # type: ignore
 from server.database.cache import clear_connected_cars  # type: ignore
 from server.database.connection import set_db_connection  # type: ignore
 from server.database.time import timestamp  # type: ignore
-from server.fleetv2_http_api.impl.controllers import (  # type: ignore
-    set_car_wait_timeout_s,
-    set_status_wait_timeout_s,
-    set_command_wait_timeout_s,
-    init_security,
-)
+
+import server.fleetv2_http_api.impl.controllers as api_controllers  # type: ignore
 from server.fleetv2_http_api.controllers.security_controller import set_auth_params  # type: ignore
 import server.database.script_args as script_args  # type: ignore
 from server.logs import configure_logging, LOGGER_NAME
@@ -65,7 +61,7 @@ def _set_up_database_jobs(config: CleanupTiming) -> None:
 def _retrieve_keycloak_public_key(keycloak_url: str, realm: str) -> str:
     """Retrieve the public key from the Keycloak server."""
     try:
-        response = requests.get(keycloak_url + "/realms/" + realm)
+        response = requests.get(keycloak_url.rstrip("/") + "/realms/" + realm)
         response.raise_for_status()
         logger.info("Retrieved public key from Keycloak server.")
         return response.json()["public_key"]
@@ -92,10 +88,10 @@ def run_server(port: int = 8080) -> None:
     configure_logging(COMPONENT_NAME, config)
     _connect_to_database(vals)
     _set_up_database_jobs(config.database.cleanup.timing_in_seconds)
-    set_car_wait_timeout_s(config.request_for_messages.timeout_in_seconds)
-    set_status_wait_timeout_s(config.request_for_messages.timeout_in_seconds)
-    set_command_wait_timeout_s(config.request_for_messages.timeout_in_seconds)
-    init_security(config.security, str(config.http_server.base_uri))
+    api_controllers.set_car_wait_timeout_s(config.request_for_messages.timeout_in_seconds)
+    api_controllers.set_status_wait_timeout_s(config.request_for_messages.timeout_in_seconds)
+    api_controllers.set_command_wait_timeout_s(config.request_for_messages.timeout_in_seconds)
+    api_controllers.init_oauth(config.security, str(config.http_server.base_uri))
     set_auth_params(
         public_key=_retrieve_keycloak_public_key(
             keycloak_url=str(config.security.keycloak_url), realm=config.security.realm
diff --git a/server/config.py b/server/config.py
index 167f38d1..5408e3b9 100644
--- a/server/config.py
+++ b/server/config.py
@@ -5,7 +5,7 @@
 
 
 LoggingLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
-from .fleetv2_http_api.impl.security import SecurityConfig as _SecurityConfig
+from .fleetv2_http_api.impl.security import SecurityConfig as SecurityConfig
 
 
 class APIConfig(pydantic.BaseModel):
@@ -13,7 +13,7 @@ class APIConfig(pydantic.BaseModel):
     http_server: HTTPServer
     database: Database
     request_for_messages: MessageRequest
-    security: _SecurityConfig
+    security: SecurityConfig
 
 
 class Logging(pydantic.BaseModel):
diff --git a/server/database/database_controller.py b/server/database/database_controller.py
index 2be223ee..a4d590b8 100644
--- a/server/database/database_controller.py
+++ b/server/database/database_controller.py
@@ -20,7 +20,7 @@
     Base,
     get_connection_source as _get_connection_source,
     set_db_connection as _set_db_connection,
-    set_test_db_connection as _set_test_db_connection
+    set_test_db_connection as _set_test_db_connection,
 )
 from server.database.cache import (  # type: ignore
     add_car,
@@ -31,8 +31,6 @@
     clear_connected_cars as _clear_connected_cars,
 )
 from server.fleetv2_http_api.models.device_id import DeviceId  # type: ignore
-
-
 from ..logs import LOGGER_NAME
 
 
@@ -75,7 +73,7 @@ def set_data_retention_period(cls, seconds: int) -> None:
 
     @staticmethod
     def from_message(
-        company_name: str, car_name: str, message: Message_DB, order: int = 0
+        company_name: str, car_name: str, message: MessageDB, order: int = 0
     ) -> MessageBase:
         return MessageBase(
             timestamp=message.timestamp,
@@ -93,7 +91,7 @@ def from_message(
         )
 
     @staticmethod
-    def from_messages(company_name: str, car_name: str, *messages: Message_DB) -> list[MessageBase]:
+    def from_messages(company_name: str, car_name: str, *messages: MessageDB) -> list[MessageBase]:
         bases: list[MessageBase] = list()
         for k in range(len(messages)):
             bases.append(
@@ -101,8 +99,8 @@ def from_messages(company_name: str, car_name: str, *messages: Message_DB) -> li
             )
         return bases
 
-    def to_message(self) -> Message_DB:
-        return Message_DB(
+    def to_message(self) -> MessageDB:
+        return MessageDB(
             timestamp=self.timestamp,
             module_id=self.module_id,
             device_type=self.device_type,
@@ -116,7 +114,7 @@ def to_message(self) -> Message_DB:
 
 
 @dataclasses.dataclass
-class Message_DB:
+class MessageDB:
     """Object defining the structure of messages sent to and retrieved from the database."""
 
     timestamp: int
@@ -153,7 +151,7 @@ def get_available_devices_from_database() -> None:
 
 
 def send_messages_to_database(
-    company_name: str, car_name: str, *messages: Message_DB
+    company_name: str, car_name: str, *messages: MessageDB
 ) -> tuple[str, int]:
     """Send a list of messages to the database, returns number of succesfully sent messages (int)."""
     try:
@@ -166,7 +164,7 @@ def send_messages_to_database(
     except _IntegrityError:
         return (
             "Some of the messages are identical to those sent previously, including their timestamps.",
-            400
+            400,
         )
     except _DatabaseNotAccessible:
         return "Database not accessible.", 503
@@ -183,7 +181,7 @@ def _get_message_for_n_messages_succesfully_sent(number_of_sent_messages: int) -
 
 def list_messages(
     company_name: str, car_name: str, message_type: tuple[str, ...], since: int = 0
-) -> list[Message_DB]:  # noqa:
+) -> list[MessageDB]:  # noqa:
     """Return a list of messages of the given type, optionally filtered by the given parameters.
     If all is not None, then all messages of the given type are returned.
 
@@ -191,7 +189,7 @@ def list_messages(
     less or equal to since are returned. Otherwise, the newest message is returned.
     """
 
-    statuses: list[Message_DB] = list()
+    statuses: list[MessageDB] = list()
     with Session(_get_connection_source()) as session:
         table = MessageBase.__table__
         selection = select(MessageBase)
diff --git a/server/fleetv2_http_api/controllers/security_controller.py b/server/fleetv2_http_api/controllers/security_controller.py
index 4ee1f9f6..f67dca07 100644
--- a/server/fleetv2_http_api/controllers/security_controller.py
+++ b/server/fleetv2_http_api/controllers/security_controller.py
@@ -3,6 +3,7 @@
 from server.database.security import get_admin
 import jwt
 
+
 _public_key: str
 _client_id: str
 
diff --git a/server/fleetv2_http_api/impl/controllers.py b/server/fleetv2_http_api/impl/controllers.py
index e3a5a555..1d45e8dd 100644
--- a/server/fleetv2_http_api/impl/controllers.py
+++ b/server/fleetv2_http_api/impl/controllers.py
@@ -11,7 +11,7 @@
 from server.fleetv2_http_api.models import Payload, DeviceId, Message, Module, Car  # type: ignore
 from server.database.database_controller import (  # type: ignore
     send_messages_to_database,
-    Message_DB,
+    MessageDB,
     cleanup_device_commands_and_warn_before_future_commands,
 )
 from server.database.database_controller import list_messages as _list_messages
@@ -26,15 +26,10 @@
 from server.database.time import timestamp as _timestamp  # type: ignore
 from server.fleetv2_http_api.impl.message_wait import MessageWaitObjManager as _MessageWaitObjManager  # type: ignore
 from server.fleetv2_http_api.impl.car_wait import CarWaitObjManager as _CarWaitObjManager  # type: ignore
-from server.fleetv2_http_api.impl.security import (  # type: ignore
-    SecurityObj as _SecurityObj,
-    SecurityObjImpl as _SecurityObjImpl,
-    SecurityConfig as _SecurityConfig,
-    KeycloakClient as _KeycloakClient,
-    empty_security_obj as _empty_security_obj,
-    OAuthAuthenticationNotSet as _OAuthAuthenticationNotSet,
-)
+
+import server.fleetv2_http_api.impl.security as _msecurity  # type: ignore
 from server.logs import LOGGER_NAME as _LOGGER_NAME
+from server.config import SecurityConfig as _SecurityConfig
 
 
 logger = logging.getLogger(_LOGGER_NAME)
@@ -46,7 +41,6 @@
 _status_wait_manager = _MessageWaitObjManager()
 _cmd_wait_manager = _MessageWaitObjManager()
 _car_wait_manager = _CarWaitObjManager()
-_security: _SecurityObj = _empty_security_obj
 
 
 def set_status_wait_timeout_s(timeout_s: float) -> None:
@@ -67,29 +61,20 @@ def get_command_wait_timeout_s() -> float:
     return _cmd_wait_manager.timeout_ms * 0.001
 
 
-def init_security(config: Any, base_uri: str) -> None:
+def init_oauth(config: _SecurityConfig, base_uri: str) -> None:
+    """Initialize OAuth with the given configuration and base URI."""
     client = _get_keycloak_openid_client(config)
-    init_security_with_client(config, base_uri, client)
-
-
-def init_security_with_client(config: Any, base_uri: str, client: _KeycloakClient) -> None:
-    global _security
-    _security = _SecurityObjImpl(config, base_uri, client)
-    if not _security.is_not_empty():
-        raise RuntimeError("Using empty security object - Keycloak authentication is not set up.")
-
+    _msecurity.init_oauth(config, base_uri, client)
+    if _msecurity.security.is_empty():
+        logger.warning("Keycloak authentication has not been initialized.")
 
-def deinit_security() -> None:
-    global _security
-    _security = _empty_security_obj
 
-
-def _get_keycloak_openid_client(config: _SecurityConfig) -> KeycloakOpenID:
+def _get_keycloak_openid_client(config: _msecurity.SecurityConfig) -> KeycloakOpenID:
     client = KeycloakOpenID(
-        server_url=config.keycloak_url,
-        client_id=config.client_id,
-        realm_name=config.realm,
-        client_secret_key=config.client_secret_key,
+        server_url=str(config.keycloak_url),
+        client_id=str(config.client_id),
+        realm_name=str(config.realm),
+        client_secret_key=str(config.client_secret_key),
     )
     return client
 
@@ -112,9 +97,9 @@ def login(device: Optional[str] = None) -> WerkzeugResponse | Response | tuple[d
     """
     if device == "":
         try:
-            auth_json = _security.device_get_authentication()
+            auth_json = _msecurity.security.device_get_authentication()
             return _log_info_and_respond(auth_json, 200, "Device authentication initialized.")
-        except _OAuthAuthenticationNotSet as e:
+        except _msecurity.UninitializedOAuth as e:
             msg = "Cannot get device authentication. Keycloak authentication is not set on the HTTP API."
             _log_debug(str(e))
             return _log_info_and_respond(msg, 500, msg)
@@ -125,10 +110,10 @@ def login(device: Optional[str] = None) -> WerkzeugResponse | Response | tuple[d
 
     elif device != None:
         try:
-            token = _security.device_token_get(device)  # type: ignore
+            token = _msecurity.security.device_token_get(device)  # type: ignore
             return _log_info_and_respond(token, 200, "Device authenticated, jwt token generated.")
-        except _OAuthAuthenticationNotSet as e:
-            msg = "Cannot get device authentication. Keycloak authentication is not set on the HTTP API."
+        except _msecurity.UninitializedOAuth as e:
+            msg = "Cannot get device token. Keycloak authentication is not set on the HTTP API."
             _log_debug(str(e))
             return _log_info_and_respond(msg, 500, msg)
         except Exception as e:
@@ -136,16 +121,14 @@ def login(device: Optional[str] = None) -> WerkzeugResponse | Response | tuple[d
             _log_debug(str(e))
             return _log_info_and_respond(msg, 400, msg)
     try:
-        return redirect(_security.get_authentication_url())
-    except _OAuthAuthenticationNotSet as e:
-        msg = (
-            "Cannot get device authentication. Keycloak authentication is not set on the HTTP API."
-        )
+        return redirect(_msecurity.security.get_authentication_url())
+    except _msecurity.UninitializedOAuth as e:
+        msg = "Cannot get authentication URL. Keycloak authentication is not set on the HTTP API."
         _log_debug(str(e))
         return _log_info_and_respond(msg, 500, msg)
     except Exception as e:
         msg = "Problem reaching oAuth service."
-        _log_debug(str(e))
+        _log_debug("(Device is None): " + str(e))
         return _log_info_and_respond(msg, 500, msg)
 
 
@@ -171,9 +154,9 @@ def token_get(
     :rtype: dict
     """
     try:
-        token = _security.token_get(state, iss, code)  # type: ignore
+        token = _msecurity.security.token_get(state, iss, code)  # type: ignore
         return _log_info_and_respond(token, 200, "Jwt token generated.")
-    except _OAuthAuthenticationNotSet as e:
+    except _msecurity.UninitializedOAuth as e:
         msg = "Cannot get token. Keycloak authentication is not set on the HTTP API."
         _log_debug(str(e))
         return _log_info_and_respond(msg, 500, msg)
@@ -194,8 +177,8 @@ def token_refresh(refresh_token: str) -> tuple[dict, int]:
     :rtype: dict
     """
     try:
-        token = _security.token_refresh(refresh_token)
-    except _OAuthAuthenticationNotSet as e:
+        token = _msecurity.security.token_refresh(refresh_token)
+    except _msecurity.UninitializedOAuth as e:
         msg = "Cannot refresh token. Keycloak authentication is not set on the HTTP API."
         _log_debug(str(e))
         return _log_info_and_respond(msg, 500, msg)
@@ -401,7 +384,7 @@ def send_commands(
         response = _car_availability(company_name, car_name)
         if response[1] != 200:
             return response
-        msg = f"Empty list of commands was sent to the API; no commands were sent to the device."
+        msg = "Empty list of commands was sent to the API; no commands were sent to the device."
         return _log_info_and_respond(msg, 200, msg)
     errors = _check_sent_commands(company_name, car_name, messages)
     if errors[0] != "":
@@ -437,7 +420,7 @@ def send_statuses(
     messages = _message_list_from_request_body(body)
 
     if messages == []:
-        msg = f"Empty list of statuses was sent to the API; no statuses were sent to the device."
+        msg = "Empty list of statuses was sent to the API; no statuses were sent to the device."
         return _log_info_and_respond(msg, 200, msg)
     errors = _check_messages((MessageType.STATUS, MessageType.STATUS_ERROR), *messages)
     if errors[0] != "":
@@ -456,7 +439,7 @@ def send_statuses(
 def _message_list_from_request_body(body: list[dict | Message]) -> list[Message]:
     messages: list[Message] = list()
     for item in body:
-        messages.append(Message.from_dict(item) if type(item) == dict else item)
+        messages.append(Message.from_dict(item) if isinstance(item, dict) else item)
     return messages
 
 
@@ -507,7 +490,7 @@ def _check_messages(expected_message_types: tuple[str, ...], *messages: Message)
 
     errors: str = ""
     errors = _check_message_types(expected_message_types, *messages)
-    if not errors.strip() == "":
+    if errors.strip() != "":
         return errors, 400
     else:
         return "", 200
@@ -573,7 +556,7 @@ def _handle_first_status_and_return_warnings(
     )
 
 
-def _message_from_db(message_db: Message_DB) -> Message:
+def _message_from_db(message_db: MessageDB) -> Message:
     """Convert Message_DB to Message."""
     return Message(
         timestamp=message_db.timestamp,
@@ -591,13 +574,13 @@ def _message_from_db(message_db: Message_DB) -> Message:
     )
 
 
-def _message_db_list(messages: list[Message]) -> list[Message_DB]:
+def _message_db_list(messages: list[Message]) -> list[MessageDB]:
     """Convert list of messages to list of Message_DB."""
     for m in messages:
         if isinstance(m.payload.message_type, MessageType):
             m.payload.message_type = m.payload.message_type.value
     return [
-        Message_DB(
+        MessageDB(
             timestamp=message.timestamp,
             serialized_device_id=_serialized_device_id(message.device_id),
             module_id=message.device_id.module_id,
@@ -643,7 +626,7 @@ def _response_for_request_for_disconnected_cars_commands(
     )
 
 
-def _update_messages_timestamp(messages: Iterable[Message_DB]) -> int:
+def _update_messages_timestamp(messages: Iterable[MessageDB]) -> int:
     timestamp_now = _timestamp()
     for message in messages:
         message.timestamp = timestamp_now
diff --git a/server/fleetv2_http_api/impl/message_wait.py b/server/fleetv2_http_api/impl/message_wait.py
index c63c3aa6..958e5dca 100644
--- a/server/fleetv2_http_api/impl/message_wait.py
+++ b/server/fleetv2_http_api/impl/message_wait.py
@@ -16,9 +16,12 @@ def __init__(self, timeout_ms: int = _default_timeout_ms) -> None:
         self._wait_dict: dict[str, dict[str, list[MessageWaitObj]]] = dict()
 
     @property
-    def timeout_ms(self) -> int: return self._timeout_ms
+    def timeout_ms(self) -> int:
+        return self._timeout_ms
 
-    def add_response_content_and_stop_waiting(self, company: str, car: str, reponse_content: list[Message]) -> None:
+    def add_response_content_and_stop_waiting(
+        self, company: str, car: str, reponse_content: list[Message]
+    ) -> None:
         """Make the next wait object in the queue to respond with specified 'reponse_content' and remove it from the queue."""
         wait_objs = self._get_wait_objects_for_given_car(company, car)
         if wait_objs:
@@ -29,9 +32,9 @@ def new_wait_obj(self, company: str, car_name: str) -> MessageWaitObj:
         """Create a new wait object and adds it to the wait queue for given company and car."""
         wait_obj = MessageWaitObj(company, car_name, self._timeout_ms)
 
-        if not company in self._wait_dict:
+        if company not in self._wait_dict:
             self._wait_dict[company] = dict()
-        if not car_name in self._wait_dict[company]:
+        if car_name not in self._wait_dict[company]:
             self._wait_dict[company][car_name] = list()
 
         self._wait_dict[company][car_name].append(wait_obj)
@@ -66,14 +69,15 @@ def wait_and_get_reponse(self, company: str, car_name: str) -> list[Message]:
 
         return msgs
 
-    def _send_content_to_all_wait_objs(self, wait_objs: list[MessageWaitObj], reponse_content: list[Message]) -> None:
+    def _send_content_to_all_wait_objs(
+        self, wait_objs: list[MessageWaitObj], reponse_content: list[Message]
+    ) -> None:
         for wait_obj in wait_objs:
             wait_obj.add_reponse_content_and_stop_waiting(reponse_content)
 
     def _get_wait_objects_for_given_car(self, company: str, car: str) -> list[MessageWaitObj]:
-        if company in self._wait_dict:
-            if car in self._wait_dict[company]:
-                return self._wait_dict[company][car]
+        if company in self._wait_dict and car in self._wait_dict[company]:
+            return self._wait_dict[company][car]
         return list()
 
     def _remove_wait_obj_list(self, company: str, car: str) -> None:
@@ -96,9 +100,12 @@ def __init__(self, company: str, car: str, timeout_ms: int) -> None:
         self._condition = threading.Condition()
 
     @property
-    def company(self) -> str: return self._company
+    def company(self) -> str:
+        return self._company
+
     @property
-    def car_name(self) -> str: return self._car_name
+    def car_name(self) -> str:
+        return self._car_name
 
     def add_reponse_content_and_stop_waiting(self, content: list[Message]) -> None:
         self._response_content = content.copy()
@@ -108,10 +115,10 @@ def add_reponse_content_and_stop_waiting(self, content: list[Message]) -> None:
     def wait_and_get_response(self) -> list[Message]:
         """Wait for the response object to be set and then return it."""
         with self._condition:
-            self._condition.wait(timeout=self._timeout_ms/1000)
+            self._condition.wait(timeout=self._timeout_ms / 1000)
         return self._response_content
 
     @staticmethod
     def timestamp() -> int:
         """Unix timestamp in milliseconds."""
-        return int(time.time()*1000)
+        return int(time.time() * 1000)
diff --git a/server/fleetv2_http_api/impl/security.py b/server/fleetv2_http_api/impl/security.py
index 6f35a9fd..cd7ddc67 100644
--- a/server/fleetv2_http_api/impl/security.py
+++ b/server/fleetv2_http_api/impl/security.py
@@ -1,9 +1,9 @@
 from __future__ import annotations
 import abc
-from typing import Protocol
-from urllib.parse import urlparse
+from typing import Protocol, Any
 import uuid
 
+from urllib.parse import urlparse
 import pydantic
 
 
@@ -39,6 +39,11 @@ class SecurityConfig(pydantic.BaseModel):
 class SecurityObj(abc.ABC):
     """Class for handling keycloak authentication."""
 
+    @classmethod
+    def is_defined(cls) -> bool:
+        """Check if the security object is defined."""
+        return hasattr(cls, "instance")
+
     @abc.abstractmethod
     def __init__(
         self, config: SecurityConfig, base_uri: str, openid_client: KeycloakClient
@@ -65,9 +70,9 @@ def device_token_get(self, device_code: str) -> dict:
     def token_refresh(self, refresh_token: str) -> dict:
         pass
 
-    def is_not_empty(self) -> bool:
+    def is_empty(self) -> bool:
         """Check if the security object is not empty and has all keycloak authentication information."""
-        return self is not empty_security_obj
+        return self is _empty_security_obj
 
 
 class SecurityObjEmpty(SecurityObj):
@@ -77,36 +82,38 @@ def __init__(self, config: SecurityConfig, base_uri: str, *args, **kwargs) -> No
         pass
 
     def get_authentication_url(self) -> str:
-        raise OAuthAuthenticationNotSet(
-            "Cannot get authentication URL. Keycloak authentication is not set"
+        raise UninitializedOAuth(
+            "Cannot get authentication URL. Keycloak authentication is not set. Security object is empty."
         )
 
     def device_get_authentication(self) -> dict:
-        raise OAuthAuthenticationNotSet(
-            "Cannot get device authentication. Keycloak authentication is not set"
+        raise UninitializedOAuth(
+            "Cannot get device authentication. Keycloak authentication is not set.  Security object is empty."
         )
 
     def token_get(self, state: str, iss: str, code: str) -> dict:
-        raise OAuthAuthenticationNotSet(
-            "Cannot get token. Keycloak authentication is not set",
+        raise UninitializedOAuth(
+            "Cannot get token. Keycloak authentication is not set.  Security object is empty.",
         )
 
     def device_token_get(self, device_code: str) -> dict:
-        raise OAuthAuthenticationNotSet(
-            "Cannot get device token. Keycloak authentication is not set"
+        raise UninitializedOAuth(
+            "Cannot get device token. Keycloak authentication is not set.  Security object is empty."
         )
 
     def token_refresh(self, refresh_token: str) -> dict:
-        raise OAuthAuthenticationNotSet("Cannot refresh token. Keycloak authentication is not set")
+        raise UninitializedOAuth(
+            "Cannot refresh token. Keycloak authentication is not set.  Security object is empty."
+        )
 
 
-class OAuthAuthenticationNotSet(Exception):
+class UninitializedOAuth(Exception):
     pass
 
 
-empty_security_obj = SecurityObjEmpty(
+_empty_security_obj = SecurityObjEmpty(
     config=SecurityConfig(
-        keycloak_url="https://empty",
+        keycloak_url=pydantic.AnyUrl("https://empty.com"),
         scope="",
         client_id="",
         client_secret_key="",
@@ -123,10 +130,10 @@ def __init__(
     ) -> None:
         """Set configuration for keycloak authentication and initialize KeycloakOpenID."""
 
-        self._keycloak_url = config.keycloak_url
+        self._keycloak_url = str(config.keycloak_url)
         self._scope = config.scope
         self._realm_name = config.realm
-        self._callback = base_uri + "/token_get"
+        self._callback = str(base_uri) + "/token_get"
         self._state = uuid.uuid4().hex
         self._client = keycloak_client
 
@@ -179,3 +186,18 @@ def _expected_issuer(self) -> str:
     def issuer_url(issuer: str) -> str:
         """Parse issuer URL from keycloak configuration."""
         return urlparse(issuer).geturl()
+
+
+security: SecurityObj = _empty_security_obj
+
+
+def init_oauth(config: Any, base_uri: str, client: KeycloakClient) -> None:
+    global security
+    security = SecurityObjImpl(config, base_uri, client)
+    if security.is_empty():
+        raise RuntimeError("Security object is empty after initialization.")
+
+
+def deinit_oauth() -> None:
+    global security
+    security = _empty_security_obj
diff --git a/tests/authentication/test_oauth_authentication.py b/tests/authentication/test_oauth_authentication.py
index a84c87bf..72beea72 100644
--- a/tests/authentication/test_oauth_authentication.py
+++ b/tests/authentication/test_oauth_authentication.py
@@ -6,16 +6,14 @@
 from server.fleetv2_http_api.impl.security import (
     SecurityConfig,
     SecurityObjImpl,
-    empty_security_obj,
-    OAuthAuthenticationNotSet,
+    _empty_security_obj,
+    init_oauth,
+    deinit_oauth,
+    UninitializedOAuth,
     GetTokenStateMismatch,
     GetTokenIssuerMismatch,
 )
-from server.fleetv2_http_api.impl.controllers import (
-    init_security_with_client,
-    login,
-    deinit_security,
-)
+from server.fleetv2_http_api.impl.controllers import login
 
 
 TEST_TOKEN_HEADER = {"alg": "RS256", "typ": "JWT", "kid": "test"}
@@ -77,14 +75,14 @@ def setUp(self):
         )
 
     def test_security_obj_empty_raises_exception(self):
-        with self.assertRaises(OAuthAuthenticationNotSet):
-            empty_security_obj.get_authentication_url()
-        with self.assertRaises(OAuthAuthenticationNotSet):
-            empty_security_obj.device_get_authentication()
-        with self.assertRaises(OAuthAuthenticationNotSet):
-            empty_security_obj.token_get("", "", "")
-        with self.assertRaises(OAuthAuthenticationNotSet):
-            empty_security_obj.device_token_get("")
+        with self.assertRaises(UninitializedOAuth):
+            _empty_security_obj.get_authentication_url()
+        with self.assertRaises(UninitializedOAuth):
+            _empty_security_obj.device_get_authentication()
+        with self.assertRaises(UninitializedOAuth):
+            _empty_security_obj.token_get("", "", "")
+        with self.assertRaises(UninitializedOAuth):
+            _empty_security_obj.device_token_get("")
 
     def test_authentication_url(self) -> None:
         security_obj = SecurityObjImpl(self.config, "https://somebasicuri", self.client_test)
@@ -122,7 +120,7 @@ def setUp(self):
             realm="test",
             client_secret_key="test",
         )
-        deinit_security()
+        deinit_oauth()
 
     def test_calling_login_without_initializing_security_yields_500_error(self):
         response = login()
@@ -133,12 +131,12 @@ def test_calling_login_with_device_without_initializing_security_yields_500_erro
         self.assertEqual(response[1], 500)
 
     def test_calling_login_after_initializing_security_yields_302_code(self):
-        init_security_with_client(self.config, "https://somebasicuri", self.client_test)
+        init_oauth(self.config, "https://somebasicuri", self.client_test)
         response = login()
         self.assertEqual(response.status_code, 302)
 
     def test_calling_login_after_initializing_security_with_device_yields_302_code(self):
-        init_security_with_client(self.config, "https://somebasicuri", self.client_test)
+        init_oauth(self.config, "https://somebasicuri", self.client_test)
         response = login("some_device")
         self.assertEqual(response[1], 200)
 
diff --git a/tests/database/test_database_controller.py b/tests/database/test_database_controller.py
index 108ee5fa..ba12ed2d 100644
--- a/tests/database/test_database_controller.py
+++ b/tests/database/test_database_controller.py
@@ -21,7 +21,7 @@
     list_messages,
     cleanup_device_commands_and_warn_before_future_commands,
     remove_old_messages,
-    Message_DB,
+    MessageDB,
     MessageBase,
 )
 from tests._utils.logs import clear_logs
@@ -34,7 +34,7 @@ def test_creating_message_base_object_from_message(self):
         car_name = "test_car"
         device_id = DeviceId(module_id=45, type=2, role="role1", name="device1")
         sdevice_id = serialized_device_id(device_id)
-        message_db = Message_DB(
+        message_db = MessageDB(
             timestamp=100,
             serialized_device_id=sdevice_id,
             module_id=device_id.module_id,
@@ -110,7 +110,7 @@ def test_send_messages_to_database(self, mock_time_in_ms: Mock):
         device_id_2 = DeviceId(module_id=45, type=2, role="role1", name="device2")
         sdevice_id_2 = serialized_device_id(device_id_1)
 
-        message1 = Message_DB(
+        message1 = MessageDB(
             timestamp=1,
             serialized_device_id=sdevice_id_1,
             module_id=device_id_1.module_id,
@@ -121,7 +121,7 @@ def test_send_messages_to_database(self, mock_time_in_ms: Mock):
             payload_encoding=EncodingType.BASE64,
             payload_data={"key1": "value1"},
         )
-        message2 = Message_DB(
+        message2 = MessageDB(
             timestamp=7,
             serialized_device_id=sdevice_id_2,
             module_id=device_id_2.module_id,
@@ -156,7 +156,7 @@ def test_cleanup_device_commands_and_warn_before_future_commands(self, mock_time
         send_messages_to_database(
             "test_company",
             "test_car",
-            Message_DB(
+            MessageDB(
                 timestamp=0,
                 serialized_device_id=sdevice_id,
                 module_id=device_id.module_id,
@@ -172,7 +172,7 @@ def test_cleanup_device_commands_and_warn_before_future_commands(self, mock_time
         send_messages_to_database(
             "test_company",
             "test_car",
-            Message_DB(
+            MessageDB(
                 timestamp=MessageBase.data_retention_period_ms() + 100,
                 serialized_device_id=sdevice_id,
                 module_id=device_id.module_id,
@@ -188,7 +188,7 @@ def test_cleanup_device_commands_and_warn_before_future_commands(self, mock_time
         send_messages_to_database(
             "test_company",
             "test_car",
-            Message_DB(
+            MessageDB(
                 timestamp=MessageBase.data_retention_period_ms() + 150,
                 serialized_device_id=sdevice_id,
                 module_id=device_id.module_id,
@@ -212,9 +212,7 @@ def test_cleanup_device_commands_and_warn_before_future_commands(self, mock_time
         self.assertEqual(len(warnings), 2)
         # Check that the device commands were cleaned up
         messages = list_messages(
-            company_name="test_company",
-            car_name="test_car",
-            message_type=(MessageType.COMMAND,)
+            company_name="test_company", car_name="test_car", message_type=(MessageType.COMMAND,)
         )
         self.assertEqual(len(messages), 0)
 
@@ -222,7 +220,7 @@ def test_cleanup_device_commands_and_warn_before_future_commands(self, mock_time
     def test_remove_old_messages(self, mock_time_in_ms: Mock):
         device_id = DeviceId(module_id=45, type=2, role="role1", name="device1")
         sdevice_id = serialized_device_id(device_id)
-        message1 = Message_DB(
+        message1 = MessageDB(
             timestamp=0,
             serialized_device_id=sdevice_id,
             module_id=device_id.module_id,
@@ -233,7 +231,7 @@ def test_remove_old_messages(self, mock_time_in_ms: Mock):
             payload_encoding=EncodingType.JSON,
             payload_data={"key1": "value1"},
         )
-        message2 = Message_DB(
+        message2 = MessageDB(
             timestamp=50,
             serialized_device_id=sdevice_id,
             module_id=device_id.module_id,
@@ -255,9 +253,7 @@ def test_remove_old_messages(self, mock_time_in_ms: Mock):
 
         # Check that the old messages were removed from the database
         messages = list_messages(
-            company_name="company1",
-            car_name="car1",
-            message_type=(MessageType.STATUS,)
+            company_name="company1", car_name="car1", message_type=(MessageType.STATUS,)
         )
         self.assertEqual(len(messages), 1)
 
@@ -295,7 +291,7 @@ def test_send_and_read_message(self, mock_time_in_ms: Mock):
         device_id = DeviceId(module_id=45, type=2, role="role1", name="device1")
         sdevice_id = serialized_device_id(device_id)
 
-        message_1 = Message_DB(
+        message_1 = MessageDB(
             timestamp=100,
             serialized_device_id=sdevice_id,
             module_id=device_id.module_id,
@@ -306,7 +302,7 @@ def test_send_and_read_message(self, mock_time_in_ms: Mock):
             payload_encoding=EncodingType.BASE64,
             payload_data={"content": "..."},
         )
-        message_2 = Message_DB(
+        message_2 = MessageDB(
             timestamp=150,
             serialized_device_id=sdevice_id,
             module_id=device_id.module_id,
@@ -326,14 +322,10 @@ def test_send_and_read_message(self, mock_time_in_ms: Mock):
         read_messages = list_messages("test_company", "test_car", (MessageType.STATUS,))
         self.assertEqual(len(read_messages), 2)
         # read only the last status
-        read_messages = list_messages(
-            "test_company", "test_car", (MessageType.STATUS,), since=150
-        )
+        read_messages = list_messages("test_company", "test_car", (MessageType.STATUS,), since=150)
         self.assertEqual(len(read_messages), 1)
         # read only statuses after timestamp 120
-        read_messages = list_messages(
-            "test_company", "test_car", (MessageType.STATUS,), since=120
-        )
+        read_messages = list_messages("test_company", "test_car", (MessageType.STATUS,), since=120)
         self.assertEqual(len(read_messages), 1)
         self.assertEqual(read_messages[0].timestamp, 150)
 


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bd6e7ff1889f629c6c9a1a9eadde6f22fa75b5dc and e6002838657f44b65f83177568de20391306bcc5.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `config/config.json` (1 hunks)
* `server/__main__.py` (1 hunks)
* `server/fleetv2_http_api/impl/controllers.py` (6 hunks)
* `server/fleetv2_http_api/impl/security.py` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>server/fleetv2_http_api/impl/security.py (2)</summary><blockquote>

<details>
<summary>server/fleetv2_http_api/impl/controllers.py (2) (2)</summary>

* `token_get` (165:196)
* `token_refresh` (199:219)

</details>
<details>
<summary>server/fleetv2_http_api/controllers/login_controller.py (2) (2)</summary>

* `token_get` (22:38)
* `token_refresh` (41:51)

</details>

</blockquote></details>
<details>
<summary>server/fleetv2_http_api/impl/controllers.py (1)</summary><blockquote>

<details>
<summary>server/fleetv2_http_api/impl/security.py (19) (19)</summary>

* `is_not_empty` (73:75)
* `device_get_authentication` (58:59)
* `device_get_authentication` (89:92)
* `device_get_authentication` (147:150)
* `token` (25:25)
* `device_token_get` (66:67)
* `device_token_get` (99:102)
* `device_token_get` (167:174)
* `device` (24:24)
* `get_authentication_url` (54:55)
* `get_authentication_url` (84:87)
* `get_authentication_url` (140:145)
* `token_get` (62:63)
* `token_get` (94:97)
* `token_get` (152:165)
* `token_refresh` (70:71)
* `token_refresh` (104:107)
* `token_refresh` (176:179)
* `refresh_token` (26:26)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>config/config.json (1)</summary>

`2-42`: **Improved JSON formatting for better readability**

The changes to this file are purely formatting-related, maintaining consistent indentation and spacing throughout the JSON structure without altering any actual configuration values.

</details>
<details>
<summary>server/__main__.py (1)</summary>

`68-68`: **Improved URL construction by removing trailing slashes**

The addition of `rstrip("/")` prevents potential URL formation issues when concatenating paths. This is a good defensive programming practice that avoids duplicate slashes in the URL.

</details>
<details>
<summary>server/fleetv2_http_api/impl/security.py (2)</summary>

`42-45`: **New helper method for checking security object definition**

This new `is_defined` class method provides a clean way to check if the security object has been initialized, which will help with error handling and debugging throughout the codebase.

---

`86-87`: **Improved error message clarity**

Adding "Security object is empty" to the error messages provides clearer feedback about why Keycloak authentication is not available, distinguishing between an unset security object and an empty one.



Also applies to: 91-92, 96-97, 101-102, 106-107

</details>
<details>
<summary>server/fleetv2_http_api/impl/controllers.py (2)</summary>

`77-83`: **Enhanced security object initialization validation**

The assertion and debug logging provide better validation and visibility into the security object's initialization state, which will help with troubleshooting security-related issues.

---

`128-128`: **Updated security object access throughout the codebase**

All direct references to `_security` have been replaced with calls to `_get_security()`, ensuring consistent and safe access to the security object throughout the application.



Also applies to: 141-141, 152-152, 187-187, 210-210

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:09:14Z
user: login: MarioIvancik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:38:57Z
user: login: MarioIvancik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:49:54Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T07:12:36Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>server/fleetv2_http_api/impl/security.py (3)</summary><blockquote>

`73-75`: **Docstring mismatch with function name.**
The docstring describes checking if the security object is “not empty,” but the function is named `is_empty` and returns `True` only if the object is the `_empty_security_obj`. Consider updating the docstring or function name for clarity.

```diff
- """Check if the security object is not empty and has all keycloak authentication information."""
+ """Check if the security object is empty (no valid keycloak authentication information)."""
```

---

`133-136`: **Potential improvement for callback URI construction.**
Appending `"/token_get"` might cause double slashes if `base_uri` ends with a slash. Consider using a safer path join strategy (e.g., `urllib.parse.urljoin`).

```diff
- self._callback = str(base_uri) + "/token_get"
+ from urllib.parse import urljoin
+ self._callback = urljoin(str(base_uri) + "/", "token_get")
```

---

`191-191`: **Global variable usage for `security`.**
While this is functional, using a global can lead to coupling. Consider a more modular design if concurrency or multiple security contexts are needed.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e6002838657f44b65f83177568de20391306bcc5 and 1c7d29278231ccab26606dc4c2358c3bc5514a8c.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `config/config.json` (1 hunks)
* `server/__main__.py` (3 hunks)
* `server/config.py` (1 hunks)
* `server/database/database_controller.py` (7 hunks)
* `server/fleetv2_http_api/controllers/security_controller.py` (1 hunks)
* `server/fleetv2_http_api/impl/controllers.py` (14 hunks)
* `server/fleetv2_http_api/impl/message_wait.py` (5 hunks)
* `server/fleetv2_http_api/impl/security.py` (6 hunks)
* `tests/authentication/test_oauth_authentication.py` (2 hunks)
* `tests/database/test_database_controller.py` (13 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* server/fleetv2_http_api/impl/message_wait.py

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* config/config.json
* server/__main__.py

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (6)</summary>

<details>
<summary>server/config.py (1)</summary><blockquote>

<details>
<summary>server/fleetv2_http_api/impl/security.py (1) (1)</summary>

* `SecurityConfig` (29-36)

</details>

</blockquote></details>
<details>
<summary>tests/database/test_database_controller.py (3)</summary><blockquote>

<details>
<summary>server/database/database_controller.py (2) (2)</summary>

* `MessageDB` (117-128)
* `list_messages` (182-208)

</details>
<details>
<summary>server/enums.py (1) (1)</summary>

* `MessageType` (4-7)

</details>
<details>
<summary>server/database/cache.py (1) (1)</summary>

* `serialized_device_id` (170-171)

</details>

</blockquote></details>
<details>
<summary>tests/authentication/test_oauth_authentication.py (1)</summary><blockquote>

<details>
<summary>server/fleetv2_http_api/impl/security.py (13) (13)</summary>

* `UninitializedOAuth` (110-111)
* `get_authentication_url` (54-55)
* `get_authentication_url` (84-87)
* `get_authentication_url` (140-145)
* `device_get_authentication` (58-59)
* `device_get_authentication` (89-92)
* `device_get_authentication` (147-150)
* `token_get` (62-63)
* `token_get` (94-97)
* `token_get` (152-165)
* `device_token_get` (66-67)
* `device_token_get` (99-102)
* `device_token_get` (167-174)

</details>

</blockquote></details>
<details>
<summary>server/fleetv2_http_api/impl/security.py (2)</summary><blockquote>

<details>
<summary>server/fleetv2_http_api/impl/controllers.py (3) (3)</summary>

* `token_get` (135-166)
* `token_refresh` (169-189)
* `init_oauth` (64-69)

</details>
<details>
<summary>server/fleetv2_http_api/controllers/login_controller.py (2) (2)</summary>

* `token_get` (22-38)
* `token_refresh` (41-51)

</details>

</blockquote></details>
<details>
<summary>server/database/database_controller.py (1)</summary><blockquote>

<details>
<summary>server/database/connection.py (1) (1)</summary>

* `set_test_db_connection` (80-90)

</details>

</blockquote></details>
<details>
<summary>server/fleetv2_http_api/impl/controllers.py (2)</summary><blockquote>

<details>
<summary>server/database/database_controller.py (1) (1)</summary>

* `MessageDB` (117-128)

</details>
<details>
<summary>server/fleetv2_http_api/impl/security.py (20) (20)</summary>

* `SecurityConfig` (29-36)
* `device_get_authentication` (58-59)
* `device_get_authentication` (89-92)
* `device_get_authentication` (147-150)
* `UninitializedOAuth` (110-111)
* `token` (25-25)
* `device_token_get` (66-67)
* `device_token_get` (99-102)
* `device_token_get` (167-174)
* `device` (24-24)
* `get_authentication_url` (54-55)
* `get_authentication_url` (84-87)
* `get_authentication_url` (140-145)
* `token_get` (62-63)
* `token_get` (94-97)
* `token_get` (152-165)
* `token_refresh` (70-71)
* `token_refresh` (104-107)
* `token_refresh` (176-179)
* `refresh_token` (26-26)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (44)</summary><blockquote>

<details>
<summary>server/fleetv2_http_api/controllers/security_controller.py (2)</summary>

`6-6`: **Good addition of line separation before global variable declarations.**

This whitespace improves code readability by separating imports from variable declarations.

---

`7-8`: **Global variable declarations look good.**

The addition of these typed global variables provides clear types for the public key and client ID used in OAuth authentication.

</details>
<details>
<summary>tests/database/test_database_controller.py (3)</summary>

`24-24`: **Consistent class naming approved.**

Renamed `Message_DB` to `MessageDB` to follow Python class naming conventions (CamelCase), improving code consistency.

---

`37-37`: **Class name usage updated consistently.**

All instances of `Message_DB` have been properly updated to `MessageDB` throughout the test file, ensuring consistent naming.



Also applies to: 113-113, 124-124, 159-159, 175-175, 191-191, 223-223, 234-234, 294-294, 305-305

---

`215-216`: **Improved formatting for list_messages calls.**

Parameter formatting has been standardized, making the code more consistent and readable.



Also applies to: 255-257, 325-326, 328-329

</details>
<details>
<summary>server/config.py (2)</summary>

`8-8`: **Simplified import of SecurityConfig.**

Removed unnecessary aliasing with underscore prefix, making the code more straightforward and following Python conventions more closely.

---

`16-16`: **Updated type annotation to match import.**

This change maintains consistency with the updated import statement on line 8.

</details>
<details>
<summary>tests/authentication/test_oauth_authentication.py (2)</summary>

`9-10`: **Updated imports to match implementation changes.**

The imports now correctly reference `_empty_security_obj` and `UninitializedOAuth`, aligning with the changes made in the security implementation.

---

`80-87`: **Exception assertions updated properly.**

The test cases now correctly expect `UninitializedOAuth` exceptions when interacting with an empty security object, consistent with the implementation changes.

</details>
<details>
<summary>server/fleetv2_http_api/impl/security.py (10)</summary>

`3-3`: **Import of Protocol and Any looks good.**
No issues anticipated with these standard library imports.

---

`6-6`: **Import of urlparse is correct and consistent with usage below.**
No concerns here.

---

`42-45`: **Class method for checking definition is straightforward.**
The `is_defined` method simply checks for `"instance"` on the class, which is sufficient for this design pattern.

---

`85-86`: **Exception message clarity approved.**
Raising `UninitializedOAuth` is correct, and the message is explicit.

---

`91-92`: **Device authentication exception handling approved.**
Clean error handling ensures clarity for uninitialized security object.

---

`95-96`: **Token retrieval exception handling approved.**
This maintains consistency with other methods.

---

`100-101`: **Device token retrieval raise is coherent.**
No issues with raising `UninitializedOAuth` here.

---

`105-107`: **Token refresh raise logic is sound.**
Matches the pattern throughout this file for uninitialized OAuth usage.

---

`110-110`: **New exception class definition looks good.**
This descriptive name clarifies the error scenario.

---

`114-116`: **Initializing the empty security object with a placeholder URL is fine.**
Using `https://empty.com` is a valid approach for a dummy AnyUrl.

</details>
<details>
<summary>server/fleetv2_http_api/impl/controllers.py (16)</summary>

`29-30`: **Reorganized imports align with the new security module and config usage.**
No issues with referencing `_msecurity` or `_SecurityConfig`. 



Also applies to: 32-32

---

`64-69`: **`init_oauth` reflects the new flow.**
Initialization calls `_msecurity.init_oauth` and properly logs a warning if empty. The flow is clear.

---

`72-79`: **Adjusted `_get_keycloak_openid_client` signature.**
The shift to `_msecurity.SecurityConfig` is consistent with the new naming conventions; usage is aligned with KeycloakOpenID’s expected params.

---

`100-102`: **Device authentication retrieval in `login` function.**
Exception handling for `UninitializedOAuth` is consistent; good defensive code.

---

`113-116`: **Device token retrieval logic.**
Properly returns the JWT upon success or logs an error if OAuth is uninitialized. Implementation is coherent.

---

`124-126`: **Redirect to authentication URL with integrity checks.**
Similarly consistent uninitialized check. The flow is appropriate for a login endpoint.

---

`131-131`: **Debug logging on exceptions.**
Provides clarity for debugging.

---

`157-159`: **`token_get` method updated to call `_msecurity.security.token_get`.**
Correctly catches uninitialized exceptions, returning 500. Good usage.

---

`180-181`: **Refresh token handling improved.**
Checks `_msecurity.security.token_refresh`; consistent with other flows in the file.

---

`193-193`: **Type hint for `available_cars` return value.**
This annotation clarifies the data shape; no immediate issues.

---

`387-388`: **Handling empty commands.**
The message is explicit: no commands are sent if the list is empty. This is user-friendly feedback.

---

`442-442`: **Appending messages from dict or object form.**
Computational logic is clear, ensuring uniform Message creation.

---

`493-494`: **Error handling for checked message types.**
The strip check is a straightforward way to see if error strings exist. Looks fine.

---

`559-559`: **`_message_from_db` rename alignment.**
Correctly references `MessageDB`, maintaining consistency with the new class name.

---

`577-578`: **`_message_db_list` function returns a list of `MessageDB`.**
Implementation properly loops over `messages` to build `MessageDB` objects. The rename is consistent.



Also applies to: 583-583

---

`629-629`: **Timestamp update method references `Iterable[MessageDB]`.**
This updated signature is consistent with the renamed class and usage across the module.

</details>
<details>
<summary>server/database/database_controller.py (9)</summary>

`23-23`: **Import alias for `set_test_db_connection`.**
Renaming the import is coherent with the updated usage in testing contexts.

---

`76-76`: **Updated `from_message` to accept `MessageDB`.**
Matches the refactored class name. Implementation remains unchanged otherwise.

---

`94-94`: **List-building helper now typed for `MessageDB`.**
Retains original functionality; just updates the name reference.

---

`102-103`: **`to_message` now returns `MessageDB`.**
Matches new naming convention and usage. Preserves logic.

---

`117-117`: **Renamed `Message_DB` → `MessageDB`.**
Renaming eliminates underscore usage, more Pythonic and consistent with other classes.

---

`154-154`: **`send_messages_to_database` now expects `MessageDB` objects.**
This aligns the function with the renamed entity.

---

`167-167`: **HTTP status code for identical messages remains 400.**
This code is consistent with invalid or repeated data logic.

---

`182-182`: **Return type annotation now `list[MessageDB]`.**
Completed as part of class rename. This clarifies the data shape for calling functions.

---

`192-192`: **`statuses: list[MessageDB] = list()` inside `list_messages`.**
Consistent with the new naming; no issues.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T08:28:01Z
user: login: MarioIvancik
body: Unit tests are failing due to wrong imports in test_oauth_authentication.py
![image](https://github.com/user-attachments/assets/8e67be52-f7e0-44e7-a7d3-ede5c7735b02)
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T09:17:44Z
user: login: MarioIvancik
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T12:53:00Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>tests/authentication/test_oauth_authentication.py (1)</summary><blockquote>

`9-12`: **Imports are aligned with the new OAuth approach.**  
These newly introduced imports for `_empty_security_obj`, `init_oauth`, `deinit_oauth`, and `UninitializedOAuth` comply with the revised design. However, note that referencing `_empty_security_obj` as a global object may pose concurrency risks in certain environments.

</blockquote></details>
<details>
<summary>server/fleetv2_http_api/impl/security.py (5)</summary><blockquote>

`42-46`: **Check `is_defined` logic for concurrency.**  
The `is_defined` method checks for a class attribute `instance`, but there's no explicit assignment to `instance` in the provided code. If you rely on this method, ensure `instance` gets set appropriately. Concurrency or re-initialization scenarios could cause unpredictable behavior.

---

`73-75`: **Docstring mismatch for `is_empty`.**  
The docstring states “Check if the security object is not empty,” but the method name and return statement indicate it's detecting whether the object *is* empty. Consider updating the docstring to reflect the actual logic.

---

`85-107`: **Consistent use of `UninitializedOAuth` exception.**  
All methods now correctly raise `UninitializedOAuth` when the security object is empty, enhancing clarity. Consider removing extra spaces in the error messages (such as “not set.  Security”) for consistency.

---

`114-116`: **Initialize `_empty_security_obj` with placeholder URL.**  
Using `AnyUrl("https://empty.com")` is acceptable as a default placeholder. Consider using a more conventional domain like `https://example.com` for clarity if desired.

---

`191-191`: **Global `security` initialization.**  
Defining a global `security` object can be risky if concurrent usage or partial re-initialization occurs. Consider using dependency injection or a shared context store to increase safety and testability.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1c7d29278231ccab26606dc4c2358c3bc5514a8c and bfcabde8126d741c7466d123cbe3c090bf8286f2.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `server/fleetv2_http_api/impl/security.py` (6 hunks)
* `tests/authentication/test_oauth_authentication.py` (4 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>server/fleetv2_http_api/impl/security.py (2)</summary><blockquote>

<details>
<summary>server/fleetv2_http_api/impl/controllers.py (3) (3)</summary>

* `token_get` (135-166)
* `token_refresh` (169-189)
* `init_oauth` (64-69)

</details>
<details>
<summary>server/fleetv2_http_api/controllers/login_controller.py (2) (2)</summary>

* `token_get` (22-38)
* `token_refresh` (41-51)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>tests/authentication/test_oauth_authentication.py (5)</summary>

`16-16`: **Import statement is consistent.**  
Importing `login` from the updated module location is appropriate for the revised design.

---

`78-85`: **Tests updated to expect UninitializedOAuth.**  
These assertions correctly ensure that the new `UninitializedOAuth` exception is raised when `_empty_security_obj` is invoked. Good job aligning tests with the refactored code.

---

`123-123`: **Reset security object in setUp.**  
Calling `deinit_oauth()` ensures each test starts with a clean security state, preventing side effects between tests.

---

`134-134`: **Init OAuth with test config.**  
Invoking `init_oauth` aligns with the refactored approach for setting up the OAuth environment in tests.

---

`139-139`: **Initialize OAuth for device test.**  
This ensures that the security object is properly initialized for device login tests, maintaining consistency with the new OAuth approach.

</details>
<details>
<summary>server/fleetv2_http_api/impl/security.py (5)</summary>

`3-6`: **Imports for Protocol, Any, and urlparse.**  
Introducing `typing.Protocol` for type hinting and `urlparse` for issuer URL parsing is appropriate. This enhances type safety and code clarity.

---

`110-110`: **New `UninitializedOAuth` exception class.**  
Renaming from `OAuthAuthenticationNotSet` improves readability and aligns with the updated logic for uninitialized security objects.

---

`133-136`: **Type conversion for `_keycloak_url` and `_callback`.**  
Casting `config.keycloak_url` and `base_uri` to strings ensures compatibility with the Keycloak client. Verify if a trailing slash is needed in `base_uri` to avoid URL concatenation issues.

---

`194-198`: **Global initialization is fragile.**  
This approach might be fragile under concurrent calls or re-initialization scenarios. Consider encapsulating the security object within a manager or container if concurrency is a concern.

---

`201-203`: **Global teardown can disrupt concurrent usage.**  
Forcibly resetting the global `security` with `deinit_oauth()` can cause issues if other operations rely on it. Consider a safer teardown model or a reference-count-based approach.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T12:53:56Z

Review comments:
diff_hunk: @@ -75,8 +74,22 @@ def init_security(config: Any, base_uri: str) -> None:
 def init_security_with_client(config: Any, base_uri: str, client: _KeycloakClient) -> None:
     global _security
     _security = _SecurityObjImpl(config, base_uri, client)
+
+    assert _security.is_not_empty(), "Security object is empty after initialization."
+
     if not _security.is_not_empty():
         raise RuntimeError("Using empty security object - Keycloak authentication is not set up.")
+    else:
+        logger.debug("Security object initialized")
+
+
+def _get_security() -> _SecurityObj:
+    global _security
+    try:
+        return _security
+    except NameError:
+        _security = _empty_security_obj
+        return _security
path: server/fleetv2_http_api/impl/controllers.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Key fix: Added safe security object access method**

This new `_get_security()` function is the core of the fix for the login endpoint issue. It ensures that the `_security` variable is always properly initialized, even if module imports cause it to be reset. The function handles the case where `_security` is not defined by setting it to `_empty_security_obj`, preventing the NameError that was occurring.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-18T14:09:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,7 +46,6 @@
 _status_wait_manager = _MessageWaitObjManager()
 _cmd_wait_manager = _MessageWaitObjManager()
 _car_wait_manager = _CarWaitObjManager()
-_security: _SecurityObj = _empty_security_obj
path: server/fleetv2_http_api/impl/controllers.py
user: login: MarioIvancik
body: I think the _security object has to be created on this level for it to work.
created_at: 2025-03-18T14:38:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,7 +149,7 @@ def login(device: Optional[str] = None) -> WerkzeugResponse | Response | tuple[d
             _log_debug(str(e))
             return _log_info_and_respond(msg, 400, msg)
     try:
-        return redirect(_security.get_authentication_url())
+        return redirect(_get_security().get_authentication_url())
     except _OAuthAuthenticationNotSet as e:
         msg = (
             "Cannot get device authentication. Keycloak authentication is not set on the HTTP API."
path: server/fleetv2_http_api/impl/controllers.py
user: login: MarioIvancik
body: This is not device authentication
created_at: 2025-03-18T14:49:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,3 +186,18 @@ def _expected_issuer(self) -> str:
     def issuer_url(issuer: str) -> str:
         """Parse issuer URL from keycloak configuration."""
         return urlparse(issuer).geturl()
+
+
+security: SecurityObj = _empty_security_obj
+
+
+def init_oauth(config: Any, base_uri: str, client: KeycloakClient) -> None:
+    global security
+    security = SecurityObjImpl(config, base_uri, client)
+    if security.is_empty():
+        raise RuntimeError("Security object is empty after initialization.")
path: server/fleetv2_http_api/impl/security.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**`init_oauth` function sets a global security object.**
This is acceptable for a simple application but might be fragile under concurrent calls or unusual re-initialization scenarios. Consider encapsulating the security object within a manager or container if required.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T08:28:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,3 +186,18 @@ def _expected_issuer(self) -> str:
     def issuer_url(issuer: str) -> str:
         """Parse issuer URL from keycloak configuration."""
         return urlparse(issuer).geturl()
+
+
+security: SecurityObj = _empty_security_obj
+
+
+def init_oauth(config: Any, base_uri: str, client: KeycloakClient) -> None:
+    global security
+    security = SecurityObjImpl(config, base_uri, client)
+    if security.is_empty():
+        raise RuntimeError("Security object is empty after initialization.")
+
+
+def deinit_security() -> None:
+    global security
+    security = _empty_security_obj
path: server/fleetv2_http_api/impl/security.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**`deinit_security` forcibly resets the global.**
Again, be mindful of concurrency or partial tear-down scenarios, as this may disrupt other ongoing operations. Consider whether a safer teardown or reference-count-based approach is needed.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T08:28:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-18T14:07:28Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
The pull request modifies several files primarily to enhance logging levels, refactor security handling, and standardize naming conventions. The logging level in `config/config.json` is updated from "info" to "debug". Changes in the `server/__main__.py` file improve URL construction for Keycloak public key retrieval. The `init_security` function is renamed to `init_oauth` in the `controllers.py` file, and various references to the `Message_DB` class are updated to `MessageDB` across multiple files. Additionally, new methods and variables are introduced to streamline security management.

## Changes

| File(s)                                            | Change Summary                                                                                                                                                                                          |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `config/config.json`                               | Updated "level" of "console" logging configuration from "info" to "debug"; JSON structure reformatted for consistency.                                                                                |
| `server/__main__.py`                               | Reorganized import statements; modified URL construction in `_retrieve_keycloak_public_key` to use `rstrip("/")` on `keycloak_url`. Replaced `init_security` with `init_oauth`.                      |
| `server/fleetv2_http_api/impl/controllers.py`      | Renamed `init_security` to `init_oauth`; updated security handling to use `_msecurity` namespace; modified `_get_keycloak_openid_client` parameter type; adjusted logging and exception handling.     |
| `server/fleetv2_http_api/impl/security.py`         | Added `is_defined` method to `SecurityObj`; renamed `is_not_empty` to `is_empty`; introduced `init_oauth` and `deinit_security` functions; updated error messages.                                   |
| `server/config.py`                                 | Removed aliasing for `SecurityConfig`, simplifying its reference in `APIConfig`.                                                                                                                      |
| `server/database/database_controller.py`           | Renamed `Message_DB` to `MessageDB`; updated method signatures and return types accordingly.                                                                                                          |
| `server/fleetv2_http_api/controllers/security_controller.py` | Added global variables `_public_key` and `_client_id` for public key and client ID storage.                                                                                                           |
| `server/fleetv2_http_api/impl/message_wait.py`    | Minor formatting adjustments and logical refinements in `MessageWaitObjManager` and `MessageWaitObj` classes to enhance readability.                                                                  |
| `tests/authentication/test_oauth_authentication.py` | Updated exception handling in tests to reflect changes in security object initialization; replaced references to `empty_security_obj` and `OAuthAuthenticationNotSet` with `_empty_security_obj` and `UninitializedOAuth`. |
| `tests/database/test_database_controller.py`       | Updated references from `Message_DB` to `MessageDB` in various test cases; minor formatting adjustments.                                                                                              |

## Possibly related PRs
- **bringauto/fleet-management-http-api#20:** The changes in the main PR, which update the logging level in the `config/config.json` file, are directly related to the modifications in the retrieved PR that enhance the logging configuration structure in the same file.
- **bringauto/fleet-protocol-http-api#19:** The changes in the main PR, which involve updating the logging level in the `config/config.json` file, are directly related to the modifications in the retrieved PR that also focus on restructuring the logging configuration in the same file.

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡🧪 Multi-step agentic review comment chat (experimental)</summary>
> 
> - We're introducing multi-step agentic chat in review comments. This experimental feature enhances review discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments.
> 						- To enable this feature, set early_access to true under in the settings.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=bringauto/fleet-protocol-http-api&utm_content=24):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-03-19T12:51:40Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=bringauto_fleet-protocol-http-api&pullRequest=24) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=bringauto_fleet-protocol-http-api&pullRequest=24&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=bringauto_fleet-protocol-http-api&pullRequest=24&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=bringauto_fleet-protocol-http-api&pullRequest=24&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=bringauto_fleet-protocol-http-api&pullRequest=24&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=bringauto_fleet-protocol-http-api&pullRequest=24&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=bringauto_fleet-protocol-http-api&pullRequest=24)


author_association: NONE