diff --git a/backend/tests/test_containers.py b/backend/tests/test_containers.py index b12033a..7a78bef 100644 --- a/backend/tests/test_containers.py +++ b/backend/tests/test_containers.py @@ -21,7 +21,7 @@ class TestContainerEndpoints: data = response.get_json() assert 'error' in data - @patch('utils.docker_client.get_docker_client') + @patch('routes.containers.list.get_docker_client') def test_get_containers_success(self, mock_get_client, client, auth_headers): """Test getting containers successfully""" # Mock Docker client @@ -44,7 +44,7 @@ class TestContainerEndpoints: assert data['containers'][0]['id'] == 'abc123' assert data['containers'][0]['name'] == 'test-container' - @patch('utils.docker_client.get_docker_client') + @patch('routes.containers.list.get_docker_client') def test_get_containers_docker_unavailable(self, mock_get_client, client, auth_headers): """Test getting containers when Docker is unavailable""" mock_get_client.return_value = None @@ -54,7 +54,7 @@ class TestContainerEndpoints: data = response.get_json() assert 'error' in data - @patch('utils.docker_client.get_docker_client') + @patch('utils.container_helpers.get_docker_client') def test_start_container_success(self, mock_get_client, client, auth_headers): """Test starting a container""" mock_container = MagicMock() @@ -68,7 +68,7 @@ class TestContainerEndpoints: assert data['success'] is True mock_container.start.assert_called_once() - @patch('utils.docker_client.get_docker_client') + @patch('utils.container_helpers.get_docker_client') def test_stop_container_success(self, mock_get_client, client, auth_headers): """Test stopping a container""" mock_container = MagicMock() @@ -82,7 +82,7 @@ class TestContainerEndpoints: assert data['success'] is True mock_container.stop.assert_called_once() - @patch('utils.docker_client.get_docker_client') + @patch('utils.container_helpers.get_docker_client') def test_restart_container_success(self, mock_get_client, client, auth_headers): """Test restarting a container""" mock_container = MagicMock() @@ -96,7 +96,7 @@ class TestContainerEndpoints: assert data['success'] is True mock_container.restart.assert_called_once() - @patch('utils.docker_client.get_docker_client') + @patch('utils.container_helpers.get_docker_client') def test_remove_container_success(self, mock_get_client, client, auth_headers): """Test removing a container""" mock_container = MagicMock() diff --git a/backend/tests/test_coverage_boost.py b/backend/tests/test_coverage_boost.py new file mode 100644 index 0000000..5fda908 --- /dev/null +++ b/backend/tests/test_coverage_boost.py @@ -0,0 +1,156 @@ +"""Tests to boost coverage to 100%.""" +import pytest +from unittest.mock import MagicMock, patch, Mock +from flask import jsonify + + +class TestContainerExceptionHandling: + """Test exception handling in container routes""" + + @patch('utils.container_helpers.get_docker_client') + def test_start_container_exception(self, mock_get_client, client, auth_headers): + """Test start container with exception""" + mock_container = MagicMock() + mock_container.start.side_effect = Exception("Container failed to start") + + mock_client = MagicMock() + mock_client.containers.get.return_value = mock_container + mock_get_client.return_value = mock_client + + response = client.post('/api/containers/test123/start', headers=auth_headers) + assert response.status_code == 500 + data = response.get_json() + assert 'error' in data + + @patch('utils.container_helpers.get_docker_client') + def test_stop_container_exception(self, mock_get_client, client, auth_headers): + """Test stop container with exception""" + mock_container = MagicMock() + mock_container.stop.side_effect = Exception("Container failed to stop") + + mock_client = MagicMock() + mock_client.containers.get.return_value = mock_container + mock_get_client.return_value = mock_client + + response = client.post('/api/containers/test123/stop', headers=auth_headers) + assert response.status_code == 500 + data = response.get_json() + assert 'error' in data + + @patch('utils.container_helpers.get_docker_client') + def test_restart_container_exception(self, mock_get_client, client, auth_headers): + """Test restart container with exception""" + mock_container = MagicMock() + mock_container.restart.side_effect = Exception("Container failed to restart") + + mock_client = MagicMock() + mock_client.containers.get.return_value = mock_container + mock_get_client.return_value = mock_client + + response = client.post('/api/containers/test123/restart', headers=auth_headers) + assert response.status_code == 500 + data = response.get_json() + assert 'error' in data + + @patch('utils.container_helpers.get_docker_client') + def test_remove_container_exception(self, mock_get_client, client, auth_headers): + """Test remove container with exception""" + mock_container = MagicMock() + mock_container.remove.side_effect = Exception("Container failed to remove") + + mock_client = MagicMock() + mock_client.containers.get.return_value = mock_container + mock_get_client.return_value = mock_client + + response = client.delete('/api/containers/test123', headers=auth_headers) + assert response.status_code == 500 + data = response.get_json() + assert 'error' in data + + @patch('routes.containers.list.get_docker_client') + def test_list_containers_exception(self, mock_get_client, client, auth_headers): + """Test list containers with exception""" + mock_client = MagicMock() + mock_client.containers.list.side_effect = Exception("Failed to list containers") + mock_get_client.return_value = mock_client + + response = client.get('/api/containers', headers=auth_headers) + assert response.status_code == 500 + data = response.get_json() + assert 'error' in data + + +class TestContainerHelpers: + """Test container_helpers exception handling""" + + @patch('utils.container_helpers.get_docker_client') + def test_get_auth_and_container_exception(self, mock_get_client): + """Test get_auth_and_container when container.get raises exception""" + from utils.container_helpers import get_auth_and_container + from config import sessions + + # Create a valid session + token = 'test_token_123' + sessions[token] = {'username': 'test'} + + # Mock client that raises exception + mock_client = MagicMock() + mock_client.containers.get.side_effect = Exception("Container not found") + mock_get_client.return_value = mock_client + + # This test needs to be called in request context + from flask import Flask + app = Flask(__name__) + + with app.test_request_context(headers={'Authorization': f'Bearer {token}'}): + container, error = get_auth_and_container('test123') + assert container is None + assert error is not None + assert error[1] == 500 + + +class TestExecHelpers: + """Test exec_helpers edge cases""" + + def test_decode_output_unicode_error(self): + """Test decode_output with invalid UTF-8""" + from utils.exec_helpers import decode_output + + mock_exec = MagicMock() + # Invalid UTF-8 sequence + mock_exec.output = b'\x80\x81\x82\x83' + + result = decode_output(mock_exec) + # Should fallback to latin-1 + assert result is not None + assert isinstance(result, str) + + def test_extract_workdir_no_marker(self): + """Test extract_workdir when no marker present""" + from utils.exec_helpers import extract_workdir + + output = "some command output" + current_workdir = "/test" + result_output, result_workdir = extract_workdir(output, current_workdir, False) + + assert result_output == output + assert result_workdir == current_workdir + + def test_execute_command_bash_fallback(self): + """Test execute_command_with_fallback when bash fails""" + from utils.exec_helpers import execute_command_with_fallback + + mock_container = MagicMock() + # Make bash fail, sh succeed + mock_container.exec_run.side_effect = [ + Exception("bash not found"), + MagicMock(output=b'success', exit_code=0) + ] + + result = execute_command_with_fallback( + mock_container, '/app', 'ls', False + ) + assert result.exit_code == 0 + assert mock_container.exec_run.call_count == 2 + + diff --git a/backend/tests/test_exec.py b/backend/tests/test_exec.py index 8ee6b78..3ba6506 100644 --- a/backend/tests/test_exec.py +++ b/backend/tests/test_exec.py @@ -12,7 +12,7 @@ class TestContainerExec: }) assert response.status_code == 401 - @patch('utils.docker_client.get_docker_client') + @patch('routes.containers.exec.get_docker_client') def test_exec_simple_command(self, mock_get_client, client, auth_headers, auth_token): """Test executing a simple command""" # Mock exec result @@ -37,7 +37,7 @@ class TestContainerExec: assert 'file1.txt' in data['output'] assert data['workdir'] == '/app' - @patch('utils.docker_client.get_docker_client') + @patch('routes.containers.exec.get_docker_client') def test_exec_cd_command(self, mock_get_client, client, auth_headers, auth_token): """Test executing cd command""" # Mock exec result for cd command @@ -62,7 +62,7 @@ class TestContainerExec: assert data['workdir'] == '/home/user' assert data['output'] == '' - @patch('utils.docker_client.get_docker_client') + @patch('routes.containers.exec.get_docker_client') def test_exec_command_with_error(self, mock_get_client, client, auth_headers, auth_token): """Test executing a command that fails""" # Mock exec result with error @@ -86,7 +86,7 @@ class TestContainerExec: assert data['exit_code'] == 127 assert 'command not found' in data['output'] - @patch('utils.docker_client.get_docker_client') + @patch('routes.containers.exec.get_docker_client') def test_exec_docker_unavailable(self, mock_get_client, client, auth_headers): """Test exec when Docker is unavailable""" mock_get_client.return_value = None @@ -99,7 +99,7 @@ class TestContainerExec: data = response.get_json() assert 'error' in data - @patch('utils.docker_client.get_docker_client') + @patch('routes.containers.exec.get_docker_client') def test_exec_unicode_handling(self, mock_get_client, client, auth_headers, auth_token): """Test exec with unicode output""" # Mock exec result with unicode diff --git a/backend/tests/test_exec_advanced.py b/backend/tests/test_exec_advanced.py index c6b72be..84f1053 100644 --- a/backend/tests/test_exec_advanced.py +++ b/backend/tests/test_exec_advanced.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch class TestExecAdvanced: """Advanced tests for command execution""" - @patch('utils.docker_client.get_docker_client') + @patch('routes.containers.exec.get_docker_client') def test_exec_bash_fallback_to_sh(self, mock_get_client, client, auth_headers, auth_token): """Test fallback from bash to sh when bash doesn't exist""" # Mock exec that fails for bash but succeeds for sh @@ -33,7 +33,7 @@ class TestExecAdvanced: data = response.get_json() assert data['exit_code'] == 0 - @patch('utils.docker_client.get_docker_client') + @patch('routes.containers.exec.get_docker_client') def test_exec_container_not_found(self, mock_get_client, client, auth_headers): """Test exec on non-existent container""" mock_client = MagicMock() @@ -48,7 +48,7 @@ class TestExecAdvanced: data = response.get_json() assert 'error' in data - @patch('utils.docker_client.get_docker_client') + @patch('routes.containers.exec.get_docker_client') def test_exec_preserves_working_directory(self, mock_get_client, client, auth_headers, auth_token): """Test that working directory is preserved across commands""" mock_exec_result = MagicMock() @@ -76,7 +76,7 @@ class TestExecAdvanced: json={'command': 'ls'}) assert response2.status_code == 200 - @patch('utils.docker_client.get_docker_client') + @patch('routes.containers.exec.get_docker_client') def test_exec_cd_with_tilde(self, mock_get_client, client, auth_headers, auth_token): """Test cd command with tilde expansion""" mock_exec_result = MagicMock() @@ -98,7 +98,7 @@ class TestExecAdvanced: data = response.get_json() assert data['workdir'] == '/home/user' - @patch('utils.docker_client.get_docker_client') + @patch('routes.containers.exec.get_docker_client') def test_exec_cd_no_args(self, mock_get_client, client, auth_headers, auth_token): """Test cd command without arguments (should go to home)""" mock_exec_result = MagicMock() @@ -122,7 +122,7 @@ class TestExecAdvanced: # workdir should be extracted from ::WORKDIR:: marker assert data['workdir'] == '/' - @patch('utils.docker_client.get_docker_client') + @patch('routes.containers.exec.get_docker_client') def test_exec_latin1_encoding_fallback(self, mock_get_client, client, auth_headers, auth_token): """Test fallback to latin-1 encoding for non-UTF-8 output""" # Create binary data that's not valid UTF-8 @@ -149,7 +149,7 @@ class TestExecAdvanced: assert data['exit_code'] == 0 assert 'output' in data - @patch('utils.docker_client.get_docker_client') + @patch('routes.containers.exec.get_docker_client') def test_exec_empty_command(self, mock_get_client, client, auth_headers, auth_token): """Test exec with empty/no command""" mock_exec_result = MagicMock() diff --git a/backend/tests/test_websocket_coverage.py b/backend/tests/test_websocket_coverage.py index 4e80ec4..94272b4 100644 --- a/backend/tests/test_websocket_coverage.py +++ b/backend/tests/test_websocket_coverage.py @@ -21,7 +21,7 @@ class TestWebSocketCoverage: from app import socketio return socketio.test_client(app, namespace='/terminal') - @patch('utils.docker_client.get_docker_client') + @patch('handlers.terminal.start.get_docker_client') def test_start_terminal_success_flow(self, mock_get_client, socketio_client, auth_token): """Test successful terminal start with mocked Docker""" # Create mock Docker client and container @@ -77,7 +77,7 @@ class TestWebSocketCoverage: assert call_args[1]['environment']['COLUMNS'] == '100' assert call_args[1]['environment']['LINES'] == '30' - @patch('utils.docker_client.get_docker_client') + @patch('handlers.terminal.start.get_docker_client') def test_start_terminal_creates_thread(self, mock_get_client, socketio_client, auth_token): """Test that starting terminal creates output thread""" mock_client = MagicMock() @@ -130,7 +130,7 @@ class TestWebSocketCoverage: decoded = invalid_utf8.decode('latin-1', errors='replace') assert decoded is not None # Should not crash - @patch('utils.docker_client.get_docker_client') + @patch('handlers.terminal.start.get_docker_client') def test_start_terminal_latin1_fallback(self, mock_get_client, socketio_client, auth_token): """Test latin-1 fallback for invalid UTF-8""" mock_client = MagicMock() @@ -166,7 +166,7 @@ class TestWebSocketCoverage: decoding_errors = [msg for msg in error_msgs if 'decode' in str(msg).lower()] assert len(decoding_errors) == 0 - @patch('utils.docker_client.get_docker_client') + @patch('handlers.terminal.start.get_docker_client') def test_start_terminal_container_not_found(self, mock_get_client, socketio_client, auth_token): """Test error when container doesn't exist""" mock_client = MagicMock() @@ -186,7 +186,7 @@ class TestWebSocketCoverage: assert len(error_msgs) > 0, "Should receive error message" assert 'not found' in error_msgs[0]['args'][0]['error'].lower() - @patch('utils.docker_client.get_docker_client') + @patch('handlers.terminal.start.get_docker_client') def test_start_terminal_exec_error(self, mock_get_client, socketio_client, auth_token): """Test error during exec_run""" mock_client = MagicMock() @@ -207,7 +207,7 @@ class TestWebSocketCoverage: assert len(error_msgs) > 0, "Should receive error message" - @patch('utils.docker_client.get_docker_client') + @patch('handlers.terminal.start.get_docker_client') def test_handle_input_error_handling(self, mock_get_client, socketio_client, auth_token): """Test error handling in handle_input when sendall fails""" import app @@ -250,7 +250,7 @@ class TestWebSocketCoverage: # Should receive error about socket problem assert len(error_msgs) > 0, "Should receive error from failed sendall" - @patch('utils.docker_client.get_docker_client') + @patch('handlers.terminal.start.get_docker_client') def test_disconnect_cleanup(self, mock_get_client, socketio_client, auth_token): """Test that disconnect properly cleans up active terminals""" import app @@ -313,7 +313,7 @@ class TestWebSocketCoverage: error_msgs = [msg for msg in received if msg['name'] == 'error'] assert len(error_msgs) == 0, "Resize should not error" - @patch('utils.docker_client.get_docker_client') + @patch('handlers.terminal.start.get_docker_client') def test_socket_close_on_exit(self, mock_get_client, socketio_client, auth_token): """Test that socket is closed when thread exits""" mock_client = MagicMock() @@ -343,7 +343,7 @@ class TestWebSocketCoverage: # but the code path is exercised assert True - @patch('utils.docker_client.get_docker_client') + @patch('handlers.terminal.start.get_docker_client') def test_default_terminal_size(self, mock_get_client, socketio_client, auth_token): """Test default terminal size when not specified""" mock_client = MagicMock() @@ -373,7 +373,7 @@ class TestWebSocketCoverage: assert call_args[1]['environment']['COLUMNS'] == '80' assert call_args[1]['environment']['LINES'] == '24' - @patch('utils.docker_client.get_docker_client') + @patch('handlers.terminal.start.get_docker_client') def test_input_with_direct_socket_fallback(self, mock_get_client, socketio_client, auth_token): """Test that input works with direct socket (no _sock attribute)""" import app