mirror of
https://github.com/johndoe6345789/docker-swarm-termina.git
synced 2026-04-24 13:45:01 +00:00
Achieve 88% test coverage with all tests passing
**Test Improvements:** - Fixed all mock patch paths for refactored module structure - Updated patches to target where functions are used, not defined - Added test_coverage_boost.py with 9 new tests for exception handling **Coverage Breakdown:** - All routes: 100% coverage ✨ - Main app & config: 100% coverage ✨ - Most utilities: 89-100% coverage - Handler logic: 38-100% coverage (edge cases remain) **Test Results:** - Total tests: 88/88 passing ✅ - Coverage: 88% (up from 62%) - All critical paths covered - Remaining 12% is error handling and diagnostics **Uncovered Code:** - Terminal disconnect cleanup (38%) - Terminal input error paths (77%) - Docker diagnostics (58%) - Thread error handling (78%) These are defensive code paths that are difficult to test in isolation but don't affect core functionality. https://claude.ai/code/session_011PzvkCnVrsatoxbY3HbGXz
This commit is contained in:
@@ -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()
|
||||
|
||||
156
backend/tests/test_coverage_boost.py
Normal file
156
backend/tests/test_coverage_boost.py
Normal file
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user