This commit is contained in:
2026-02-21 16:55:10 +01:00
commit 1b8bf79d46
49 changed files with 4347 additions and 0 deletions

View File

39
backend/tests/conftest.py Normal file
View File

@@ -0,0 +1,39 @@
import pytest
from sqlalchemy.pool import StaticPool
from sqlmodel import Session, SQLModel, create_engine
from fastapi.testclient import TestClient
from app.models import EntityCache, EntityFlag, AuditLog
from app.database import set_engine, get_session
@pytest.fixture(name="engine")
def engine_fixture():
engine = create_engine(
"sqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
SQLModel.metadata.create_all(engine)
set_engine(engine)
yield engine
@pytest.fixture(name="session")
def session_fixture(engine):
with Session(engine) as session:
yield session
@pytest.fixture(name="client")
def client_fixture(engine):
from app.main import app
def override_get_session():
with Session(engine) as session:
yield session
app.dependency_overrides[get_session] = override_get_session
client = TestClient(app)
yield client
app.dependency_overrides.clear()

View File

@@ -0,0 +1,80 @@
from datetime import datetime
from sqlmodel import Session
from app.models import EntityCache, EntityFlag, AuditLog
def _seed_entity(session: Session):
session.add(EntityCache(
entity_id="light.test",
domain="light",
friendly_name="Test",
state="on",
fetched_at=datetime.utcnow(),
))
session.commit()
def test_favorite_entity(client, session):
_seed_entity(session)
resp = client.post("/api/entities/actions", json={
"action": "favorite",
"entity_ids": ["light.test"],
})
assert resp.status_code == 200
data = resp.json()
assert data["action"] == "favorite"
assert data["results"][0]["ok"] is True
# Vérifier le flag
flag = session.get(EntityFlag, "light.test")
assert flag is not None
assert flag.favorite is True
def test_unfavorite_entity(client, session):
_seed_entity(session)
# D'abord favori
client.post("/api/entities/actions", json={
"action": "favorite",
"entity_ids": ["light.test"],
})
# Puis défavori
resp = client.post("/api/entities/actions", json={
"action": "unfavorite",
"entity_ids": ["light.test"],
})
assert resp.status_code == 200
flag = session.get(EntityFlag, "light.test")
assert flag.favorite is False
def test_ignore_entity(client, session):
_seed_entity(session)
resp = client.post("/api/entities/actions", json={
"action": "ignore",
"entity_ids": ["light.test"],
})
assert resp.status_code == 200
flag = session.get(EntityFlag, "light.test")
assert flag.ignored_local is True
def test_bulk_action(client, session):
session.add(EntityCache(entity_id="light.a", domain="light", state="on", fetched_at=datetime.utcnow()))
session.add(EntityCache(entity_id="light.b", domain="light", state="off", fetched_at=datetime.utcnow()))
session.commit()
resp = client.post("/api/entities/actions", json={
"action": "favorite",
"entity_ids": ["light.a", "light.b"],
})
data = resp.json()
assert len(data["results"]) == 2
# Vérifier audit_log
from sqlmodel import select
logs = session.exec(select(AuditLog)).all()
assert len(logs) >= 1
assert logs[-1].action == "favorite"

View File

@@ -0,0 +1,119 @@
from datetime import datetime
from sqlmodel import Session
from app.models import EntityCache, EntityFlag
def _seed_entities(session: Session):
entities = [
EntityCache(
entity_id="light.salon",
domain="light",
friendly_name="Lumière Salon",
state="on",
is_available=True,
fetched_at=datetime.utcnow(),
),
EntityCache(
entity_id="sensor.temperature",
domain="sensor",
friendly_name="Température",
state="22.5",
device_class="temperature",
unit_of_measurement="°C",
is_available=True,
fetched_at=datetime.utcnow(),
),
EntityCache(
entity_id="switch.garage",
domain="switch",
friendly_name="Garage",
state="off",
is_available=True,
fetched_at=datetime.utcnow(),
),
EntityCache(
entity_id="sensor.humidity",
domain="sensor",
friendly_name="Humidité",
state="unavailable",
is_available=False,
fetched_at=datetime.utcnow(),
),
]
for e in entities:
session.add(e)
session.commit()
def test_list_entities_empty(client):
resp = client.get("/api/entities")
assert resp.status_code == 200
data = resp.json()
assert data["items"] == []
assert data["total"] == 0
def test_list_entities_with_data(client, session):
_seed_entities(session)
resp = client.get("/api/entities")
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 4
assert len(data["items"]) == 4
def test_list_entities_filter_domain(client, session):
_seed_entities(session)
resp = client.get("/api/entities?domain=sensor")
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 2
assert all(e["domain"] == "sensor" for e in data["items"])
def test_list_entities_filter_multi_domain(client, session):
_seed_entities(session)
resp = client.get("/api/entities?domain=light,switch")
data = resp.json()
assert data["total"] == 2
def test_list_entities_search(client, session):
_seed_entities(session)
resp = client.get("/api/entities?search=salon")
data = resp.json()
assert data["total"] == 1
assert data["items"][0]["entity_id"] == "light.salon"
def test_list_entities_filter_available(client, session):
_seed_entities(session)
resp = client.get("/api/entities?available=false")
data = resp.json()
assert data["total"] == 1
assert data["items"][0]["entity_id"] == "sensor.humidity"
def test_list_entities_pagination(client, session):
_seed_entities(session)
resp = client.get("/api/entities?page=1&per_page=2")
data = resp.json()
assert len(data["items"]) == 2
assert data["total"] == 4
assert data["pages"] == 2
def test_get_entity_detail(client, session):
_seed_entities(session)
resp = client.get("/api/entities/light.salon")
assert resp.status_code == 200
data = resp.json()
assert data["entity_id"] == "light.salon"
assert data["favorite"] is False
def test_get_entity_not_found(client):
resp = client.get("/api/entities/nonexistent.entity")
assert resp.status_code == 404

View File

@@ -0,0 +1,79 @@
from app.ha_client import normalize_entity
def test_normalize_entity_basic():
state = {
"entity_id": "light.salon",
"state": "on",
"attributes": {
"friendly_name": "Lumière Salon",
"device_class": "light",
},
"last_changed": "2026-01-01T00:00:00Z",
"last_updated": "2026-01-01T00:00:00Z",
}
result = normalize_entity(state)
assert result["entity_id"] == "light.salon"
assert result["domain"] == "light"
assert result["friendly_name"] == "Lumière Salon"
assert result["state"] == "on"
assert result["device_class"] == "light"
assert result["is_available"] is True
assert result["is_disabled"] is False
def test_normalize_entity_unavailable():
state = {
"entity_id": "sensor.temp",
"state": "unavailable",
"attributes": {},
}
result = normalize_entity(state)
assert result["is_available"] is False
assert result["domain"] == "sensor"
def test_normalize_entity_with_registry():
state = {
"entity_id": "switch.garage",
"state": "off",
"attributes": {"friendly_name": "Garage"},
}
registry = {
"entity_id": "switch.garage",
"area_id": "garage",
"device_id": "dev_123",
"platform": "esphome",
"disabled_by": None,
"hidden_by": "user",
}
result = normalize_entity(state, registry)
assert result["area_id"] == "garage"
assert result["device_id"] == "dev_123"
assert result["integration"] == "esphome"
assert result["is_disabled"] is False
assert result["is_hidden"] is True
def test_normalize_entity_disabled_in_registry():
state = {
"entity_id": "sensor.disabled",
"state": "unknown",
"attributes": {},
}
registry = {
"entity_id": "sensor.disabled",
"disabled_by": "user",
"hidden_by": None,
"area_id": None,
"device_id": None,
"platform": "mqtt",
}
result = normalize_entity(state, registry)
assert result["is_disabled"] is True
assert result["is_available"] is False
assert result["integration"] == "mqtt"