mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
Add comprehensive automated testing infrastructure
Backend: - pytest configuration with async support and coverage - Unit tests for notification service (23 tests) - Unit tests for smart plug manager (12 tests) - Unit tests for archive service (16 tests) - Integration tests for API endpoints - Fix: notifications now send immediately (digest is summary only) Frontend: - Vitest configuration with jsdom and coverage - MSW for API mocking - Component tests for Toggle, Button, Card, ConfirmModal (77 tests) - Test utilities with custom render wrapper CI/CD: - GitHub Actions workflow for automated testing - Backend lint, unit tests, integration tests - Frontend lint, type-check, unit tests, build
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Integration tests for BamBuddy API endpoints."""
|
||||
@@ -0,0 +1,189 @@
|
||||
"""Integration tests for AMS History API endpoints."""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class TestAMSHistoryAPI:
|
||||
"""Integration tests for /api/v1/ams-history endpoints."""
|
||||
|
||||
@pytest.fixture
|
||||
async def ams_history_factory(self, db_session, printer_factory):
|
||||
"""Factory to create test AMS history records."""
|
||||
async def _create_history(printer_id=None, ams_id=0, **kwargs):
|
||||
from backend.app.models.ams_history import AMSSensorHistory
|
||||
|
||||
if printer_id is None:
|
||||
printer = await printer_factory()
|
||||
printer_id = printer.id
|
||||
|
||||
defaults = {
|
||||
"printer_id": printer_id,
|
||||
"ams_id": ams_id,
|
||||
"humidity": 45.0,
|
||||
"humidity_raw": 4500,
|
||||
"temperature": 25.0,
|
||||
"recorded_at": datetime.now(),
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
||||
history = AMSSensorHistory(**defaults)
|
||||
db_session.add(history)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(history)
|
||||
return history
|
||||
|
||||
return _create_history
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_ams_history_empty(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify empty history returns empty data array."""
|
||||
printer = await printer_factory()
|
||||
response = await async_client.get(f"/api/v1/ams-history/{printer.id}/0")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["printer_id"] == printer.id
|
||||
assert data["ams_id"] == 0
|
||||
assert data["data"] == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_ams_history_with_data(
|
||||
self, async_client: AsyncClient, ams_history_factory, db_session
|
||||
):
|
||||
"""Verify history returns recorded data."""
|
||||
# Create history records
|
||||
history = await ams_history_factory()
|
||||
printer_id = history.printer_id
|
||||
|
||||
response = await async_client.get(f"/api/v1/ams-history/{printer_id}/0")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["data"]) >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_ams_history_with_stats(
|
||||
self, async_client: AsyncClient, ams_history_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify history includes statistics."""
|
||||
printer = await printer_factory()
|
||||
# Create multiple records with different values
|
||||
await ams_history_factory(printer_id=printer.id, humidity=40.0, temperature=24.0)
|
||||
await ams_history_factory(printer_id=printer.id, humidity=50.0, temperature=26.0)
|
||||
await ams_history_factory(printer_id=printer.id, humidity=45.0, temperature=25.0)
|
||||
|
||||
response = await async_client.get(f"/api/v1/ams-history/{printer.id}/0")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Check statistics
|
||||
assert data["min_humidity"] == 40.0
|
||||
assert data["max_humidity"] == 50.0
|
||||
assert data["min_temperature"] == 24.0
|
||||
assert data["max_temperature"] == 26.0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_ams_history_with_hours_filter(
|
||||
self, async_client: AsyncClient, ams_history_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify hours parameter filters data."""
|
||||
printer = await printer_factory()
|
||||
# Create a recent record
|
||||
await ams_history_factory(
|
||||
printer_id=printer.id,
|
||||
recorded_at=datetime.now()
|
||||
)
|
||||
# Create an old record (outside default 24h)
|
||||
await ams_history_factory(
|
||||
printer_id=printer.id,
|
||||
recorded_at=datetime.now() - timedelta(hours=48)
|
||||
)
|
||||
|
||||
# Request only last 24 hours (default)
|
||||
response = await async_client.get(f"/api/v1/ams-history/{printer.id}/0")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
# Should only get the recent record
|
||||
assert len(data["data"]) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_ams_history_custom_hours(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify custom hours parameter works."""
|
||||
printer = await printer_factory()
|
||||
response = await async_client.get(
|
||||
f"/api/v1/ams-history/{printer.id}/0",
|
||||
params={"hours": 48}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["printer_id"] == printer.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_ams_history_different_ams_units(
|
||||
self, async_client: AsyncClient, ams_history_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify filtering by AMS unit ID."""
|
||||
printer = await printer_factory()
|
||||
await ams_history_factory(printer_id=printer.id, ams_id=0, humidity=40.0)
|
||||
await ams_history_factory(printer_id=printer.id, ams_id=1, humidity=50.0)
|
||||
|
||||
# Get AMS unit 0
|
||||
response = await async_client.get(f"/api/v1/ams-history/{printer.id}/0")
|
||||
assert response.status_code == 200
|
||||
data0 = response.json()
|
||||
assert len(data0["data"]) == 1
|
||||
assert data0["data"][0]["humidity"] == 40.0
|
||||
|
||||
# Get AMS unit 1
|
||||
response = await async_client.get(f"/api/v1/ams-history/{printer.id}/1")
|
||||
assert response.status_code == 200
|
||||
data1 = response.json()
|
||||
assert len(data1["data"]) == 1
|
||||
assert data1["data"][0]["humidity"] == 50.0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_old_history(
|
||||
self, async_client: AsyncClient, ams_history_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify old history can be deleted."""
|
||||
printer = await printer_factory()
|
||||
# Create an old record
|
||||
await ams_history_factory(
|
||||
printer_id=printer.id,
|
||||
recorded_at=datetime.now() - timedelta(days=60)
|
||||
)
|
||||
|
||||
# Delete records older than 30 days
|
||||
response = await async_client.delete(
|
||||
f"/api/v1/ams-history/{printer.id}",
|
||||
params={"days": 30}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["deleted"] >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_old_history_no_records(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify delete with no old records returns 0."""
|
||||
printer = await printer_factory()
|
||||
response = await async_client.delete(
|
||||
f"/api/v1/ams-history/{printer.id}",
|
||||
params={"days": 30}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["deleted"] == 0
|
||||
@@ -0,0 +1,294 @@
|
||||
"""Integration tests for Archives API endpoints.
|
||||
|
||||
Tests the full request/response cycle for /api/v1/archives/ endpoints.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class TestArchivesAPI:
|
||||
"""Integration tests for /api/v1/archives/ endpoints."""
|
||||
|
||||
# ========================================================================
|
||||
# List endpoints
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_archives_empty(self, async_client: AsyncClient):
|
||||
"""Verify empty list is returned when no archives exist."""
|
||||
response = await async_client.get("/api/v1/archives/")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_archives_with_data(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify list returns existing archives."""
|
||||
printer = await printer_factory()
|
||||
await archive_factory(printer.id, print_name="Test Archive")
|
||||
|
||||
response = await async_client.get("/api/v1/archives/")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 1
|
||||
assert any(a["print_name"] == "Test Archive" for a in data)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_archives_pagination(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify pagination works correctly."""
|
||||
printer = await printer_factory()
|
||||
# Create 5 archives
|
||||
for i in range(5):
|
||||
await archive_factory(printer.id, print_name=f"Archive {i}")
|
||||
|
||||
# Get first page with limit 2
|
||||
response = await async_client.get("/api/v1/archives/?limit=2&offset=0")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_archives_filter_by_printer(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify filtering by printer_id works."""
|
||||
printer1 = await printer_factory(name="Printer 1", serial_number="00M09A000000001")
|
||||
printer2 = await printer_factory(name="Printer 2", serial_number="00M09A000000002")
|
||||
await archive_factory(printer1.id, print_name="Printer 1 Archive")
|
||||
await archive_factory(printer2.id, print_name="Printer 2 Archive")
|
||||
|
||||
response = await async_client.get(
|
||||
f"/api/v1/archives/?printer_id={printer1.id}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert all(a["printer_id"] == printer1.id for a in data)
|
||||
|
||||
# ========================================================================
|
||||
# Get single endpoint
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_archive(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify single archive can be retrieved."""
|
||||
printer = await printer_factory()
|
||||
archive = await archive_factory(printer.id, print_name="Get Test Archive")
|
||||
|
||||
response = await async_client.get(f"/api/v1/archives/{archive.id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["id"] == archive.id
|
||||
assert result["print_name"] == "Get Test Archive"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_archive_not_found(self, async_client: AsyncClient):
|
||||
"""Verify 404 for non-existent archive."""
|
||||
response = await async_client.get("/api/v1/archives/9999")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
# ========================================================================
|
||||
# Update endpoints
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_archive_name(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify archive name can be updated."""
|
||||
printer = await printer_factory()
|
||||
archive = await archive_factory(printer.id, print_name="Original Name")
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/archives/{archive.id}",
|
||||
json={"print_name": "Updated Name"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["print_name"] == "Updated Name"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_archive_notes(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify archive notes can be updated."""
|
||||
printer = await printer_factory()
|
||||
archive = await archive_factory(printer.id)
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/archives/{archive.id}",
|
||||
json={"notes": "Great print!"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["notes"] == "Great print!"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_archive_favorite(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify archive favorite status can be updated."""
|
||||
printer = await printer_factory()
|
||||
archive = await archive_factory(printer.id)
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/archives/{archive.id}",
|
||||
json={"is_favorite": True}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["is_favorite"] is True
|
||||
|
||||
# ========================================================================
|
||||
# Delete endpoints
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_archive(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify archive can be deleted."""
|
||||
printer = await printer_factory()
|
||||
archive = await archive_factory(printer.id)
|
||||
archive_id = archive.id
|
||||
|
||||
response = await async_client.delete(f"/api/v1/archives/{archive_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# Verify deleted
|
||||
response = await async_client.get(f"/api/v1/archives/{archive_id}")
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_nonexistent_archive(self, async_client: AsyncClient):
|
||||
"""Verify deleting non-existent archive returns 404."""
|
||||
response = await async_client.delete("/api/v1/archives/9999")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
# ========================================================================
|
||||
# Statistics endpoints
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_archive_stats(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify archive statistics can be retrieved."""
|
||||
printer = await printer_factory()
|
||||
await archive_factory(
|
||||
printer.id,
|
||||
status="completed",
|
||||
print_time_seconds=3600,
|
||||
filament_used_grams=50.0,
|
||||
)
|
||||
await archive_factory(
|
||||
printer.id,
|
||||
status="completed",
|
||||
print_time_seconds=7200,
|
||||
filament_used_grams=100.0,
|
||||
)
|
||||
|
||||
response = await async_client.get("/api/v1/archives/stats")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
# Check for actual stats fields
|
||||
assert "total_prints" in result
|
||||
assert "successful_prints" in result
|
||||
|
||||
|
||||
class TestArchiveDataIntegrity:
|
||||
"""Tests for archive data integrity."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_archive_linked_to_printer(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify archive is properly linked to printer."""
|
||||
printer = await printer_factory(name="My Printer")
|
||||
archive = await archive_factory(printer.id)
|
||||
|
||||
response = await async_client.get(f"/api/v1/archives/{archive.id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["printer_id"] == printer.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_archive_stores_print_data(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
"""Verify archive stores all print data correctly."""
|
||||
printer = await printer_factory()
|
||||
archive = await archive_factory(
|
||||
printer.id,
|
||||
print_name="Test Print",
|
||||
filename="test.3mf",
|
||||
status="completed",
|
||||
filament_type="PLA",
|
||||
filament_used_grams=75.5,
|
||||
print_time_seconds=5400,
|
||||
)
|
||||
|
||||
response = await async_client.get(f"/api/v1/archives/{archive.id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["print_name"] == "Test Print"
|
||||
assert result["filename"] == "test.3mf"
|
||||
assert result["status"] == "completed"
|
||||
assert result["filament_type"] == "PLA"
|
||||
assert result["filament_used_grams"] == 75.5
|
||||
assert result["print_time_seconds"] == 5400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_archive_update_persists(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
"""CRITICAL: Verify archive updates persist."""
|
||||
printer = await printer_factory()
|
||||
archive = await archive_factory(printer.id, notes="Original notes")
|
||||
|
||||
# Update
|
||||
await async_client.patch(
|
||||
f"/api/v1/archives/{archive.id}",
|
||||
json={"notes": "Updated notes", "is_favorite": True}
|
||||
)
|
||||
|
||||
# Verify persistence
|
||||
response = await async_client.get(f"/api/v1/archives/{archive.id}")
|
||||
result = response.json()
|
||||
assert result["notes"] == "Updated notes"
|
||||
assert result["is_favorite"] is True
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Integration tests for External Links API endpoints."""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class TestExternalLinksAPI:
|
||||
"""Integration tests for /api/v1/external-links endpoints."""
|
||||
|
||||
@pytest.fixture
|
||||
async def link_factory(self, db_session):
|
||||
"""Factory to create test external links."""
|
||||
_counter = [0]
|
||||
|
||||
async def _create_link(**kwargs):
|
||||
from backend.app.models.external_link import ExternalLink
|
||||
|
||||
_counter[0] += 1
|
||||
counter = _counter[0]
|
||||
|
||||
defaults = {
|
||||
"name": f"Test Link {counter}",
|
||||
"url": f"https://example.com/{counter}",
|
||||
"icon": "Link",
|
||||
"sort_order": counter,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
||||
link = ExternalLink(**defaults)
|
||||
db_session.add(link)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(link)
|
||||
return link
|
||||
|
||||
return _create_link
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_external_links_empty(self, async_client: AsyncClient):
|
||||
"""Verify empty list when no links exist."""
|
||||
response = await async_client.get("/api/v1/external-links/")
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_external_links_with_data(
|
||||
self, async_client: AsyncClient, link_factory, db_session
|
||||
):
|
||||
"""Verify list returns existing links."""
|
||||
await link_factory(name="My Link")
|
||||
response = await async_client.get("/api/v1/external-links/")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert any(link["name"] == "My Link" for link in data)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_external_link(self, async_client: AsyncClient):
|
||||
"""Verify external link can be created."""
|
||||
data = {
|
||||
"name": "New Link",
|
||||
"url": "https://new-link.example.com",
|
||||
"icon": "ExternalLink",
|
||||
}
|
||||
response = await async_client.post("/api/v1/external-links/", json=data)
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["name"] == "New Link"
|
||||
assert result["url"] == "https://new-link.example.com"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_external_link(
|
||||
self, async_client: AsyncClient, link_factory, db_session
|
||||
):
|
||||
"""Verify single link can be retrieved."""
|
||||
link = await link_factory(name="Get Test Link")
|
||||
response = await async_client.get(f"/api/v1/external-links/{link.id}")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Get Test Link"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_external_link_not_found(self, async_client: AsyncClient):
|
||||
"""Verify 404 for non-existent link."""
|
||||
response = await async_client.get("/api/v1/external-links/9999")
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_external_link(
|
||||
self, async_client: AsyncClient, link_factory, db_session
|
||||
):
|
||||
"""Verify link can be updated."""
|
||||
link = await link_factory(name="Original")
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/external-links/{link.id}",
|
||||
json={"name": "Updated", "url": "https://updated.example.com"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["name"] == "Updated"
|
||||
assert result["url"] == "https://updated.example.com"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_external_link(
|
||||
self, async_client: AsyncClient, link_factory, db_session
|
||||
):
|
||||
"""Verify link can be deleted."""
|
||||
link = await link_factory()
|
||||
response = await async_client.delete(f"/api/v1/external-links/{link.id}")
|
||||
assert response.status_code == 200
|
||||
# Verify deleted
|
||||
response = await async_client.get(f"/api/v1/external-links/{link.id}")
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_reorder_external_links(
|
||||
self, async_client: AsyncClient, link_factory, db_session
|
||||
):
|
||||
"""Verify links can be reordered."""
|
||||
link1 = await link_factory(name="Link 1")
|
||||
link2 = await link_factory(name="Link 2")
|
||||
link3 = await link_factory(name="Link 3")
|
||||
|
||||
# Reorder: 3, 1, 2
|
||||
response = await async_client.put(
|
||||
"/api/v1/external-links/reorder",
|
||||
json={"ids": [link3.id, link1.id, link2.id]}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
# First link should be link3
|
||||
assert data[0]["id"] == link3.id
|
||||
assert data[0]["sort_order"] == 0
|
||||
|
||||
|
||||
class TestExternalLinksIconAPI:
|
||||
"""Tests for external link icon upload/delete."""
|
||||
|
||||
@pytest.fixture
|
||||
async def link_factory(self, db_session):
|
||||
"""Factory to create test external links."""
|
||||
async def _create_link(**kwargs):
|
||||
from backend.app.models.external_link import ExternalLink
|
||||
|
||||
defaults = {
|
||||
"name": "Icon Test Link",
|
||||
"url": "https://example.com",
|
||||
"icon": "Link",
|
||||
"sort_order": 0,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
||||
link = ExternalLink(**defaults)
|
||||
db_session.add(link)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(link)
|
||||
return link
|
||||
|
||||
return _create_link
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_icon_not_set(
|
||||
self, async_client: AsyncClient, link_factory, db_session
|
||||
):
|
||||
"""Verify 404 when no custom icon is set."""
|
||||
link = await link_factory()
|
||||
response = await async_client.get(f"/api/v1/external-links/{link.id}/icon")
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_icon_when_none(
|
||||
self, async_client: AsyncClient, link_factory, db_session
|
||||
):
|
||||
"""Verify deleting non-existent icon succeeds silently."""
|
||||
link = await link_factory()
|
||||
response = await async_client.delete(f"/api/v1/external-links/{link.id}/icon")
|
||||
assert response.status_code == 200
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Integration tests for Filaments API endpoints."""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class TestFilamentsAPI:
|
||||
"""Integration tests for /api/v1/filaments/ endpoints."""
|
||||
|
||||
@pytest.fixture
|
||||
async def filament_factory(self, db_session):
|
||||
"""Factory to create test filaments."""
|
||||
async def _create_filament(**kwargs):
|
||||
from backend.app.models.filament import Filament
|
||||
|
||||
defaults = {
|
||||
"name": "Test PLA",
|
||||
"type": "PLA",
|
||||
"color": "Red",
|
||||
"color_hex": "#FF0000",
|
||||
"brand": "Generic",
|
||||
"cost_per_kg": 25.0,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
||||
filament = Filament(**defaults)
|
||||
db_session.add(filament)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(filament)
|
||||
return filament
|
||||
|
||||
return _create_filament
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_filaments_empty(self, async_client: AsyncClient):
|
||||
"""Verify empty list when no filaments exist."""
|
||||
response = await async_client.get("/api/v1/filaments/")
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_filaments_with_data(
|
||||
self, async_client: AsyncClient, filament_factory, db_session
|
||||
):
|
||||
"""Verify list returns existing filaments."""
|
||||
await filament_factory(name="Test Filament")
|
||||
response = await async_client.get("/api/v1/filaments/")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert any(f["name"] == "Test Filament" for f in data)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_filament(self, async_client: AsyncClient):
|
||||
"""Verify filament can be created."""
|
||||
data = {
|
||||
"name": "New PETG",
|
||||
"type": "PETG",
|
||||
"color": "Blue",
|
||||
"color_hex": "#0000FF",
|
||||
"brand": "Bambu",
|
||||
"cost_per_kg": 30.0,
|
||||
}
|
||||
response = await async_client.post("/api/v1/filaments/", json=data)
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["name"] == "New PETG"
|
||||
assert result["type"] == "PETG"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_filament(
|
||||
self, async_client: AsyncClient, filament_factory, db_session
|
||||
):
|
||||
"""Verify single filament can be retrieved."""
|
||||
filament = await filament_factory(name="Get Test")
|
||||
response = await async_client.get(f"/api/v1/filaments/{filament.id}")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Get Test"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_filament_not_found(self, async_client: AsyncClient):
|
||||
"""Verify 404 for non-existent filament."""
|
||||
response = await async_client.get("/api/v1/filaments/9999")
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_filament(
|
||||
self, async_client: AsyncClient, filament_factory, db_session
|
||||
):
|
||||
"""Verify filament can be updated."""
|
||||
filament = await filament_factory(name="Original")
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/filaments/{filament.id}",
|
||||
json={"name": "Updated", "cost_per_kg": 35.0}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["name"] == "Updated"
|
||||
assert result["cost_per_kg"] == 35.0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_filament(
|
||||
self, async_client: AsyncClient, filament_factory, db_session
|
||||
):
|
||||
"""Verify filament can be deleted."""
|
||||
filament = await filament_factory()
|
||||
response = await async_client.delete(f"/api/v1/filaments/{filament.id}")
|
||||
assert response.status_code == 200
|
||||
# Verify deleted
|
||||
response = await async_client.get(f"/api/v1/filaments/{filament.id}")
|
||||
assert response.status_code == 404
|
||||
@@ -0,0 +1,268 @@
|
||||
"""Integration tests for Maintenance API endpoints."""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class TestMaintenanceTypesAPI:
|
||||
"""Integration tests for /api/v1/maintenance/types endpoints."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_maintenance_types(self, async_client: AsyncClient):
|
||||
"""Verify maintenance types list returns data with defaults."""
|
||||
response = await async_client.get("/api/v1/maintenance/types")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
# Should have default system types
|
||||
assert len(data) >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_includes_system_types(self, async_client: AsyncClient):
|
||||
"""Verify default system types are created."""
|
||||
response = await async_client.get("/api/v1/maintenance/types")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
names = [t["name"] for t in data]
|
||||
# Check for some default types
|
||||
assert "Lubricate Linear Rails" in names or len(data) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_custom_maintenance_type(self, async_client: AsyncClient):
|
||||
"""Verify custom maintenance type can be created."""
|
||||
data = {
|
||||
"name": "Custom Test Task",
|
||||
"description": "Test description",
|
||||
"default_interval_hours": 200.0,
|
||||
"interval_type": "hours",
|
||||
"icon": "Wrench",
|
||||
}
|
||||
response = await async_client.post("/api/v1/maintenance/types", json=data)
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["name"] == "Custom Test Task"
|
||||
assert result["is_system"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_maintenance_type(self, async_client: AsyncClient):
|
||||
"""Verify maintenance type can be updated."""
|
||||
# First create a custom type
|
||||
create_data = {
|
||||
"name": "Update Test",
|
||||
"description": "Original",
|
||||
"default_interval_hours": 100.0,
|
||||
}
|
||||
create_response = await async_client.post(
|
||||
"/api/v1/maintenance/types", json=create_data
|
||||
)
|
||||
assert create_response.status_code == 200
|
||||
type_id = create_response.json()["id"]
|
||||
|
||||
# Update it
|
||||
update_data = {"description": "Updated description"}
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/maintenance/types/{type_id}", json=update_data
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["description"] == "Updated description"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_custom_maintenance_type(self, async_client: AsyncClient):
|
||||
"""Verify custom maintenance type can be deleted."""
|
||||
# Create a custom type
|
||||
create_data = {
|
||||
"name": "Delete Test",
|
||||
"description": "To be deleted",
|
||||
"default_interval_hours": 50.0,
|
||||
}
|
||||
create_response = await async_client.post(
|
||||
"/api/v1/maintenance/types", json=create_data
|
||||
)
|
||||
type_id = create_response.json()["id"]
|
||||
|
||||
# Delete it
|
||||
response = await async_client.delete(f"/api/v1/maintenance/types/{type_id}")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestPrinterMaintenanceAPI:
|
||||
"""Integration tests for /api/v1/maintenance/printers endpoints."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_printer_maintenance_not_found(self, async_client: AsyncClient):
|
||||
"""Verify 404 for non-existent printer."""
|
||||
response = await async_client.get("/api/v1/maintenance/printers/9999")
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_printer_maintenance(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify maintenance overview for a printer."""
|
||||
printer = await printer_factory(name="Maintenance Test Printer")
|
||||
response = await async_client.get(f"/api/v1/maintenance/printers/{printer.id}")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["printer_id"] == printer.id
|
||||
assert data["printer_name"] == "Maintenance Test Printer"
|
||||
assert "maintenance_items" in data
|
||||
assert "total_print_hours" in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_all_maintenance_overview(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify overview endpoint returns all printers."""
|
||||
await printer_factory(name="Overview Printer 1")
|
||||
await printer_factory(name="Overview Printer 2")
|
||||
response = await async_client.get("/api/v1/maintenance/overview")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_maintenance_summary(self, async_client: AsyncClient):
|
||||
"""Verify summary endpoint returns counts."""
|
||||
response = await async_client.get("/api/v1/maintenance/summary")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "total_due" in data
|
||||
assert "total_warning" in data
|
||||
assert "printers_with_issues" in data
|
||||
|
||||
|
||||
class TestMaintenanceItemsAPI:
|
||||
"""Integration tests for /api/v1/maintenance/items endpoints."""
|
||||
|
||||
@pytest.fixture
|
||||
async def maintenance_item(self, async_client: AsyncClient, printer_factory, db_session):
|
||||
"""Create a maintenance item for testing."""
|
||||
printer = await printer_factory(name="Item Test Printer")
|
||||
# Get the printer's maintenance overview to create items
|
||||
response = await async_client.get(f"/api/v1/maintenance/printers/{printer.id}")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
# Return the first maintenance item
|
||||
if data["maintenance_items"]:
|
||||
return data["maintenance_items"][0]
|
||||
return None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_maintenance_item(
|
||||
self, async_client: AsyncClient, maintenance_item
|
||||
):
|
||||
"""Verify maintenance item can be updated."""
|
||||
if not maintenance_item:
|
||||
pytest.skip("No maintenance items available")
|
||||
|
||||
item_id = maintenance_item["id"]
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/maintenance/items/{item_id}",
|
||||
json={"custom_interval_hours": 150.0}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_disable_maintenance_item(
|
||||
self, async_client: AsyncClient, maintenance_item
|
||||
):
|
||||
"""Verify maintenance item can be disabled."""
|
||||
if not maintenance_item:
|
||||
pytest.skip("No maintenance items available")
|
||||
|
||||
item_id = maintenance_item["id"]
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/maintenance/items/{item_id}",
|
||||
json={"enabled": False}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["enabled"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_perform_maintenance(
|
||||
self, async_client: AsyncClient, maintenance_item
|
||||
):
|
||||
"""Verify maintenance can be marked as performed."""
|
||||
if not maintenance_item:
|
||||
pytest.skip("No maintenance items available")
|
||||
|
||||
item_id = maintenance_item["id"]
|
||||
response = await async_client.post(
|
||||
f"/api/v1/maintenance/items/{item_id}/perform",
|
||||
json={"notes": "Test maintenance performed"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["last_performed_at"] is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_maintenance_history(
|
||||
self, async_client: AsyncClient, maintenance_item
|
||||
):
|
||||
"""Verify maintenance history can be retrieved."""
|
||||
if not maintenance_item:
|
||||
pytest.skip("No maintenance items available")
|
||||
|
||||
item_id = maintenance_item["id"]
|
||||
# First perform maintenance to create history
|
||||
await async_client.post(
|
||||
f"/api/v1/maintenance/items/{item_id}/perform",
|
||||
json={"notes": "History test"}
|
||||
)
|
||||
|
||||
response = await async_client.get(f"/api/v1/maintenance/items/{item_id}/history")
|
||||
assert response.status_code == 200
|
||||
history = response.json()
|
||||
assert isinstance(history, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_maintenance_item_not_found(self, async_client: AsyncClient):
|
||||
"""Verify 404 for non-existent maintenance item."""
|
||||
response = await async_client.patch(
|
||||
"/api/v1/maintenance/items/9999",
|
||||
json={"enabled": False}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestPrinterHoursAPI:
|
||||
"""Integration tests for /api/v1/maintenance/printers/{id}/hours endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_set_printer_hours(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify printer hours can be set."""
|
||||
printer = await printer_factory(name="Hours Test Printer")
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/maintenance/printers/{printer.id}/hours",
|
||||
params={"total_hours": 500.0}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total_hours"] == 500.0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_set_printer_hours_not_found(self, async_client: AsyncClient):
|
||||
"""Verify 404 for non-existent printer."""
|
||||
response = await async_client.patch(
|
||||
"/api/v1/maintenance/printers/9999/hours",
|
||||
params={"total_hours": 100.0}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
@@ -0,0 +1,445 @@
|
||||
"""Integration tests for Notifications API endpoints.
|
||||
|
||||
Tests the full request/response cycle for /api/v1/notifications/ endpoints.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class TestNotificationsAPI:
|
||||
"""Integration tests for /api/v1/notifications/ endpoints."""
|
||||
|
||||
# ========================================================================
|
||||
# List endpoints
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_notification_providers_empty(
|
||||
self, async_client: AsyncClient
|
||||
):
|
||||
"""Verify empty list is returned when no providers exist."""
|
||||
response = await async_client.get("/api/v1/notifications/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_notification_providers_with_data(
|
||||
self, async_client: AsyncClient, notification_provider_factory, db_session
|
||||
):
|
||||
"""Verify list returns existing providers."""
|
||||
provider = await notification_provider_factory(name="Test Provider")
|
||||
|
||||
response = await async_client.get("/api/v1/notifications/")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) >= 1
|
||||
assert any(p["name"] == "Test Provider" for p in data)
|
||||
|
||||
# ========================================================================
|
||||
# Create endpoints
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_callmebot_provider(self, async_client: AsyncClient):
|
||||
"""Verify callmebot notification provider can be created."""
|
||||
data = {
|
||||
"name": "Test CallMeBot",
|
||||
"provider_type": "callmebot",
|
||||
"enabled": True,
|
||||
"config": {"phone_number": "+1234567890", "api_key": "test-api-key"},
|
||||
"on_print_start": True,
|
||||
"on_print_complete": True,
|
||||
"on_print_failed": True,
|
||||
"on_print_stopped": False,
|
||||
}
|
||||
|
||||
response = await async_client.post("/api/v1/notifications/", json=data)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["name"] == "Test CallMeBot"
|
||||
assert result["provider_type"] == "callmebot"
|
||||
assert result["on_print_start"] is True
|
||||
assert result["on_print_stopped"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_ntfy_provider(self, async_client: AsyncClient):
|
||||
"""Verify ntfy notification provider can be created."""
|
||||
data = {
|
||||
"name": "Test Ntfy",
|
||||
"provider_type": "ntfy",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"server": "https://ntfy.sh",
|
||||
"topic": "test-topic",
|
||||
},
|
||||
"on_print_complete": True,
|
||||
}
|
||||
|
||||
response = await async_client.post("/api/v1/notifications/", json=data)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["provider_type"] == "ntfy"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_provider_with_printer(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify provider can be linked to specific printer."""
|
||||
printer = await printer_factory(name="Test Printer")
|
||||
|
||||
data = {
|
||||
"name": "Printer Ntfy",
|
||||
"provider_type": "ntfy",
|
||||
"config": {"server": "https://ntfy.sh", "topic": "test-topic"},
|
||||
"printer_id": printer.id,
|
||||
}
|
||||
|
||||
response = await async_client.post("/api/v1/notifications/", json=data)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["printer_id"] == printer.id
|
||||
|
||||
# ========================================================================
|
||||
# Get single endpoint
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_notification_provider(
|
||||
self, async_client: AsyncClient, notification_provider_factory, db_session
|
||||
):
|
||||
"""Verify single provider can be retrieved."""
|
||||
provider = await notification_provider_factory(name="Get Test Provider")
|
||||
|
||||
response = await async_client.get(f"/api/v1/notifications/{provider.id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["id"] == provider.id
|
||||
assert result["name"] == "Get Test Provider"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_provider_not_found(self, async_client: AsyncClient):
|
||||
"""Verify 404 for non-existent provider."""
|
||||
response = await async_client.get("/api/v1/notifications/9999")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
# ========================================================================
|
||||
# Update endpoints (CRITICAL - toggle persistence)
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_event_toggles(
|
||||
self, async_client: AsyncClient, notification_provider_factory, db_session
|
||||
):
|
||||
"""CRITICAL: Verify notification event toggles persist correctly."""
|
||||
provider = await notification_provider_factory(
|
||||
on_print_start=True,
|
||||
on_print_complete=True,
|
||||
on_print_stopped=False,
|
||||
)
|
||||
|
||||
# Toggle on_print_stopped to True
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/notifications/{provider.id}",
|
||||
json={"on_print_stopped": True}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["on_print_stopped"] is True
|
||||
|
||||
# Verify change persisted
|
||||
response = await async_client.get(f"/api/v1/notifications/{provider.id}")
|
||||
assert response.json()["on_print_stopped"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_ams_alarm_toggles(
|
||||
self, async_client: AsyncClient, notification_provider_factory, db_session
|
||||
):
|
||||
"""CRITICAL: Verify AMS alarm toggles persist correctly."""
|
||||
provider = await notification_provider_factory(
|
||||
on_ams_humidity_high=False,
|
||||
on_ams_temperature_high=False,
|
||||
)
|
||||
|
||||
# Enable AMS alarms
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/notifications/{provider.id}",
|
||||
json={
|
||||
"on_ams_humidity_high": True,
|
||||
"on_ams_temperature_high": True,
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["on_ams_humidity_high"] is True
|
||||
assert result["on_ams_temperature_high"] is True
|
||||
|
||||
# Verify persistence
|
||||
response = await async_client.get(f"/api/v1/notifications/{provider.id}")
|
||||
result = response.json()
|
||||
assert result["on_ams_humidity_high"] is True
|
||||
assert result["on_ams_temperature_high"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_enable_disable_provider(
|
||||
self, async_client: AsyncClient, notification_provider_factory, db_session
|
||||
):
|
||||
"""Verify provider can be enabled/disabled."""
|
||||
provider = await notification_provider_factory(enabled=True)
|
||||
|
||||
# Disable
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/notifications/{provider.id}",
|
||||
json={"enabled": False}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["enabled"] is False
|
||||
|
||||
# Enable
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/notifications/{provider.id}",
|
||||
json={"enabled": True}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["enabled"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_quiet_hours(
|
||||
self, async_client: AsyncClient, notification_provider_factory, db_session
|
||||
):
|
||||
"""Verify quiet hours can be configured."""
|
||||
provider = await notification_provider_factory(quiet_hours_enabled=False)
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/notifications/{provider.id}",
|
||||
json={
|
||||
"quiet_hours_enabled": True,
|
||||
"quiet_hours_start": "22:00",
|
||||
"quiet_hours_end": "07:00",
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["quiet_hours_enabled"] is True
|
||||
assert result["quiet_hours_start"] == "22:00"
|
||||
assert result["quiet_hours_end"] == "07:00"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_daily_digest(
|
||||
self, async_client: AsyncClient, notification_provider_factory, db_session
|
||||
):
|
||||
"""Verify daily digest can be configured."""
|
||||
provider = await notification_provider_factory(daily_digest_enabled=False)
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/notifications/{provider.id}",
|
||||
json={
|
||||
"daily_digest_enabled": True,
|
||||
"daily_digest_time": "09:00",
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["daily_digest_enabled"] is True
|
||||
assert result["daily_digest_time"] == "09:00"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_multiple_event_toggles(
|
||||
self, async_client: AsyncClient, notification_provider_factory, db_session
|
||||
):
|
||||
"""Verify multiple event toggles can be updated at once."""
|
||||
provider = await notification_provider_factory(
|
||||
on_print_start=True,
|
||||
on_print_complete=True,
|
||||
on_print_failed=True,
|
||||
on_print_stopped=False,
|
||||
on_printer_offline=False,
|
||||
)
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/notifications/{provider.id}",
|
||||
json={
|
||||
"on_print_start": False,
|
||||
"on_print_stopped": True,
|
||||
"on_printer_offline": True,
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["on_print_start"] is False
|
||||
assert result["on_print_stopped"] is True
|
||||
assert result["on_printer_offline"] is True
|
||||
# Unchanged fields should remain
|
||||
assert result["on_print_complete"] is True
|
||||
assert result["on_print_failed"] is True
|
||||
|
||||
# ========================================================================
|
||||
# Test notification endpoint
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_test_notification(
|
||||
self, async_client: AsyncClient, notification_provider_factory,
|
||||
mock_httpx_client, db_session
|
||||
):
|
||||
"""Verify test notification can be sent."""
|
||||
provider = await notification_provider_factory()
|
||||
|
||||
response = await async_client.post(
|
||||
f"/api/v1/notifications/{provider.id}/test"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["success"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_test_notification_disabled_provider(
|
||||
self, async_client: AsyncClient, notification_provider_factory, db_session
|
||||
):
|
||||
"""Verify test notification works even for disabled provider."""
|
||||
provider = await notification_provider_factory(enabled=False)
|
||||
|
||||
response = await async_client.post(
|
||||
f"/api/v1/notifications/{provider.id}/test"
|
||||
)
|
||||
|
||||
# Test should still work for disabled providers
|
||||
assert response.status_code == 200
|
||||
|
||||
# ========================================================================
|
||||
# Delete endpoint
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_notification_provider(
|
||||
self, async_client: AsyncClient, notification_provider_factory, db_session
|
||||
):
|
||||
"""Verify notification provider can be deleted."""
|
||||
provider = await notification_provider_factory()
|
||||
provider_id = provider.id
|
||||
|
||||
response = await async_client.delete(f"/api/v1/notifications/{provider_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# Verify deleted
|
||||
response = await async_client.get(f"/api/v1/notifications/{provider_id}")
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_nonexistent_provider(self, async_client: AsyncClient):
|
||||
"""Verify deleting non-existent provider returns 404."""
|
||||
response = await async_client.delete("/api/v1/notifications/9999")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestNotificationTemplatesAPI:
|
||||
"""Integration tests for /api/v1/notification-templates/ endpoints."""
|
||||
|
||||
@pytest.fixture
|
||||
async def seeded_templates(self, db_session):
|
||||
"""Seed notification templates for tests."""
|
||||
from backend.app.models.notification_template import NotificationTemplate, DEFAULT_TEMPLATES
|
||||
|
||||
templates = []
|
||||
for template_data in DEFAULT_TEMPLATES:
|
||||
template = NotificationTemplate(**template_data)
|
||||
db_session.add(template)
|
||||
templates.append(template)
|
||||
await db_session.commit()
|
||||
for template in templates:
|
||||
await db_session.refresh(template)
|
||||
return templates
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_templates(self, async_client: AsyncClient, seeded_templates):
|
||||
"""Verify default templates are seeded and can be listed."""
|
||||
response = await async_client.get("/api/v1/notification-templates/")
|
||||
|
||||
assert response.status_code == 200
|
||||
templates = response.json()
|
||||
# Should have default templates seeded
|
||||
assert len(templates) >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_template_by_id(self, async_client: AsyncClient, seeded_templates):
|
||||
"""Verify template can be retrieved by ID."""
|
||||
# Get first template ID from seeded data
|
||||
template_id = seeded_templates[0].id
|
||||
|
||||
response = await async_client.get(
|
||||
f"/api/v1/notification-templates/{template_id}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
template = response.json()
|
||||
assert template["id"] == template_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_template(self, async_client: AsyncClient, seeded_templates):
|
||||
"""Verify template can be updated."""
|
||||
# Get first template
|
||||
template_id = seeded_templates[0].id
|
||||
|
||||
# Update it (route uses PUT, not PATCH)
|
||||
response = await async_client.put(
|
||||
f"/api/v1/notification-templates/{template_id}",
|
||||
json={
|
||||
"title_template": "Custom Title: {printer}",
|
||||
"body_template": "Custom body for {filename}",
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["title_template"] == "Custom Title: {printer}"
|
||||
assert result["body_template"] == "Custom body for {filename}"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_reset_template_to_default(self, async_client: AsyncClient, seeded_templates):
|
||||
"""Verify template can be reset to default."""
|
||||
template_id = seeded_templates[0].id
|
||||
|
||||
response = await async_client.post(
|
||||
f"/api/v1/notification-templates/{template_id}/reset"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["is_default"] is True
|
||||
@@ -0,0 +1,284 @@
|
||||
"""Integration tests for Printers API endpoints.
|
||||
|
||||
Tests the full request/response cycle for /api/v1/printers/ endpoints.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from unittest.mock import patch, MagicMock, AsyncMock
|
||||
|
||||
|
||||
class TestPrintersAPI:
|
||||
"""Integration tests for /api/v1/printers/ endpoints."""
|
||||
|
||||
# ========================================================================
|
||||
# List endpoints
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_printers_empty(self, async_client: AsyncClient):
|
||||
"""Verify empty list is returned when no printers exist."""
|
||||
response = await async_client.get("/api/v1/printers/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_printers_with_data(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify list returns existing printers."""
|
||||
printer = await printer_factory(name="Test Printer")
|
||||
|
||||
response = await async_client.get("/api/v1/printers/")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) >= 1
|
||||
assert any(p["name"] == "Test Printer" for p in data)
|
||||
|
||||
# ========================================================================
|
||||
# Create endpoints
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_printer(self, async_client: AsyncClient):
|
||||
"""Verify printer can be created."""
|
||||
data = {
|
||||
"name": "New Printer",
|
||||
"serial_number": "00M09A111111111",
|
||||
"ip_address": "192.168.1.100",
|
||||
"access_code": "12345678",
|
||||
"is_active": True,
|
||||
"model": "X1C",
|
||||
}
|
||||
|
||||
response = await async_client.post("/api/v1/printers/", json=data)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["name"] == "New Printer"
|
||||
assert result["serial_number"] == "00M09A111111111"
|
||||
assert result["model"] == "X1C"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_printer_duplicate_serial(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify duplicate serial number is rejected."""
|
||||
await printer_factory(serial_number="00M09A222222222")
|
||||
|
||||
data = {
|
||||
"name": "Duplicate Printer",
|
||||
"serial_number": "00M09A222222222",
|
||||
"ip_address": "192.168.1.101",
|
||||
"access_code": "12345678",
|
||||
}
|
||||
|
||||
response = await async_client.post("/api/v1/printers/", json=data)
|
||||
|
||||
# Should fail due to duplicate serial
|
||||
assert response.status_code in [400, 409, 422, 500]
|
||||
|
||||
# ========================================================================
|
||||
# Get single endpoint
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_printer(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify single printer can be retrieved."""
|
||||
printer = await printer_factory(name="Get Test Printer")
|
||||
|
||||
response = await async_client.get(f"/api/v1/printers/{printer.id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["id"] == printer.id
|
||||
assert result["name"] == "Get Test Printer"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_printer_not_found(self, async_client: AsyncClient):
|
||||
"""Verify 404 for non-existent printer."""
|
||||
response = await async_client.get("/api/v1/printers/9999")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
# ========================================================================
|
||||
# Update endpoints
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_printer_name(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify printer name can be updated."""
|
||||
printer = await printer_factory(name="Original Name")
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/printers/{printer.id}",
|
||||
json={"name": "Updated Name"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Updated Name"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_printer_active_status(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify printer active status can be updated."""
|
||||
printer = await printer_factory(is_active=True)
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/printers/{printer.id}",
|
||||
json={"is_active": False}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["is_active"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_printer_auto_archive(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify auto_archive setting can be updated."""
|
||||
printer = await printer_factory(auto_archive=True)
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/printers/{printer.id}",
|
||||
json={"auto_archive": False}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["auto_archive"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_nonexistent_printer(self, async_client: AsyncClient):
|
||||
"""Verify updating non-existent printer returns 404."""
|
||||
response = await async_client.patch(
|
||||
"/api/v1/printers/9999",
|
||||
json={"name": "New Name"}
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
# ========================================================================
|
||||
# Delete endpoints
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_printer(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify printer can be deleted."""
|
||||
printer = await printer_factory()
|
||||
printer_id = printer.id
|
||||
|
||||
response = await async_client.delete(f"/api/v1/printers/{printer_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# Verify deleted
|
||||
response = await async_client.get(f"/api/v1/printers/{printer_id}")
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_nonexistent_printer(self, async_client: AsyncClient):
|
||||
"""Verify deleting non-existent printer returns 404."""
|
||||
response = await async_client.delete("/api/v1/printers/9999")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
# ========================================================================
|
||||
# Status endpoint
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_printer_status(
|
||||
self, async_client: AsyncClient, printer_factory, mock_printer_manager, db_session
|
||||
):
|
||||
"""Verify printer status can be retrieved."""
|
||||
printer = await printer_factory()
|
||||
|
||||
response = await async_client.get(f"/api/v1/printers/{printer.id}/status")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert "connected" in result
|
||||
assert "state" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_printer_status_not_found(self, async_client: AsyncClient):
|
||||
"""Verify 404 for status of non-existent printer."""
|
||||
response = await async_client.get("/api/v1/printers/9999/status")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
# ========================================================================
|
||||
# Test connection endpoint
|
||||
# ========================================================================
|
||||
|
||||
class TestPrinterDataIntegrity:
|
||||
"""Tests for printer data integrity."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_printer_stores_all_fields(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify printer stores all fields correctly."""
|
||||
printer = await printer_factory(
|
||||
name="Full Test Printer",
|
||||
serial_number="00M09A444444444",
|
||||
ip_address="192.168.1.150",
|
||||
model="P1S",
|
||||
is_active=True,
|
||||
auto_archive=False,
|
||||
)
|
||||
|
||||
response = await async_client.get(f"/api/v1/printers/{printer.id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["name"] == "Full Test Printer"
|
||||
assert result["serial_number"] == "00M09A444444444"
|
||||
assert result["ip_address"] == "192.168.1.150"
|
||||
assert result["model"] == "P1S"
|
||||
assert result["is_active"] is True
|
||||
assert result["auto_archive"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_printer_update_persists(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""CRITICAL: Verify printer updates persist."""
|
||||
printer = await printer_factory(name="Original", is_active=True)
|
||||
|
||||
# Update
|
||||
await async_client.patch(
|
||||
f"/api/v1/printers/{printer.id}",
|
||||
json={"name": "Updated", "is_active": False}
|
||||
)
|
||||
|
||||
# Verify persistence
|
||||
response = await async_client.get(f"/api/v1/printers/{printer.id}")
|
||||
result = response.json()
|
||||
assert result["name"] == "Updated"
|
||||
assert result["is_active"] is False
|
||||
@@ -0,0 +1,160 @@
|
||||
"""Integration tests for Projects API endpoints."""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class TestProjectsAPI:
|
||||
"""Integration tests for /api/v1/projects endpoints."""
|
||||
|
||||
@pytest.fixture
|
||||
async def project_factory(self, db_session):
|
||||
"""Factory to create test projects."""
|
||||
_counter = [0]
|
||||
|
||||
async def _create_project(**kwargs):
|
||||
from backend.app.models.project import Project
|
||||
|
||||
_counter[0] += 1
|
||||
counter = _counter[0]
|
||||
|
||||
defaults = {
|
||||
"name": f"Test Project {counter}",
|
||||
"description": "Test project description",
|
||||
"color": "#FF0000",
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
||||
project = Project(**defaults)
|
||||
db_session.add(project)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(project)
|
||||
return project
|
||||
|
||||
return _create_project
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_projects_empty(self, async_client: AsyncClient):
|
||||
"""Verify empty list when no projects exist."""
|
||||
response = await async_client.get("/api/v1/projects/")
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_projects_with_data(
|
||||
self, async_client: AsyncClient, project_factory, db_session
|
||||
):
|
||||
"""Verify list returns existing projects."""
|
||||
await project_factory(name="My Project")
|
||||
response = await async_client.get("/api/v1/projects/")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert any(p["name"] == "My Project" for p in data)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_project(self, async_client: AsyncClient):
|
||||
"""Verify project can be created."""
|
||||
data = {
|
||||
"name": "New Project",
|
||||
"description": "A new project",
|
||||
"color": "#00FF00",
|
||||
}
|
||||
response = await async_client.post("/api/v1/projects/", json=data)
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["name"] == "New Project"
|
||||
assert result["color"] == "#00FF00"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_project(
|
||||
self, async_client: AsyncClient, project_factory, db_session
|
||||
):
|
||||
"""Verify single project can be retrieved."""
|
||||
project = await project_factory(name="Get Test Project")
|
||||
response = await async_client.get(f"/api/v1/projects/{project.id}")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Get Test Project"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_project_not_found(self, async_client: AsyncClient):
|
||||
"""Verify 404 for non-existent project."""
|
||||
response = await async_client.get("/api/v1/projects/9999")
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_project(
|
||||
self, async_client: AsyncClient, project_factory, db_session
|
||||
):
|
||||
"""Verify project can be updated."""
|
||||
project = await project_factory(name="Original")
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/projects/{project.id}",
|
||||
json={"name": "Updated", "description": "Updated description"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["name"] == "Updated"
|
||||
assert result["description"] == "Updated description"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_project(
|
||||
self, async_client: AsyncClient, project_factory, db_session
|
||||
):
|
||||
"""Verify project can be deleted."""
|
||||
project = await project_factory()
|
||||
response = await async_client.delete(f"/api/v1/projects/{project.id}")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["message"] == "Project deleted"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_project_not_found(self, async_client: AsyncClient):
|
||||
"""Verify 404 for deleting non-existent project."""
|
||||
response = await async_client.delete("/api/v1/projects/9999")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestProjectArchivesAPI:
|
||||
"""Tests for project-archive relationships."""
|
||||
|
||||
@pytest.fixture
|
||||
async def project_factory(self, db_session):
|
||||
"""Factory to create test projects."""
|
||||
async def _create_project(**kwargs):
|
||||
from backend.app.models.project import Project
|
||||
|
||||
defaults = {
|
||||
"name": "Archive Test Project",
|
||||
"description": "Test project",
|
||||
"color": "#0000FF",
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
||||
project = Project(**defaults)
|
||||
db_session.add(project)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(project)
|
||||
return project
|
||||
|
||||
return _create_project
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_project_with_archives(
|
||||
self, async_client: AsyncClient, project_factory, db_session
|
||||
):
|
||||
"""Verify project can be retrieved with archive count."""
|
||||
project = await project_factory()
|
||||
response = await async_client.get(f"/api/v1/projects/{project.id}")
|
||||
assert response.status_code == 200
|
||||
# Project should have an archive count (may be 0)
|
||||
data = response.json()
|
||||
assert "name" in data
|
||||
@@ -0,0 +1,215 @@
|
||||
"""Integration tests for Settings API endpoints.
|
||||
|
||||
Tests the full request/response cycle for /api/v1/settings/ endpoints.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class TestSettingsAPI:
|
||||
"""Integration tests for /api/v1/settings/ endpoints."""
|
||||
|
||||
# ========================================================================
|
||||
# Get settings
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_settings(self, async_client: AsyncClient):
|
||||
"""Verify settings can be retrieved."""
|
||||
response = await async_client.get("/api/v1/settings/")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
# Check for actual settings fields
|
||||
assert "auto_archive" in result
|
||||
assert "currency" in result
|
||||
assert "date_format" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_settings_has_defaults(self, async_client: AsyncClient):
|
||||
"""Verify default settings values are returned."""
|
||||
response = await async_client.get("/api/v1/settings/")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
# Verify some default values
|
||||
assert isinstance(result["auto_archive"], bool)
|
||||
assert isinstance(result["currency"], str)
|
||||
|
||||
# ========================================================================
|
||||
# Update settings
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_auto_archive(self, async_client: AsyncClient):
|
||||
"""Verify auto_archive can be updated."""
|
||||
# First get current value
|
||||
response = await async_client.get("/api/v1/settings/")
|
||||
original = response.json()["auto_archive"]
|
||||
|
||||
# Update to opposite value
|
||||
new_value = not original
|
||||
response = await async_client.put(
|
||||
"/api/v1/settings/",
|
||||
json={"auto_archive": new_value}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["auto_archive"] == new_value
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_currency(self, async_client: AsyncClient):
|
||||
"""Verify currency can be updated."""
|
||||
response = await async_client.put(
|
||||
"/api/v1/settings/",
|
||||
json={"currency": "EUR"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["currency"] == "EUR"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_date_format(self, async_client: AsyncClient):
|
||||
"""Verify date format can be updated."""
|
||||
response = await async_client.put(
|
||||
"/api/v1/settings/",
|
||||
json={"date_format": "eu"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["date_format"] == "eu"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_time_format(self, async_client: AsyncClient):
|
||||
"""Verify time format can be updated."""
|
||||
response = await async_client.put(
|
||||
"/api/v1/settings/",
|
||||
json={"time_format": "24h"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["time_format"] == "24h"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_filament_cost(self, async_client: AsyncClient):
|
||||
"""Verify default filament cost can be updated."""
|
||||
response = await async_client.put(
|
||||
"/api/v1/settings/",
|
||||
json={"default_filament_cost": 30.0}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["default_filament_cost"] == 30.0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_energy_cost(self, async_client: AsyncClient):
|
||||
"""Verify energy cost can be updated."""
|
||||
response = await async_client.put(
|
||||
"/api/v1/settings/",
|
||||
json={"energy_cost_per_kwh": 0.20}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["energy_cost_per_kwh"] == 0.20
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_multiple_settings(self, async_client: AsyncClient):
|
||||
"""Verify multiple settings can be updated at once."""
|
||||
response = await async_client.put(
|
||||
"/api/v1/settings/",
|
||||
json={
|
||||
"currency": "GBP",
|
||||
"date_format": "iso",
|
||||
"time_format": "12h",
|
||||
"save_thumbnails": False,
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["currency"] == "GBP"
|
||||
assert result["date_format"] == "iso"
|
||||
assert result["time_format"] == "12h"
|
||||
assert result["save_thumbnails"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_spoolman_settings(self, async_client: AsyncClient):
|
||||
"""Verify Spoolman settings can be updated."""
|
||||
response = await async_client.put(
|
||||
"/api/v1/settings/",
|
||||
json={
|
||||
"spoolman_enabled": True,
|
||||
"spoolman_url": "http://localhost:7912",
|
||||
"spoolman_sync_mode": "manual",
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["spoolman_enabled"] is True
|
||||
assert result["spoolman_url"] == "http://localhost:7912"
|
||||
assert result["spoolman_sync_mode"] == "manual"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_ams_thresholds(self, async_client: AsyncClient):
|
||||
"""Verify AMS threshold settings can be updated."""
|
||||
response = await async_client.put(
|
||||
"/api/v1/settings/",
|
||||
json={
|
||||
"ams_humidity_good": 35,
|
||||
"ams_humidity_fair": 55,
|
||||
"ams_temp_good": 25.0,
|
||||
"ams_temp_fair": 32.0,
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["ams_humidity_good"] == 35
|
||||
assert result["ams_humidity_fair"] == 55
|
||||
assert result["ams_temp_good"] == 25.0
|
||||
assert result["ams_temp_fair"] == 32.0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_notification_language(self, async_client: AsyncClient):
|
||||
"""Verify notification language can be updated."""
|
||||
response = await async_client.put(
|
||||
"/api/v1/settings/",
|
||||
json={"notification_language": "de"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["notification_language"] == "de"
|
||||
|
||||
# ========================================================================
|
||||
# Settings persistence tests
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_settings_persist_after_update(self, async_client: AsyncClient):
|
||||
"""CRITICAL: Verify settings changes persist across requests."""
|
||||
# Update settings
|
||||
await async_client.put(
|
||||
"/api/v1/settings/",
|
||||
json={"currency": "JPY", "check_updates": False}
|
||||
)
|
||||
|
||||
# Verify persistence in new request
|
||||
response = await async_client.get("/api/v1/settings/")
|
||||
result = response.json()
|
||||
assert result["currency"] == "JPY"
|
||||
assert result["check_updates"] is False
|
||||
@@ -0,0 +1,388 @@
|
||||
"""Integration tests for Smart Plugs API endpoints.
|
||||
|
||||
Tests the full request/response cycle for /api/v1/smart-plugs/ endpoints.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class TestSmartPlugsAPI:
|
||||
"""Integration tests for /api/v1/smart-plugs/ endpoints."""
|
||||
|
||||
# ========================================================================
|
||||
# List endpoints
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_smart_plugs_empty(self, async_client: AsyncClient):
|
||||
"""Verify empty list is returned when no plugs exist."""
|
||||
response = await async_client.get("/api/v1/smart-plugs/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_smart_plugs_with_data(
|
||||
self, async_client: AsyncClient, smart_plug_factory, db_session
|
||||
):
|
||||
"""Verify list returns existing plugs."""
|
||||
plug = await smart_plug_factory(name="Test Plug 1")
|
||||
|
||||
response = await async_client.get("/api/v1/smart-plugs/")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) >= 1
|
||||
assert any(p["name"] == "Test Plug 1" for p in data)
|
||||
|
||||
# ========================================================================
|
||||
# Create endpoints
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_smart_plug(self, async_client: AsyncClient):
|
||||
"""Verify smart plug can be created."""
|
||||
data = {
|
||||
"name": "New Plug",
|
||||
"ip_address": "192.168.1.100",
|
||||
"enabled": True,
|
||||
"auto_on": True,
|
||||
"auto_off": False,
|
||||
}
|
||||
|
||||
response = await async_client.post("/api/v1/smart-plugs/", json=data)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["name"] == "New Plug"
|
||||
assert result["ip_address"] == "192.168.1.100"
|
||||
assert result["auto_off"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_smart_plug_with_printer(
|
||||
self, async_client: AsyncClient, printer_factory, db_session
|
||||
):
|
||||
"""Verify smart plug can be linked to a printer."""
|
||||
printer = await printer_factory(name="Test Printer")
|
||||
|
||||
data = {
|
||||
"name": "Printer Plug",
|
||||
"ip_address": "192.168.1.101",
|
||||
"printer_id": printer.id,
|
||||
}
|
||||
|
||||
response = await async_client.post("/api/v1/smart-plugs/", json=data)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["printer_id"] == printer.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_plug_with_invalid_printer_id(
|
||||
self, async_client: AsyncClient
|
||||
):
|
||||
"""Verify creating plug with non-existent printer fails."""
|
||||
data = {
|
||||
"name": "Test Plug",
|
||||
"ip_address": "192.168.1.100",
|
||||
"printer_id": 9999,
|
||||
}
|
||||
|
||||
response = await async_client.post("/api/v1/smart-plugs/", json=data)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "Printer not found" in response.json()["detail"]
|
||||
|
||||
# ========================================================================
|
||||
# Get single endpoint
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_smart_plug(
|
||||
self, async_client: AsyncClient, smart_plug_factory, db_session
|
||||
):
|
||||
"""Verify single plug can be retrieved."""
|
||||
plug = await smart_plug_factory(name="Get Test Plug")
|
||||
|
||||
response = await async_client.get(f"/api/v1/smart-plugs/{plug.id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["id"] == plug.id
|
||||
assert result["name"] == "Get Test Plug"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_smart_plug_not_found(self, async_client: AsyncClient):
|
||||
"""Verify 404 for non-existent plug."""
|
||||
response = await async_client.get("/api/v1/smart-plugs/9999")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
# ========================================================================
|
||||
# Update endpoints (CRITICAL - toggle persistence)
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_auto_off_toggle(
|
||||
self, async_client: AsyncClient, smart_plug_factory, db_session
|
||||
):
|
||||
"""CRITICAL: Verify auto_off toggle persists correctly.
|
||||
|
||||
This tests the regression scenario where toggling auto_off
|
||||
wasn't being saved properly.
|
||||
"""
|
||||
# Create plug with auto_off=True
|
||||
plug = await smart_plug_factory(auto_off=True)
|
||||
|
||||
# Verify initial state
|
||||
response = await async_client.get(f"/api/v1/smart-plugs/{plug.id}")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["auto_off"] is True
|
||||
|
||||
# Toggle auto_off to False
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/smart-plugs/{plug.id}",
|
||||
json={"auto_off": False}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["auto_off"] is False
|
||||
|
||||
# Verify change persisted by fetching again
|
||||
response = await async_client.get(f"/api/v1/smart-plugs/{plug.id}")
|
||||
assert response.json()["auto_off"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_auto_on_toggle(
|
||||
self, async_client: AsyncClient, smart_plug_factory, db_session
|
||||
):
|
||||
"""Verify auto_on toggle persists correctly."""
|
||||
plug = await smart_plug_factory(auto_on=True)
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/smart-plugs/{plug.id}",
|
||||
json={"auto_on": False}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["auto_on"] is False
|
||||
|
||||
# Verify persistence
|
||||
response = await async_client.get(f"/api/v1/smart-plugs/{plug.id}")
|
||||
assert response.json()["auto_on"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_enabled_toggle(
|
||||
self, async_client: AsyncClient, smart_plug_factory, db_session
|
||||
):
|
||||
"""Verify enabled toggle persists correctly."""
|
||||
plug = await smart_plug_factory(enabled=True)
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/smart-plugs/{plug.id}",
|
||||
json={"enabled": False}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["enabled"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_off_delay_mode(
|
||||
self, async_client: AsyncClient, smart_plug_factory, db_session
|
||||
):
|
||||
"""Verify off_delay_mode can be changed."""
|
||||
plug = await smart_plug_factory(off_delay_mode="time")
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/smart-plugs/{plug.id}",
|
||||
json={"off_delay_mode": "temperature", "off_temp_threshold": 50}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["off_delay_mode"] == "temperature"
|
||||
assert result["off_temp_threshold"] == 50
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_schedule_settings(
|
||||
self, async_client: AsyncClient, smart_plug_factory, db_session
|
||||
):
|
||||
"""Verify schedule settings can be updated."""
|
||||
plug = await smart_plug_factory(schedule_enabled=False)
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/smart-plugs/{plug.id}",
|
||||
json={
|
||||
"schedule_enabled": True,
|
||||
"schedule_on_time": "08:00",
|
||||
"schedule_off_time": "22:00",
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["schedule_enabled"] is True
|
||||
assert result["schedule_on_time"] == "08:00"
|
||||
assert result["schedule_off_time"] == "22:00"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_multiple_fields(
|
||||
self, async_client: AsyncClient, smart_plug_factory, db_session
|
||||
):
|
||||
"""Verify multiple fields can be updated at once."""
|
||||
plug = await smart_plug_factory(
|
||||
name="Old Name",
|
||||
auto_on=True,
|
||||
auto_off=True,
|
||||
)
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/smart-plugs/{plug.id}",
|
||||
json={
|
||||
"name": "New Name",
|
||||
"auto_on": False,
|
||||
"auto_off": False,
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["name"] == "New Name"
|
||||
assert result["auto_on"] is False
|
||||
assert result["auto_off"] is False
|
||||
|
||||
# ========================================================================
|
||||
# Control endpoints
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_control_smart_plug_on(
|
||||
self, async_client: AsyncClient, smart_plug_factory, mock_tasmota_service, db_session
|
||||
):
|
||||
"""Verify smart plug can be turned on."""
|
||||
plug = await smart_plug_factory()
|
||||
|
||||
response = await async_client.post(
|
||||
f"/api/v1/smart-plugs/{plug.id}/control",
|
||||
json={"action": "on"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["success"] is True
|
||||
assert result["action"] == "on"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_control_smart_plug_off(
|
||||
self, async_client: AsyncClient, smart_plug_factory, mock_tasmota_service, db_session
|
||||
):
|
||||
"""Verify smart plug can be turned off."""
|
||||
plug = await smart_plug_factory()
|
||||
|
||||
response = await async_client.post(
|
||||
f"/api/v1/smart-plugs/{plug.id}/control",
|
||||
json={"action": "off"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["success"] is True
|
||||
assert result["action"] == "off"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_control_smart_plug_toggle(
|
||||
self, async_client: AsyncClient, smart_plug_factory, mock_tasmota_service, db_session
|
||||
):
|
||||
"""Verify smart plug can be toggled."""
|
||||
plug = await smart_plug_factory()
|
||||
|
||||
response = await async_client.post(
|
||||
f"/api/v1/smart-plugs/{plug.id}/control",
|
||||
json={"action": "toggle"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["success"] is True
|
||||
assert result["action"] == "toggle"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_control_invalid_action(
|
||||
self, async_client: AsyncClient, smart_plug_factory, db_session
|
||||
):
|
||||
"""Verify invalid action returns error."""
|
||||
plug = await smart_plug_factory()
|
||||
|
||||
response = await async_client.post(
|
||||
f"/api/v1/smart-plugs/{plug.id}/control",
|
||||
json={"action": "invalid"}
|
||||
)
|
||||
|
||||
# FastAPI returns 422 for pydantic validation errors
|
||||
assert response.status_code == 422
|
||||
|
||||
# ========================================================================
|
||||
# Status endpoint
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_smart_plug_status(
|
||||
self, async_client: AsyncClient, smart_plug_factory, mock_tasmota_service, db_session
|
||||
):
|
||||
"""Verify smart plug status can be retrieved."""
|
||||
plug = await smart_plug_factory()
|
||||
|
||||
response = await async_client.get(f"/api/v1/smart-plugs/{plug.id}/status")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["state"] == "ON"
|
||||
assert result["reachable"] is True
|
||||
|
||||
# ========================================================================
|
||||
# Delete endpoint
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_smart_plug(
|
||||
self, async_client: AsyncClient, smart_plug_factory, db_session
|
||||
):
|
||||
"""Verify smart plug can be deleted."""
|
||||
plug = await smart_plug_factory()
|
||||
plug_id = plug.id
|
||||
|
||||
response = await async_client.delete(f"/api/v1/smart-plugs/{plug_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# Verify deleted
|
||||
response = await async_client.get(f"/api/v1/smart-plugs/{plug_id}")
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_delete_nonexistent_plug(self, async_client: AsyncClient):
|
||||
"""Verify deleting non-existent plug returns 404."""
|
||||
response = await async_client.delete("/api/v1/smart-plugs/9999")
|
||||
|
||||
assert response.status_code == 404
|
||||
Reference in New Issue
Block a user