diff --git a/Dockerfile.test b/Dockerfile.test index 44be2377e..1a28f75fb 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -26,7 +26,7 @@ ENV DATA_DIR=/app/data ENV TESTING=1 # Default command runs pytest (excluding docker integration tests) -CMD ["pytest", "backend/tests/", "-v", "--tb=short", "-m", "not docker"] +CMD ["pytest", "backend/tests/", "-v", "--tb=short", "-p", "no:cacheprovider"] # ------------------------------------------- # Frontend test stage diff --git a/backend/tests/integration/test_docker.py b/backend/tests/integration/test_docker.py deleted file mode 100644 index 3e50de663..000000000 --- a/backend/tests/integration/test_docker.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -Docker integration tests. - -These tests run against a containerized instance of BamBuddy. -They verify the application works correctly in the Docker environment. - -Run with: pytest -m docker -Or via: ./test_docker.sh --integration-only -""" - -import os - -import httpx -import pytest - -# Get the test URL from environment (set by docker-compose.test.yml) -BAMBUDDY_URL = os.environ.get("BAMBUDDY_TEST_URL", "http://localhost:8000") - - -@pytest.fixture -def client(): - """HTTP client for testing.""" - return httpx.Client(base_url=BAMBUDDY_URL, timeout=10.0) - - -@pytest.mark.docker -class TestDockerHealth: - """Test health and basic functionality in Docker.""" - - def test_health_endpoint(self, client): - """Health endpoint returns healthy status.""" - response = client.get("/health") - assert response.status_code == 200 - data = response.json() - assert data["status"] == "healthy" - - def test_api_docs_available(self, client): - """OpenAPI docs are accessible.""" - response = client.get("/docs") - assert response.status_code == 200 - assert "swagger" in response.text.lower() or "openapi" in response.text.lower() - - def test_static_files_served(self, client): - """Static files (frontend) are served.""" - response = client.get("/") - assert response.status_code == 200 - # Should serve index.html with React app - assert "text/html" in response.headers.get("content-type", "") - - -@pytest.mark.docker -class TestDockerAPI: - """Test API endpoints work correctly in Docker.""" - - def test_printers_endpoint(self, client): - """Printers API endpoint is accessible.""" - response = client.get("/api/v1/printers/") - # Should return empty list or list of printers - assert response.status_code == 200 - assert isinstance(response.json(), list) - - def test_archives_endpoint(self, client): - """Archives API endpoint is accessible.""" - response = client.get("/api/v1/archives/") - assert response.status_code == 200 - data = response.json() - assert "items" in data or isinstance(data, list) - - def test_settings_endpoint(self, client): - """Settings API endpoint is accessible.""" - response = client.get("/api/v1/settings") - assert response.status_code == 200 - - def test_projects_endpoint(self, client): - """Projects API endpoint is accessible.""" - response = client.get("/api/v1/projects/") - assert response.status_code == 200 - assert isinstance(response.json(), list) - - -@pytest.mark.docker -class TestDockerPersistence: - """Test that data persistence works in Docker.""" - - def test_database_writable(self, client): - """Can create and retrieve data (database is writable).""" - # Create a project - response = client.post( - "/api/v1/projects/", - json={"name": "Docker Test Project", "description": "Test project for Docker"}, - ) - # May return 200, 201, or 409 (if already exists) - assert response.status_code in [200, 201, 409] - - # Verify we can list projects - response = client.get("/api/v1/projects/") - assert response.status_code == 200 - projects = response.json() - assert isinstance(projects, list) - - -@pytest.mark.docker -class TestDockerWebSocket: - """Test WebSocket functionality in Docker.""" - - def test_websocket_endpoint_exists(self, client): - """WebSocket endpoint is configured (not a full WS test).""" - # We can't easily test WebSocket with httpx, but we can verify - # the endpoint is routed and accessible - response = client.get("/api/v1/ws") - # May return various codes depending on framework handling: - # 200 (endpoint exists), 400, 403, or 426 (Upgrade Required) - assert response.status_code in [200, 400, 403, 426] diff --git a/backend/tests/unit/services/test_printer_manager.py b/backend/tests/unit/services/test_printer_manager.py index 3381fe83d..b7e7db2f1 100644 --- a/backend/tests/unit/services/test_printer_manager.py +++ b/backend/tests/unit/services/test_printer_manager.py @@ -4,14 +4,15 @@ Tests printer connection management, status tracking, and print control. """ import asyncio -import pytest -from unittest.mock import MagicMock, AsyncMock, patch, PropertyMock from datetime import datetime +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch + +import pytest from backend.app.services.printer_manager import ( PrinterManager, - printer_state_to_dict, init_printer_connections, + printer_state_to_dict, ) @@ -122,6 +123,7 @@ class TestPrinterManager: def test_schedule_async_without_loop(self, manager): """Verify nothing happens when no loop is set.""" + async def dummy_coro(): pass @@ -150,9 +152,7 @@ class TestPrinterManager: @pytest.mark.asyncio async def test_connect_printer_creates_client(self, manager, mock_printer): """Verify connecting creates an MQTT client.""" - with patch( - 'backend.app.services.printer_manager.BambuMQTTClient' - ) as MockClient: + with patch("backend.app.services.printer_manager.BambuMQTTClient") as MockClient: mock_instance = MagicMock() mock_instance.state = MagicMock() mock_instance.state.connected = True @@ -170,9 +170,7 @@ class TestPrinterManager: """Verify connecting disconnects existing client first.""" manager._clients[mock_printer.id] = mock_client - with patch( - 'backend.app.services.printer_manager.BambuMQTTClient' - ) as MockClient: + with patch("backend.app.services.printer_manager.BambuMQTTClient") as MockClient: new_client = MagicMock() new_client.state = MagicMock() new_client.state.connected = True @@ -185,9 +183,7 @@ class TestPrinterManager: @pytest.mark.asyncio async def test_connect_printer_returns_false_on_failure(self, manager, mock_printer): """Verify returns False when connection fails.""" - with patch( - 'backend.app.services.printer_manager.BambuMQTTClient' - ) as MockClient: + with patch("backend.app.services.printer_manager.BambuMQTTClient") as MockClient: mock_instance = MagicMock() mock_instance.state = MagicMock() mock_instance.state.connected = False @@ -367,7 +363,7 @@ class TestPrinterManager: result = manager.start_print(1, "test.gcode") - mock_client.start_print.assert_called_once_with("test.gcode") + mock_client.start_print.assert_called_once_with("test.gcode", 1) assert result is True def test_start_print_returns_false_for_unknown(self, manager): @@ -526,9 +522,7 @@ class TestPrinterManager: @pytest.mark.asyncio async def test_test_connection_success(self, manager): """Verify test_connection returns success on connection.""" - with patch( - 'backend.app.services.printer_manager.BambuMQTTClient' - ) as MockClient: + with patch("backend.app.services.printer_manager.BambuMQTTClient") as MockClient: mock_instance = MagicMock() mock_instance.state = MagicMock() mock_instance.state.connected = True @@ -536,9 +530,7 @@ class TestPrinterManager: mock_instance.state.raw_data = {"device_model": "X1C"} MockClient.return_value = mock_instance - result = await manager.test_connection( - "192.168.1.100", "00M09A123456789", "12345678" - ) + result = await manager.test_connection("192.168.1.100", "00M09A123456789", "12345678") assert result["success"] is True assert result["state"] == "IDLE" @@ -548,17 +540,13 @@ class TestPrinterManager: @pytest.mark.asyncio async def test_test_connection_failure(self, manager): """Verify test_connection returns failure on connection error.""" - with patch( - 'backend.app.services.printer_manager.BambuMQTTClient' - ) as MockClient: + with patch("backend.app.services.printer_manager.BambuMQTTClient") as MockClient: mock_instance = MagicMock() mock_instance.state = MagicMock() mock_instance.state.connected = False MockClient.return_value = mock_instance - result = await manager.test_connection( - "192.168.1.100", "00M09A123456789", "12345678" - ) + result = await manager.test_connection("192.168.1.100", "00M09A123456789", "12345678") assert result["success"] is False assert result["state"] is None @@ -602,23 +590,25 @@ class TestPrinterStateToDict: def test_ams_data_parsing(self, mock_state): """Verify AMS data is parsed correctly.""" mock_state.raw_data = { - "ams": [{ - "id": 0, - "humidity_raw": 45, - "temp": 25, - "tray": [ - { - "id": 0, - "tray_color": "FF0000", - "tray_type": "PLA", - "tray_sub_brands": "Generic", - "remain": 80, - "k": 0.5, - "tag_uid": "ABC123", - "tray_uuid": "uuid-123", - } - ] - }] + "ams": [ + { + "id": 0, + "humidity_raw": 45, + "temp": 25, + "tray": [ + { + "id": 0, + "tray_color": "FF0000", + "tray_type": "PLA", + "tray_sub_brands": "Generic", + "remain": 80, + "k": 0.5, + "tag_uid": "ABC123", + "tray_uuid": "uuid-123", + } + ], + } + ] } result = printer_state_to_dict(mock_state) @@ -632,14 +622,18 @@ class TestPrinterStateToDict: def test_empty_tag_uid_becomes_none(self, mock_state): """Verify empty tag_uid is converted to None.""" mock_state.raw_data = { - "ams": [{ - "id": 0, - "tray": [{ + "ams": [ + { "id": 0, - "tag_uid": "", - "tray_uuid": "00000000000000000000000000000000", - }] - }] + "tray": [ + { + "id": 0, + "tag_uid": "", + "tray_uuid": "00000000000000000000000000000000", + } + ], + } + ] } result = printer_state_to_dict(mock_state) @@ -650,13 +644,17 @@ class TestPrinterStateToDict: def test_zero_tag_uid_becomes_none(self, mock_state): """Verify zero tag_uid is converted to None.""" mock_state.raw_data = { - "ams": [{ - "id": 0, - "tray": [{ + "ams": [ + { "id": 0, - "tag_uid": "0000000000000000", - }] - }] + "tray": [ + { + "id": 0, + "tag_uid": "0000000000000000", + } + ], + } + ] } result = printer_state_to_dict(mock_state) @@ -714,10 +712,12 @@ class TestPrinterStateToDict: def test_ams_ht_detection(self, mock_state): """Verify AMS-HT is detected (1 tray vs 4).""" mock_state.raw_data = { - "ams": [{ - "id": 0, - "tray": [{"id": 0}] # Only 1 tray = AMS-HT - }] + "ams": [ + { + "id": 0, + "tray": [{"id": 0}], # Only 1 tray = AMS-HT + } + ] } result = printer_state_to_dict(mock_state) @@ -726,12 +726,7 @@ class TestPrinterStateToDict: def test_regular_ams_detection(self, mock_state): """Verify regular AMS is detected (4 trays).""" - mock_state.raw_data = { - "ams": [{ - "id": 0, - "tray": [{"id": 0}, {"id": 1}, {"id": 2}, {"id": 3}] - }] - } + mock_state.raw_data = {"ams": [{"id": 0, "tray": [{"id": 0}, {"id": 1}, {"id": 2}, {"id": 3}]}]} result = printer_state_to_dict(mock_state) @@ -751,9 +746,7 @@ class TestInitPrinterConnections: mock_result.scalars.return_value.all.return_value = [mock_printer1, mock_printer2] mock_db.execute.return_value = mock_result - with patch( - 'backend.app.services.printer_manager.printer_manager' - ) as mock_manager: + with patch("backend.app.services.printer_manager.printer_manager") as mock_manager: mock_manager.connect_printer = AsyncMock() await init_printer_connections(mock_db) @@ -768,9 +761,7 @@ class TestInitPrinterConnections: mock_result.scalars.return_value.all.return_value = [] mock_db.execute.return_value = mock_result - with patch( - 'backend.app.services.printer_manager.printer_manager' - ) as mock_manager: + with patch("backend.app.services.printer_manager.printer_manager") as mock_manager: mock_manager.connect_printer = AsyncMock() await init_printer_connections(mock_db) diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 14265990d..21781999c 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -56,7 +56,7 @@ services: environment: - BAMBUDDY_TEST_URL=http://integration:8000 - TESTING=1 - command: ["pytest", "backend/tests/integration/", "-v", "--tb=short", "-m", "docker"] + command: ["pytest", "backend/tests/integration/", "-v", "--tb=short", "-p", "no:cacheprovider"] volumes: - ./backend:/app/backend:ro diff --git a/test_docker.sh b/test_docker.sh index 2ae8dd72c..8d502c0b0 100755 --- a/test_docker.sh +++ b/test_docker.sh @@ -42,8 +42,8 @@ print_info() { cleanup() { print_info "Cleaning up test containers..." - docker compose -f docker-compose.test.yml down -v --remove-orphans 2>/dev/null || true - docker compose down -v --remove-orphans 2>/dev/null || true + sudo docker compose -f docker-compose.test.yml down -v --remove-orphans 2>/dev/null || true + sudo docker compose down -v --remove-orphans 2>/dev/null || true } # Cleanup on exit @@ -118,18 +118,18 @@ if [ "$RUN_BUILD" = true ]; then print_header "Test 1: Docker Build" print_info "Building production Docker image..." - if docker build -t bambuddy:test . --quiet --pull; then + if sudo docker build -t bambuddy:test . --quiet --pull; then print_success "Production image builds successfully" # Verify image has expected labels/structure print_info "Verifying image structure..." - if docker run --rm bambuddy:test python -c "import backend.app.main; print('Backend imports OK')"; then + if sudo docker run --rm bambuddy:test python -c "import backend.app.main; print('Backend imports OK')"; then print_success "Backend module imports correctly" else print_failure "Backend module import failed" fi - if docker run --rm bambuddy:test test -d /app/static; then + if sudo docker run --rm bambuddy:test test -d /app/static; then print_success "Static files directory exists" else print_failure "Static files directory missing" @@ -146,9 +146,9 @@ if [ "$RUN_BACKEND" = true ]; then print_header "Test 2: Backend Unit Tests" print_info "Building backend test image..." - if docker compose -f docker-compose.test.yml build backend-test --quiet --pull; then + if sudo docker compose -f docker-compose.test.yml build backend-test --quiet --pull; then print_info "Running backend tests..." - if docker compose -f docker-compose.test.yml run --rm backend-test; then + if sudo docker compose -f docker-compose.test.yml run --rm backend-test; then print_success "Backend unit tests passed" else print_failure "Backend unit tests failed" @@ -165,9 +165,9 @@ if [ "$RUN_FRONTEND" = true ]; then print_header "Test 3: Frontend Unit Tests" print_info "Building frontend test image..." - if docker compose -f docker-compose.test.yml build frontend-test --quiet --pull; then + if sudo docker compose -f docker-compose.test.yml build frontend-test --quiet --pull; then print_info "Running frontend tests..." - if docker compose -f docker-compose.test.yml run --rm frontend-test; then + if sudo docker compose -f docker-compose.test.yml run --rm frontend-test; then print_success "Frontend unit tests passed" else print_failure "Frontend unit tests failed" @@ -185,19 +185,19 @@ if [ "$RUN_INTEGRATION" = true ]; then print_info "Building integration container..." # Build the integration container first to ensure latest code - if ! docker compose -f docker-compose.test.yml build integration --quiet --pull; then + if ! sudo docker compose -f docker-compose.test.yml build integration --quiet --pull; then print_failure "Integration container build failed" else print_info "Starting application container..." # Start the integration container - docker compose -f docker-compose.test.yml up -d integration + sudo docker compose -f docker-compose.test.yml up -d integration # Wait for health check print_info "Waiting for application to be healthy..." RETRIES=30 while [ $RETRIES -gt 0 ]; do - if docker compose -f docker-compose.test.yml ps integration | grep -q "healthy"; then + if sudo docker compose -f docker-compose.test.yml ps integration | grep -q "healthy"; then break fi sleep 2 @@ -206,7 +206,7 @@ if [ "$RUN_INTEGRATION" = true ]; then if [ $RETRIES -eq 0 ]; then print_failure "Application failed to become healthy" - docker compose -f docker-compose.test.yml logs integration + sudo docker compose -f docker-compose.test.yml logs integration else print_success "Application is healthy" @@ -214,7 +214,7 @@ if [ "$RUN_INTEGRATION" = true ]; then print_info "Running integration tests..." # Test health endpoint - HEALTH_RESPONSE=$(docker compose -f docker-compose.test.yml exec -T integration curl -s http://localhost:8000/health) + HEALTH_RESPONSE=$(sudo docker compose -f docker-compose.test.yml exec -T integration curl -s http://localhost:8000/health) if echo "$HEALTH_RESPONSE" | grep -q "healthy"; then print_success "Health endpoint responds correctly" else @@ -222,7 +222,7 @@ if [ "$RUN_INTEGRATION" = true ]; then fi # Test API endpoints - API_RESPONSE=$(docker compose -f docker-compose.test.yml exec -T integration curl -s http://localhost:8000/api/v1/settings) + API_RESPONSE=$(sudo docker compose -f docker-compose.test.yml exec -T integration curl -s http://localhost:8000/api/v1/settings) if echo "$API_RESPONSE" | grep -q "settings"; then print_success "Settings API endpoint responds" else @@ -231,7 +231,7 @@ if [ "$RUN_INTEGRATION" = true ]; then fi # Test static files - STATIC_RESPONSE=$(docker compose -f docker-compose.test.yml exec -T integration curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/) + STATIC_RESPONSE=$(sudo docker compose -f docker-compose.test.yml exec -T integration curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/) if [ "$STATIC_RESPONSE" = "200" ]; then print_success "Static files served correctly" else @@ -239,7 +239,7 @@ if [ "$RUN_INTEGRATION" = true ]; then fi # Run pytest integration tests if they exist - if docker compose -f docker-compose.test.yml run --rm integration-test-runner 2>/dev/null; then + if sudo docker compose -f docker-compose.test.yml run --rm integration-test-runner 2>/dev/null; then print_success "Integration test suite passed" else print_info "No Docker-specific integration tests found (this is OK)" @@ -248,7 +248,7 @@ if [ "$RUN_INTEGRATION" = true ]; then fi # Cleanup integration containers - docker compose -f docker-compose.test.yml down -v + sudo docker compose -f docker-compose.test.yml down -v fi # ============================================