From 58881b5914fb7bbf6d5e7caccb4172e81888c3c6 Mon Sep 17 00:00:00 2001 From: Twirre Meulenbelt <43213592+TwirreM@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:47:59 +0100 Subject: [PATCH] test: add test cases ref: N25B-401 --- .../api/v1/endpoints/test_logs_endpoint.py | 68 ++++++++++++++++++- test/unit/logging/test_dated_file_handler.py | 45 ++++++++++++ test/unit/logging/test_file_handler.py | 18 ----- 3 files changed, 111 insertions(+), 20 deletions(-) create mode 100644 test/unit/logging/test_dated_file_handler.py delete mode 100644 test/unit/logging/test_file_handler.py diff --git a/test/unit/api/v1/endpoints/test_logs_endpoint.py b/test/unit/api/v1/endpoints/test_logs_endpoint.py index 50ee740..4aaa90e 100644 --- a/test/unit/api/v1/endpoints/test_logs_endpoint.py +++ b/test/unit/api/v1/endpoints/test_logs_endpoint.py @@ -1,7 +1,7 @@ -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException from fastapi.testclient import TestClient from starlette.responses import StreamingResponse @@ -61,3 +61,67 @@ async def test_log_stream_endpoint_lines(client): # Optional: assert subscribe/connect were called assert dummy_socket.subscribed # at least some log levels subscribed assert dummy_socket.connected # connect was called + + +@patch("control_backend.api.v1.endpoints.logs.LOGGING_DIR") +def test_files_endpoint(LOGGING_DIR, client): + file_1, file_2 = MagicMock(), MagicMock() + file_1.name = "file_1" + file_2.name = "file_2" + LOGGING_DIR.glob.return_value = [file_1, file_2] + result = client.get("/api/logs/files") + + assert result.status_code == 200 + assert result.json() == ["file_1", "file_2"] + + +@patch("control_backend.api.v1.endpoints.logs.FileResponse") +@patch("control_backend.api.v1.endpoints.logs.LOGGING_DIR") +def test_log_file_endpoint_success(LOGGING_DIR, MockFileResponse, client): + mock_file_path = MagicMock() + mock_file_path.is_relative_to.return_value = True + mock_file_path.is_file.return_value = True + mock_file_path.name = "test.log" + + LOGGING_DIR.__truediv__ = MagicMock(return_value=mock_file_path) + mock_file_path.resolve.return_value = mock_file_path + + MockFileResponse.return_value = MagicMock() + + result = client.get("/api/logs/files/test.log") + + assert result.status_code == 200 + MockFileResponse.assert_called_once_with(mock_file_path, filename="test.log") + + +@pytest.mark.asyncio +@patch("control_backend.api.v1.endpoints.logs.LOGGING_DIR") +async def test_log_file_endpoint_path_traversal(LOGGING_DIR): + from control_backend.api.v1.endpoints.logs import log_file + + mock_file_path = MagicMock() + mock_file_path.is_relative_to.return_value = False + + LOGGING_DIR.__truediv__ = MagicMock(return_value=mock_file_path) + mock_file_path.resolve.return_value = mock_file_path + + with pytest.raises(HTTPException) as exc_info: + await log_file("../secret.txt") + + assert exc_info.value.status_code == 400 + assert exc_info.value.detail == "Invalid filename." + + +@patch("control_backend.api.v1.endpoints.logs.LOGGING_DIR") +def test_log_file_endpoint_file_not_found(LOGGING_DIR, client): + mock_file_path = MagicMock() + mock_file_path.is_relative_to.return_value = True + mock_file_path.is_file.return_value = False + + LOGGING_DIR.__truediv__ = MagicMock(return_value=mock_file_path) + mock_file_path.resolve.return_value = mock_file_path + + result = client.get("/api/logs/files/nonexistent.log") + + assert result.status_code == 404 + assert result.json()["detail"] == "File not found." diff --git a/test/unit/logging/test_dated_file_handler.py b/test/unit/logging/test_dated_file_handler.py new file mode 100644 index 0000000..14809fb --- /dev/null +++ b/test/unit/logging/test_dated_file_handler.py @@ -0,0 +1,45 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from control_backend.logging.dated_file_handler import DatedFileHandler + + +@patch("control_backend.logging.dated_file_handler.DatedFileHandler._open") +def test_reset(open_): + stream = MagicMock() + open_.return_value = stream + + # A file should be opened when the logger is created + handler = DatedFileHandler(file_prefix="anything") + assert open_.call_count == 1 + + # Upon reset, the current file should be closed, and a new one should be opened + handler.do_rollover() + assert stream.close.call_count == 1 + assert open_.call_count == 2 + + +@patch("control_backend.logging.dated_file_handler.Path") +@patch("control_backend.logging.dated_file_handler.DatedFileHandler._open") +def test_creates_dir(open_, Path_): + stream = MagicMock() + open_.return_value = stream + + test_path = MagicMock() + test_path.parent.is_dir.return_value = False + Path_.return_value = test_path + + DatedFileHandler(file_prefix="anything") + + # The directory should've been created + test_path.parent.mkdir.assert_called_once() + + +@patch("control_backend.logging.dated_file_handler.DatedFileHandler._open") +def test_invalid_constructor(_): + with pytest.raises(ValueError): + DatedFileHandler(file_prefix=None) + + with pytest.raises(ValueError): + DatedFileHandler(file_prefix="") diff --git a/test/unit/logging/test_file_handler.py b/test/unit/logging/test_file_handler.py deleted file mode 100644 index 9d1ee90..0000000 --- a/test/unit/logging/test_file_handler.py +++ /dev/null @@ -1,18 +0,0 @@ -from unittest.mock import MagicMock, patch - -from control_backend.logging.dated_file_handler import DatedFileHandler - - -@patch("control_backend.logging.file_handler.DatedFileHandler._open") -def test_reset(open_): - stream = MagicMock() - open_.return_value = stream - - # A file should be opened when the logger is created - handler = DatedFileHandler(prefix="anything") - assert open_.call_count == 1 - - # Upon reset, the current file should be closed, and a new one should be opened - handler.do_rollover() - assert stream.close.call_count == 1 - assert open_.call_count == 2