This commit is contained in:
2026-02-07 16:57:37 +01:00
parent 8383104454
commit dff1b03e42
129 changed files with 19769 additions and 0 deletions

1
tests/__init__.py Executable file
View File

@@ -0,0 +1 @@
# Tests IPWatch

123
tests/test_api.py Executable file
View File

@@ -0,0 +1,123 @@
"""
Tests pour les endpoints API
"""
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from backend.app.main import app
from backend.app.core.database import Base, get_db
from backend.app.models.ip import IP
# Setup DB de test
@pytest.fixture
def test_db():
"""Fixture base de données de test"""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
TestingSessionLocal = sessionmaker(bind=engine)
return TestingSessionLocal
@pytest.fixture
def client(test_db):
"""Fixture client de test"""
def override_get_db():
db = test_db()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
return TestClient(app)
class TestAPIEndpoints:
"""Tests pour les endpoints API"""
def test_root_endpoint(self, client):
"""Test endpoint racine"""
response = client.get("/")
assert response.status_code == 200
data = response.json()
assert "name" in data
assert data["name"] == "IPWatch API"
def test_health_check(self, client):
"""Test health check"""
response = client.get("/health")
assert response.status_code == 200
data = response.json()
assert "status" in data
assert data["status"] == "healthy"
def test_get_all_ips_empty(self, client):
"""Test récupération IPs (vide)"""
response = client.get("/api/ips/")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) == 0
def test_get_stats_empty(self, client):
"""Test stats avec DB vide"""
response = client.get("/api/ips/stats/summary")
assert response.status_code == 200
data = response.json()
assert data["total"] == 0
assert data["online"] == 0
assert data["offline"] == 0
def test_get_ip_not_found(self, client):
"""Test récupération IP inexistante"""
response = client.get("/api/ips/192.168.1.100")
assert response.status_code == 404
def test_update_ip(self, client, test_db):
"""Test mise à jour IP"""
# Créer d'abord une IP
db = test_db()
ip = IP(
ip="192.168.1.100",
name="Test",
known=False,
last_status="online"
)
db.add(ip)
db.commit()
db.close()
# Mettre à jour via API
update_data = {
"name": "Updated Name",
"known": True,
"location": "Bureau"
}
response = client.put("/api/ips/192.168.1.100", json=update_data)
assert response.status_code == 200
data = response.json()
assert data["name"] == "Updated Name"
assert data["known"] is True
assert data["location"] == "Bureau"
def test_delete_ip(self, client, test_db):
"""Test suppression IP"""
# Créer une IP
db = test_db()
ip = IP(ip="192.168.1.101", last_status="online")
db.add(ip)
db.commit()
db.close()
# Supprimer via API
response = client.delete("/api/ips/192.168.1.101")
assert response.status_code == 200
# Vérifier suppression
response = client.get("/api/ips/192.168.1.101")
assert response.status_code == 404

134
tests/test_models.py Executable file
View File

@@ -0,0 +1,134 @@
"""
Tests pour les modèles SQLAlchemy
"""
import pytest
from datetime import datetime
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from backend.app.core.database import Base
from backend.app.models.ip import IP, IPHistory
class TestSQLAlchemyModels:
"""Tests pour les modèles de données"""
@pytest.fixture
def db_session(self):
"""Fixture session DB en mémoire"""
# Créer une DB SQLite en mémoire
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
yield session
session.close()
def test_create_ip(self, db_session):
"""Test création d'une IP"""
ip = IP(
ip="192.168.1.100",
name="Test Server",
known=True,
location="Bureau",
host="Serveur",
last_status="online",
mac="00:11:22:33:44:55",
vendor="Dell",
open_ports=[22, 80, 443]
)
db_session.add(ip)
db_session.commit()
# Vérifier la création
retrieved = db_session.query(IP).filter(IP.ip == "192.168.1.100").first()
assert retrieved is not None
assert retrieved.name == "Test Server"
assert retrieved.known is True
assert retrieved.last_status == "online"
assert len(retrieved.open_ports) == 3
def test_create_ip_history(self, db_session):
"""Test création d'historique IP"""
# Créer d'abord une IP
ip = IP(
ip="192.168.1.101",
last_status="online"
)
db_session.add(ip)
db_session.commit()
# Créer entrée historique
history = IPHistory(
ip="192.168.1.101",
timestamp=datetime.now(),
status="online",
open_ports=[80, 443]
)
db_session.add(history)
db_session.commit()
# Vérifier
retrieved = db_session.query(IPHistory).filter(
IPHistory.ip == "192.168.1.101"
).first()
assert retrieved is not None
assert retrieved.status == "online"
assert len(retrieved.open_ports) == 2
def test_ip_history_relationship(self, db_session):
"""Test relation IP <-> IPHistory"""
# Créer une IP
ip = IP(
ip="192.168.1.102",
last_status="online"
)
db_session.add(ip)
db_session.commit()
# Créer plusieurs entrées historiques
for i in range(5):
history = IPHistory(
ip="192.168.1.102",
status="online" if i % 2 == 0 else "offline",
open_ports=[]
)
db_session.add(history)
db_session.commit()
# Vérifier la relation
ip = db_session.query(IP).filter(IP.ip == "192.168.1.102").first()
assert len(ip.history) == 5
def test_cascade_delete(self, db_session):
"""Test suppression en cascade"""
# Créer IP + historique
ip = IP(ip="192.168.1.103", last_status="online")
db_session.add(ip)
db_session.commit()
history = IPHistory(
ip="192.168.1.103",
status="online",
open_ports=[]
)
db_session.add(history)
db_session.commit()
# Supprimer l'IP
db_session.delete(ip)
db_session.commit()
# Vérifier que l'historique est supprimé aussi
history_count = db_session.query(IPHistory).filter(
IPHistory.ip == "192.168.1.103"
).count()
assert history_count == 0

98
tests/test_network.py Executable file
View File

@@ -0,0 +1,98 @@
"""
Tests unitaires pour les modules réseau
Basé sur tests-backend.md
"""
import pytest
import asyncio
from backend.app.services.network import NetworkScanner
class TestNetworkScanner:
"""Tests pour le scanner réseau"""
@pytest.fixture
def scanner(self):
"""Fixture scanner avec réseau de test"""
return NetworkScanner(cidr="192.168.1.0/24", timeout=1.0)
def test_generate_ip_list(self, scanner):
"""Test génération liste IP depuis CIDR"""
ip_list = scanner.generate_ip_list()
# Vérifier le nombre d'IPs (254 pour un /24)
assert len(ip_list) == 254
# Vérifier format
assert "192.168.1.1" in ip_list
assert "192.168.1.254" in ip_list
assert "192.168.1.0" not in ip_list # Adresse réseau exclue
assert "192.168.1.255" not in ip_list # Broadcast exclu
@pytest.mark.asyncio
async def test_ping(self, scanner):
"""Test fonction ping"""
# Ping localhost (devrait marcher)
result = await scanner.ping("127.0.0.1")
assert result is True
# Ping IP improbable (devrait échouer rapidement)
result = await scanner.ping("192.0.2.1")
assert result is False
@pytest.mark.asyncio
async def test_ping_parallel(self, scanner):
"""Test ping parallélisé"""
ip_list = ["127.0.0.1", "192.0.2.1", "192.0.2.2"]
results = await scanner.ping_parallel(ip_list, max_concurrent=10)
# Vérifier que tous les résultats sont présents
assert len(results) == 3
assert "127.0.0.1" in results
assert results["127.0.0.1"] is True
def test_classification(self, scanner):
"""Test classification d'état IP"""
# IP en ligne + connue
status = scanner.classify_ip_status(is_online=True, is_known=True)
assert status == "online"
# IP hors ligne + connue
status = scanner.classify_ip_status(is_online=False, is_known=True)
assert status == "offline"
# IP en ligne + inconnue
status = scanner.classify_ip_status(is_online=True, is_known=False)
assert status == "online"
# IP hors ligne + inconnue
status = scanner.classify_ip_status(is_online=False, is_known=False)
assert status == "offline"
@pytest.mark.asyncio
async def test_port_scan(self, scanner):
"""Test scan de ports"""
# Scanner des ports communs sur localhost
ports = [22, 80, 443, 9999] # 9999 probablement fermé
open_ports = await scanner.scan_ports("127.0.0.1", ports)
# Au moins vérifier que la fonction retourne une liste
assert isinstance(open_ports, list)
# Tous les ports retournés doivent être dans la liste demandée
for port in open_ports:
assert port in ports
def test_get_mac_vendor(self, scanner):
"""Test lookup fabricant MAC"""
# Tester avec des MACs connus
vendor = scanner._get_mac_vendor("00:0C:29:XX:XX:XX")
assert vendor == "VMware"
vendor = scanner._get_mac_vendor("B8:27:EB:XX:XX:XX")
assert vendor == "Raspberry Pi"
# MAC inconnu
vendor = scanner._get_mac_vendor("AA:BB:CC:DD:EE:FF")
assert vendor == "Unknown"

76
tests/test_scheduler.py Executable file
View File

@@ -0,0 +1,76 @@
"""
Tests pour le scheduler APScheduler
"""
import pytest
import asyncio
from backend.app.services.scheduler import ScanScheduler
class TestScheduler:
"""Tests pour le scheduler"""
@pytest.fixture
def scheduler(self):
"""Fixture scheduler"""
sched = ScanScheduler()
yield sched
if sched.is_running:
sched.stop()
def test_scheduler_start_stop(self, scheduler):
"""Test démarrage/arrêt du scheduler"""
assert scheduler.is_running is False
scheduler.start()
assert scheduler.is_running is True
scheduler.stop()
assert scheduler.is_running is False
def test_add_ping_scan_job(self, scheduler):
"""Test ajout tâche ping scan"""
scheduler.start()
async def dummy_scan():
pass
scheduler.add_ping_scan_job(dummy_scan, interval_seconds=60)
jobs = scheduler.get_jobs()
job_ids = [job.id for job in jobs]
assert 'ping_scan' in job_ids
def test_add_port_scan_job(self, scheduler):
"""Test ajout tâche port scan"""
scheduler.start()
async def dummy_scan():
pass
scheduler.add_port_scan_job(dummy_scan, interval_seconds=300)
jobs = scheduler.get_jobs()
job_ids = [job.id for job in jobs]
assert 'port_scan' in job_ids
def test_remove_job(self, scheduler):
"""Test suppression de tâche"""
scheduler.start()
async def dummy_scan():
pass
scheduler.add_ping_scan_job(dummy_scan, interval_seconds=60)
# Vérifier présence
jobs = scheduler.get_jobs()
assert len(jobs) == 1
# Supprimer
scheduler.remove_job('ping_scan')
# Vérifier absence
jobs = scheduler.get_jobs()
assert len(jobs) == 0